Les vulnérabilités XSS, omniprésentes et très communément remontées lors d’audits de sécurité, pentests et Bug Bounty, restent mal considérées et sous-évaluées. Les protections et spécificités des navigateurs modernes ainsi que les contre-mesures applicatives complexifient la conception de payloads génériques. Cet article vise à présenter la dissection d’un payload dans un contexte (très) contraint et filtré rencontré lors d’un audit.
1. Introduction et contextualisation
L’injection de code JavaScript arbitraire côté client, au travers d’une réflexion (Reflected-XSS) ou du stockage persistant (Stored-XSS) de la charge utile au sein d’un applicatif web, reste depuis maintes années l’une des faiblesses les plus communément observées et remontées par les chasseurs de vulnérabilités.
Les XSS permettent de compromettre le contexte de navigation des victimes. Ce sont des vulnérabilités qui impactent le côté client (et non pas l’applicatif côté serveur). Les réelles victimes sont en conséquence les internautes, visiteurs, utilisateurs d’un site web, et non pas le site web lui-même qui n’est que le vecteur de transmission de la XSS.
La traditionnelle fonction « alert(1) » suffit amplement à prouver l’exploitabilité d’une XSS dans 99 % des cas. Tout auditeur, pentesteur ou bug hunter se limite à déclencher une alertbox afin de justifier la présence d’une telle vulnérabilité.
C’est au détour d’une injection XSS décelée au cours d’un bug bounty, dans un contexte particulièrement filtré résultant en une simple alertbox, que j’ai eu l’occasion d’échanger avec le client (triager) sur les possibilités d’accroître la criticité/dangerosité de la charge utile pour étendre ses possibilités, la rendre invisible et ouvrir la voie à d’autres attaques...
1.1 Rappel concernant les XSS
« Compromettre le contexte de navigation des victimes », qu’est-ce que cela signifie ?
Le JavaScript est le langage principal interprété par les navigateurs (Chrome, Firefox, IE/Edge, etc.) permettant d’intégrer du dynamisme durant la navigation des utilisateurs. C’est via ce langage que le côté statique (page HTML) du Web 1.0 a évolué par le Web 2.0 où les sites web réalisent des myriades de traitements et d’opérations influant sur le rendu des pages dans les navigateurs (appels API, AJAX, mises à jour dynamiques du DOM, etc.).
Lorsqu’une requête est réalisée depuis votre navigateur à destination d’un site web, la réponse intègre un code source (HTML, CSS, JS, etc.) considéré comme « statique », telle une « photo figée » qui va être interprétée par votre navigateur afin d’afficher la page visuellement (c’est la source de la page, visible avec un Ctrl+U). Une fois chargée dans votre navigateur, cette page peut changer, s’auto-modifier et évolue dynamiquement tel un « film » : c’est le DOM (Document Object Model), dont l’état courant peut être consulté avec la « console developer » accessible via F12.
Un attaquant qui exploite une XSS cherche à incorporer un code JavaScript arbitraire/malicieux dans les réponses aux requêtes que des victimes réalisent sur un site web, pour altérer le DOM de ces visiteurs et voler leurs cookies, modifier le rendu des pages, les rediriger à leur insu vers des sites tiers, etc. : ils contrôlent le contexte de navigation.
Pour rappel (non exhaustif), une XSS permet de réaliser des attaques de :
- Ad-Jacking, en injectant des publicités invasives afin de générer des revenus à l'insu du site légitime ;
- Click-Jacking, ajout d’une surcouche (overlay) cachée sur une page pour hijack (capturer / manipuler) des clics d'une victime ;
- Session Hijacking, en dérobant les cookies de session des victimes non protégées par le drapeau HttpOnly ;
- Content-Spoofing, en altérant le rendu des pages, le DOM, toutes les ressources côté client pour afficher un autre contenu ;
- Credential Harvesting, en modifiant le rendu pour afficher une fausse page/mire d'authentification en vue de dérober les crédentiels d'une victime ;
- Forced Downloads, pour forcer le téléchargement d'un fichier malicieux sur un domaine de confiance / légitime ;
- Crypto Mining, en exploitant les ressources du CPU des victimes afin de générer de la cryptomonnaie ;
- Anti-CSRF bypass, contournement de toutes les protections anti-CSRF (token dans des forms, en cookies, referer, etc.) ;
- Keylogging, en définissant des events JavaScript capturant les frappes du clavier sur la page impactée ;
- Recording audio, avec les évolutions de JavaScript et HTML5, il est possible d'accéder au microphone de la victime et d'enregistrer / espionner les échanges vocaux (nécessite une autorisation) ;
- Taking pictures, accès à la webcam de la victime et espionnage (nécessite une autorisation) ;
- Geo-location, accès à la géolocalisation de la victime (nécessite une autorisation) ;
- Stealing HTML5 web storage data, une XSS permet d'atteindre et de dérober le contenu des nouveaux lieux de stockage locaux de données utilisés par les applications modernes, dont window.localStorage() et window.webStorage() ;
- Browser & System Fingerprinting, récupération du browser name, version, plugins installés et leurs versions, système d'exploitation, architecture, heure, langue, résolution d'écran, etc. ;
- Network scanning, via une XSS un attaquant a la main sur le site chargé dans le navigateur d'une victime, donc il se situe dans le réseau de la victime et peut scanner des ranges d'IP et ports à sa convenance (via JavaScript) [BEE] ;
- Crashing Browser, simplement pour nuire à des victimes, en réalisant des traitements très consommateurs en ressources ou via des exploits DoS destinés à une version précise ;
- Stealing information, récupération d'informations sensibles / personnelles sur une page web authentifiée sous le compte d'une victime, et les transmettre au serveur de l'attaquant ;
- Redirecting, une XSS permet la redirection d'une victime et donc engendre une Open-Redirect ;
- Tab-napping, en détectant l'activité d'une victime (pas de clic ni de touche clavier durant 1 minute, la victime a quitté son poste). En conséquence il est possible de remplacer la page courante par une autre à son insu ;
- Capturing screenshot, via HTML5 Canvas, il est possible de simuler la prise d'un screenshot complet de la page web courante (très employé avec les Blind-XSS) ;
- Perform Actions, l'attaquant ayant la main sur le navigateur, celui-ci peut réaliser toutes les actions souhaitées dans le contexte de la victime. Sur un réseau social : poster des messages, XSS-worm, etc.
- Black-SEO, avec les « bots » tels que le Google-Bot qui reposent sur des navigateurs headless (et donc interprète le JavaScript), il est possible de nuire au ranking et référencement d'un site web en injectant et référençant des URL comprenant des XSS qui injectent du contenu nuisible (Viagra, etc.) [TOM] ;
- Brute-force / user-enumeration distributed, en exploitant une XSS et des faiblesses au niveau des CORS, un attaquant peut profiter de toutes les victimes de son XSS pour brute-forcer un espace authentifié tiers ou réaliser une énumération de manière distribuée avec la multitude d'adresses IP que lui fournissent ses victimes.
Les XSS permettent d’aller encore plus loin avec des cadriciels (frameworks) dédiés tels que BeEF [BEE]. Un précédent article MISC illustre comment une simple XSS-GET réfléchie permet l’obtention d’un reverse-shell root sur la distribution firewall-routeur pfSense [MIS].
1.2 Forme canonique d’illustration
La forme dite « canonique » d’illustration de la présence d’une XSS consiste à déclencher (trigger) une simple boîte de dialogue (alertbox, promptbox, confirmbox…) : <script>alert(1);</script>.
Ce payload traditionnel et bien connu des solutions de sécurité, dispose de très nombreuses variantes lorsque celui-ci est filtré. En effet, en présence d’un WAF (Web Application Firewall), il peut être nécessaire de modifier / réécrire la charge utile pour contourner les protections mises en place.
La démarche générique de réécriture est la suivante :
- Identifier une balise HTML qui supporte des évènements JavaScript non-filtrée par le WAF ;
- Identifier un évènement JavaScript compatible avec la balise HTML non-filtré par le WAF ;
- Rédiger le payload JavaScript lui-même en contournant les filtres du WAF.
À titre d’exemple, si une réflexion d’une entrée utilisateur est décelée sur une cible web, mais que le payload <script>alert(1);</script> est filtré, l’attaquant peut chercher à utiliser d’autres balises HTML qui elles seraient autorisées, comme la balise <img> ou <details>.
Puis il chercherait à identifier les évènements JavaScript non-filtrés et autorisés sur ces balises tels que onerror, open ou ontoggle.
Pour enfin réaliser son alertbox en la réécrivant d’une manière à contourner les filtres en place :
Un tel résultat peut être obtenu via un nombre incalculable d’autres charges utiles, permettant de contourner des protections en place, des briques de sécurité telles que les WAF. Quelques exemples produisant des alertbox :
Le langage JavaScript est particulièrement malléable et permissif, permettant une infinité de réécritures d’une même instruction et facilitant l’obfuscation de son code, et donc le contournement des filtres de sécurité.
1.3 Les XSS durant les audits et Bug Bounty
La qualification des XSS et notamment de leur criticité reste un sujet épineux et à débats depuis de longues années :
- La méthode de scoring CVSS est-elle vraiment adaptée pour ce genre de vulnérabilité ?
- Une XSS se déclenchant dès le chargement de la page (onload) doit-elle être considérée sans interaction utilisateur (UI:None) contrairement à une XSS qui se déclenche via l’évènement « onclick » ?
- Le périmètre de l’attaque est-il à considérer comme changé (Scope:Changed) puisque les actions malicieuses issues de la XSS portent dorénavant sur le contexte de navigation des victimes et non plus sur le site web de la cible initiale ?
Bien que la qualification ne soit pas le sujet principal de ce présent article, il est hélas constaté que bien trop souvent, les clients (triagers) sous-estiment celles-ci, tendent à les rabaisser voire même ne les considèrent pas du tout (out-of-scope).
Les chasseurs de failles, quant à eux, s’arrêtent généralement à simplement déclencher une alert(1) sans aller plus loin.
2. Une XSS particulièrement filtrée
2.1 À la recherche d’une alertbox...
C’est au cours d’un audit / bug bounty, qu’une réflexion d’une entrée utilisateur via un simple paramètre GET a été découverte.
Par souci d’anonymisation, considérons l’URL de l’injection telle que :
Où la réflexion dans la réponse se situait dans un contexte tel que :
Au regard de cette réflexion, où il n’était pas possible de s’évader de la balise courante (< et > filtrés), des payloads standards ont été testés en espérant déclencher une alertbox dès le chargement de la page, tels que :
Pour tenter de produire la réflexion syntaxiquement valide suivante :
Seulement, un premier constat a été fait : les parenthèses ( et ) étaient filtrées.
Pour contourner l’usage des parenthèses, la backquote ` peut être employée : alert`1`, mais filtrée également…
Une autre forme d’injection sans parenthèses consiste à redéfinir le déclenchement des erreurs JavaScript. L’idée est de redéfinir la fonction à déclencher lors de l’évènement « onerror » par « alert », puis de déclencher (throw) volontairement une erreur. Ainsi : onerror=alert;throw 1;.
Les points-virgules ; étaient filtrés. Nouvelle réécriture sans points-virgules : {onerror=alert}throw 1 voire même avec l’usage d’expressions où la redéfinition du « onerror » intervient après le déclenchement de l’erreur : throw onerror=alert,1
Bingo ! La boîte de dialogue apparaît (bien qu’avec un préfixe Uncaught ou Uncaught exception:) ! Quelques minutes plus tard, la vulnérabilité était détaillée, son contexte, sa criticité, ses remédiations étaient transmis au client-triager.
MAIS ! C’est là que le client-triager, que je garderai anonyme (mais salue, et remercie grandement pour les échanges très constructifs que nous avons eu ensemble), a souhaité creuser cette injection en ma compagnie pour l’élever à un autre stade :
- Oui, une alertbox est possible via cette injection où les parenthèses, backquotes et point-virgules sont filtrés.
- Mais est-il possible d’élever cette injection pour ne plus faire une « alert », mais un « eval » et donc ouvrir la voie à un quelconque réel payload malicieux ?
2.2 Exception handling et browsers...
Pour répondre à ce nouveau challenge, l’idée de simplement redéfinir le « onerror » avec « eval » plutôt que « alert » semble la plus naturelle. Seulement, en pratique, il en résulte que ça ne fonctionne pas et c’est loin d’être aussi simple...
Le payload initial était voué à faire un simple « alert(1) ». Seulement, comme visible sur la figure 2, chaque navigateur réagit différemment. Certes, une alertbox est visible, mais pour chacun d’eux le contenu de celle-ci diffère où un préfixe apparaît :
- Chrome affiche Uncaught 1, tout comme Edge ;
- Internet Explorer affiche uniquement 1, comme attendu ;
- Firefox affiche Uncaught exception: 1.
Ainsi, remplacer le onerror=alert par un onerror=eval se traduit par une erreur de syntaxe. eval("Uncaught 1"); ou eval("Uncaught exception: 1"); sont syntaxiquement invalides : le moteur JavaScript cherche la variable « Uncaught » qui s’avère indéfinie.
Pour satisfaire Chrome et Edge, une variable nommée « Uncaught » se doit d’être définie pour ne pas causer d’erreur. De plus, un séparateur devra terminer l’instruction « Uncaught » avant l’interprétation du réel payload. La forme suivante est donc fonctionnelle pour ces navigateurs : throw onerror=Uncaught=eval,"\x3balert\x282\x29".
Firefox oblige, quant à lui, que le dernier paramètre (réellement transmis au eval), soit un objet de type « Exception » ou hérité. C’est donc l’attribut « message » de cet objet qui est évalué implicitement : throw onerror=eval,e=new Error,e.message="alert\x282\x29",e.
Une telle approche est fonctionnelle pour évaluer un payload totalement arbitraire via la levée d’exception JavaScript au travers de throw, onerror et eval, sans aucune parenthèse, point-virgule ou backquote.
Cependant, de nouveaux caractères exotiques entrent en jeu et complexifient l’ensemble, tels que les quotes, double-quotes, anti-slash, etc.
De plus, les payloads sont propres à des navigateurs précis, afin de respecter les spécificités de ceux-ci.
En partant de ces constats, l’idée est de concevoir un payload unique, générique, plus évolué, compatible avec tous les navigateurs, le plus court possible et utilisant le moins de caractères exotiques potentiellement filtrés.
3. Conception du payload
3.1 Anonymisation et invisibilité de l’injection
Pour faciliter l’exécution du payload final (que l’on qualifiera de « stage 2 »), l’idée est que le payload initial (stage 1) fasse office de « dropper / jumper / stagger ». Autrement dit, que la charge utile soit la plus générique possible pour sauter et évaluer un code JavaScript tiers, localisé ailleurs.
Lorsque de réels attaquants exploitent des XSS à l’encontre des visiteurs finaux d’un site web, des traces sont laissées :
- Les équipements réseaux en coupure peuvent journaliser toutes les requêtes GET / POST incluant les payloads et révélant les méfaits des attaquants (proxy, reverse-proxy, load-balancer...) ;
- Les journaux du serveur web (Apache, Nginx, etc.) enregistrent toutes les URL avec les paramètres GET par défaut, révélant les payloads malicieux.
Toutefois, il faut garder à l’esprit que les attaques XSS sont uniquement côté client. Ainsi, il est possible d’utiliser l’ancre « # » (location.hash) au sein des URL pour y dissimuler le réel payload malicieux (stage 2) : celui-ci ne transite jamais sur le réseau et n’est jamais journalisé côté serveur.
Une telle exploitation consiste donc à produire un payload stage 1, qui évalue ce qui se trouve dans le location.hash (stage 2), garantissant l’anonymat et l’invisibilité de la charge malicieuse finale.
L’idée est donc de produire un eval("/*"+location.hash), avec dans l’URL : #*/;alert(3); pour une évaluation finale telle que : eval("/*#*/;alert(3);"). Le location.hash débutant toujours par un #, l’utilisation des commentaires /* et */ permet d’éviter une erreur de syntaxe du eval (la méthode substring(1) n’est volontairement pas utilisée pour ne pas ajouter de parenthèse).
Adapté à la charge utile sans parenthèses de notre contexte, cela donne, avec dans l’URL d’appel #*/;alert(3); :
- Pour Chrome / Edge :
- Pour Firefox :
3.2 One string to rule them all
Comme vu précédemment, la gestion des Exceptions sous Chrome/Edge diffère de Firefox. Chrome et Edge acceptent de throw une simple string, alors que Firefox impose un objet.
Pour concevoir un payload unique de type « dropper » vers le location.hash compatible avec tous ces navigateurs, il est nécessaire de disposer d’un élément discriminant qui permettrait d’identifier que l’exécution est sous Firefox ou non.
En JavaScript pur, une méthode simple pour savoir si l’exécution courante est sous Firefox consiste à vérifier la condition suivante : typeof InstallTrigger !== 'undefined'; [STA].
En effet, l’objet InstallTrigger existe dans tous les contextes d’exécution JavaScript au sein des moteurs Mozilla Firefox. Cette condition peut se simplifier ainsi : !!window.InstallTrigger.
Les payloads dropper initiaux (pour Chrome / Edge et Firefox) peuvent donc fusionner en un seul avec l’utilisation d’une condition ternaire :
Sous Chrome / Edge, la condition est fausse, ainsi c’est le « e.message » (string) qui est évalué ; alors que sous Firefox, l’objet « e » est « throw » comme attendu.
3.3 No *quote
Les chaînes de caractères peuvent être évincées en les reconstruisant à partir de portions de variables préexistantes. L’ancre s’y prête parfaitement #*/;alert(3); où le /* peuvent être reconstruit grâce à h[2]+h[1] :
3.4 No space
Dans le cas où les espaces ne sont pas autorisés, ceux-ci peuvent également être contournés. Deux espaces principaux sont problématiques, celui suivant le « throw » et celui après le « new » lors de l’instanciation de l’objet « Error ».
Pour le throw, il est possible d’évincer l’espace via des commentaires /**/, une RegExp /x/, ou un objet vide {},.
Pour le new, un changement de syntaxe afin de créer l’objet et chacun de ses attributs peut être employé :
3.5 No curly bracket
D’autres variantes en fonction des contextes et des filtres peuvent être conçues. Exemple pour la suppression des accolades, la création de l’objet e s’effectue en copiant un objet préexistant (URL) et en lui définissant les attributs attendus manuellement.
Conclusion, protection, mitigation et contre-mesure
La conception de charges utiles destinées à l’exploitation de vulnérabilités XSS peut vite se complexifier lorsque des filtres de caractères ou des solutions de sécurité (WAF) sont en place.
La malléabilité et les possibilités quasi infinies d’obfuscation et de réécriture offertes par le langage JavaScript ouvrent la voie à une multitude de contournements possibles [POR].
Certes, dans 99 % des cas, produire une alertbox suffit à prouver la présence d’une XSS. Toutefois la conception de payloads plus évolués / génériques (eval) peut rapidement devenir un vrai casse-tête.
Par souci d’invisibilité et afin de camoufler au maximum ses traces (dans les journaux et sur le réseau), l’emploi de charges utiles à étages jusqu’à l’ancre (#) est une technique prisée des réels attaquants.
Le présent article démontre comment un payload générique anonyme peut être conçu et exécuté, compatible cross-browsers, en contournant le filtre principal des parenthèses ainsi que des filtres secondaires (quotes, double-quotes, backquotes, espaces, slashs, anti-slashs, accolades, point-virgules, etc.).
En ce qui concerne les vulnérabilités de type XSS, l’adage « Never trust user inputs » reste toujours d’actualité. Les technologies web actuelles offrent une multitude de méthodes, fonctions, bibliothèques ou frameworks permettant de prémunir ce type d’injection.
Il est recommandé de ne pas réfléchir par liste noire pour protéger les entrées, mais par liste blanche ; ainsi que de toujours valider, contrôler, échapper ces entrées en vérifiant leur format et leur type.
Certains navigateurs (côté client) intègrent des mécanismes de sécurité anti-XSS intrinsèques qu’il convient d’activer, ainsi que d’utiliser les CSP (Content-Security-Policy) et ajouter des briques de sécurité en coupure telles que les WAF.
Pour finir, je tenais à saluer tous ceux qui se sont creusé les méninges en ma compagnie pour la conception de ces payloads cross-browsers sans parenthèses [POR], un grand merci notamment à @RenwaX23 [REN], @terjanq et @Garethheyes [GA1][GA2] pour ces échanges.
Références
[REN] RenwaX23, XSS-Payloads Without Parentheses, https://github.com/RenwaX23/XSS-Payloads
[POR] Portswigger, Cross-site scripting (XSS) cheat sheet,
https://portswigger.net/web-security/cross-site-scripting/cheat-sheet
[GA1] Garethheyes, How to abuse throw and onerror on Firefox,
https://twitter.com/garethheyes/status/1126526480614416395
[GA2] Garethheyes, Another way to use throw without a semi-colon,
https://twitter.com/garethheyes/status/1126922526796468224
[TOM] TomAnthony, XSS attackers Googlebot index manipulation,
http://www.tomanthony.co.uk/blog/xss-attacks-googlebot-index-manipulation/
[BEE] BeEF, The Browser exploitation framework project, https://beefproject.com/
[MIS] Yann CAM, « pfSense : obtention d’un reverse-shell root à partir d’une XSS », MISC n°94, novembre 2017, https://connect.ed-diamond.com/MISC/misc-094/pfsense-obtention-d-un-reverse-shell-root-a-partir-d-une-xss
[STA] StackOverflow, How to detect Safari, Chrome, IE, Firefox and Opera browsers?,
https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browsers