Présentation d’un write-up de résolution du challenge « Web – Mr. WorldWide » de la BreizhCTF 2018.
Durant la nuit du 20/04/2018 se déroulait la BreizhCTF 2018 sous forme d’un CTF Jeopardy. Ayant eu l’occasion et le temps d’y participer avec quelques collègues et amis, voici un write-up de résolution d’un des challenges.
- Catégorie : Web
- Nom : Mr. WordlWide
- Description : Hire me muthafucka!
- URL : https://148.60.87.243:25000
- Points : 300
tl;dr; : Générer un « magic hash » sur la base de hash_hmac(« md5 ») pour exploiter une file disclosure.
Ce challenge nous a résisté durant de longues heures. Alliant web et crypto, nous avons dû employer du brute-force pour calculer des « magic hash ». Notre équipe a été la seule à réussir celui-ci jusqu’à la fin de du CTF :
En se rendant à l’URL principale, nous tombions sur le CV internationalisé / multilingue de Oussama Fayrir :
Ce CV s’avérait plutôt statique. En analysant la source, quelques commentaires attirèrent notre attention en bas de page :
Ces commentaires nous révèlent la présence de 3 variables GET avec lesquelles nous allons pouvoir jouer / interagir avec la cible :
- path : un chemin vers un répertoire (de langue) ;
- secret : une clé qui nous est encore inconnue ;
- lang : la langue courante de traduction (fr, en…).
Clairement, « path » et « secret » sont liés (appel au sein d’un même traitement), et « lang » influe sur un hash qui sert à la comparaison.
On note également d’après ces commentaires, que le système d’internationalisation de la langue n’est pas encore au point / achevé ; et qu’une faiblesse de sécurité semble s’y trouver.
En jouant avec des valeurs arbitraires sur ces variables GET on obtient des hashs dans le code source. Hash de 32 caractères, surement du MD5 :
La langue « fr » nous génère le hash « fab2987fd25eae8a126c4f38eb3dc5e8 » et ce hash semble comparé au hash d’une combinaison de « path » avec « secret ». Hum.
Ce n’est pas un simple appel à la fonction « md5() » de PHP, car le hash md5 de la chaîne « fr » diffère de celui qui nous est retourné. Très certainement du « hash_hmac() » MD5. En typant nos variables GET en tant que tableaux, il est possible de produire un Full Path Disclosure (FPD) et de révéler la fonction utilisée via un Warning PHP :
Bingo, c’est bien du hash_hmac qui est employé. Petit rappel du prototype :
string hash_hmac ( string $algo , string $data , string $key [, bool $raw_output = false ] )
En conséquence, on peut s’attendre à ce qu’une comparaison de hash de type hmac-md5 soit réalisée et que nous devons satisfaire cette condition avant d’aller plus loin :
hash_hmac("md5", $_GET["path"], $_GET["secret"]) == hash_hmac("md5", $_GET["lang"], $SECRET_LANG)
Nous avons la main sur la variable GET « path » et « secret » du premier appel à hash_hmac() (supposition), ainsi que sur la valeur (lang) du second appel sans pour autant connaître le secret associé.
Essayons de jouer avec la variable « lang » :
Arf… Nous n’avons pas tant de liberté que ça sur la valeur de « lang ». Bon, il nous faut donc trouver quelle langue utiliser parmis celles disponibles dans la page. Afin de valider la condition de comparaison des deux hash_hmac() précédents, nous partons sur l’hypothèse qu’une simple comparaison « == » est en place et non pas une vérification du type « === ». En conséquence, une faiblesse de type « PHP type Juggling » est très certainement présente.
Pour rappel, ce type de vulnérabilité porte sur l’opérateur « == » de PHP, un peu laxiste en termes de comparaison puisque PHP va essayer de typer lui-même certaines valeurs. Ce qui se traduit par des égalités atypiques, par exemple les chaînes suivantes sont égales :
"0e1337" == "0e7331"
La notation sous forme de string « 0e » suivie de chiffres correspond à la notation scientifique d’un nombre. En tant que chaîne de caractère, PHP va donc convertir à la volée cette chaîne en nombre et la condition de comparaison sera valide. L’idée est donc de trouver un hash de « lang » respectant ce format « 0eXXXXXXXX […] XXXXXXXXXX ».
Commençons par extraire toutes les langues supportées par l’applicatif :
Automatisons la génération de tous les hashs correspondant via Burp et l’application :
YES ! Une des langues proposées dans l’application, la langue « mni« , génère bien un « hash magic » via l’appel de hash_hmac. Ce hash est bien au format « 0e » suivi de chiffre uniquement :
hash_hmac("md5", "mni", $SECRET_LANG) => 0e010787602300372264769241728411
Le chemin à emprunter pour résoudre ce challenge se précise, un tel « magic hash » n’est pas là par hasard.
Nous avons donc très certainement la bonne valeur de « lang » pour générer le magic hash. A présent, il nous faut trouver la bonne combinaison de valeur « path » et « secret » qui nous génère également un magic hash au format « 0e » suivi de chiffre uniquement, ainsi la combinaison initiale sera respectée :
hash_hmac("md5", $_GET["path"], $_GET["secret"]) == hash_hmac("md5", $_GET["lang"], $SECRET_LANG)
La source et notamment les message d’erreur nous affichent un joli « Are you trying to LFI??? » nous permettant d’émettre l’hypothèse que si l’égalité des hashs est respectée, un appel à include(), require() (_once), file_get_contents() ou équivalent sera réalisé par la suite.
Cherchons une valeur d’intérêt pour la variable « path », et calculons la valeur de « secret » correspondante pour générer un magic hash :
- path=index.php (nous souhaitons récupérer le code source de cette page)
- secret=??? (brute-force oblige…)
- lang=mni (magic hash)
Fonction de brute-force (quick-n-dirty) :
<?php $maxLength = 20; $charSet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!_'; $size = strlen($charSet); $base = array(); $counter = 0; $baseSize = 1; $found = false; $path = "index.php"; while($baseSize <= $maxLength && !$found) { $found=false; for($i=0;$i<$size;$i++) { $base[0] = $i; $str=""; for($j=$baseSize-1;$j>=0;$j--) { $str.=$charSet[$base[$j]]; } $hm=hash_hmac('md5', $path, $str); if ($hm=="0" && !$found){ $found=true; echo "[+] YEAHHHH ! path=$path, secret=$str, hash_hmac_MD5=$hm"; } } for($i=0;$i<$baseSize;$i++) { if($base[$i] == $size-1) $counter++; else break; } if($counter == $baseSize) { for($i=0;$i<=$baseSize;$i++) { $base[$i] = 0; } $baseSize = count($base); } else { $base[$counter]++; for($i=0;$i<$counter;$i++) $base[$i] = 0; } $counter=0; } ?>
On démarre le brute-force et patientons (quelques minutes) :
Jackpot, un magic hash a été déterminé :
hash_hmac("md5", "index.php", "4PrOK") == hash_hmac("md5", "mni", $SECRET_LANG) "" == "0e010787602300372264769241728411" bool(true)
Générons l’URL et observons le résultat :
https://148.60.87.243:25000/?path=index.php&secret=4PrOK&lang=mni
Huhu, la condition est ok et le code PHP de l’index semble fuiter, consultons le depuis la source :
Nos hypothèses se confirment d’après le code source de l’index. Nous sommes bien en présence de deux fonctions hash_hmac() MD5 et le secret utilisé pour l’appel avec la variable « lang » est révélé « aabckklmoo« . C’est bien une simple comparaison « == » qui est visible entre les deux hashs, et si celle-ci est valide, un « file_get_contents() » du « path » indiqué (dans /var/www/html) est réalisé.
Très bien. Nous avons notre magic hash de la langue « mni« , nous avons le code source de l’index.php, nous avons notre « brute-forceur » (chronophage) pour déterminer le secret associé au « path », et nous avons notre exploitation de « file_get_contents() » pour récupérer des codes sources et contenu de fichier. Mais quel fichier inclure ?
Une multitude d’essais infructueux a été fait :
- flag.php
- Flag.php
- FLAG.php
- bzhctf.php
- .htaccess
- .htpasswd
- envoie.php (référence à ce fichier dans un JavaScript chargé dans l’application)
- Bazinga.php (puisque la photo du CV faisait référence à Sheldon Cooper)
- […]
Aucun de ces fichiers ne semblaient exister. Revenons-en aux bases, chargeons le « /etc/passwd » :
- path=../../../etc/passwd (remontée de répertoire puisque dans /var/www/html de base) ;
- secret=??? (brute-force oblige…) ;
- lang=mni (magic hash).
Après encore quelques minutes de brute-force, on récupère le secret correspondant permettant de générer un magic hash :
hash_hmac("md5", "../../../etc/passwd", "dqfdJ") == hash_hmac("md5", "mni", $SECRET_LANG) "0e521691631272858454605463704898" == "0e010787602300372264769241728411" bool(true)
URL :
https://148.60.87.243:25000/?path=../../../etc/passwd&secret=dqfdJ&lang=mni
YEAH ! Flagued !
bzhctf{Bru73f0rC1n9_hm4c_15n7_4_pUss35}
Chapeau à Estelle, Martin, Charles et Timothée pour celui-ci ?
Merci à toute l’équipe de la BreizhCTF pour l’organisation et la qualité des challenges !
Salutations à toute l’équipe, on remet ça quand vous voulez ? // Gr3etZ