HeroCTF v3 — System Write-up : PrivesCorp #4: Go Deep

Les Pires Hat
4 min readApr 30, 2021

Du 23 avril au 25 avril 2021 a eu lieu la troisième édition du HeroCTF (page d’accueil du CTF). Notre équipe a terminé 2ème au classement général (première place au classement étudiant) sur plus de 600 équipes.

Nous allons voir dans ce write-up comment résoudre le challenge “PrivesCorp #4: Go Deep” créé par Log_s.

PrivesCorp #4: Go Deep

Pour ce challenge, Log_s nous fournit l’énoncé suivant :

The layoff list was leaked… The reponsible hr was let go, but before that, he barricaded very important information on his account. If you could get to it, there would probably be a promotion in sight. Your credentials on PrivesCorp’s network -> bob:password123

En effet, pour accéder à ce challenge, nous avons d’abord résolu les étapes #1, #2 et #3, où nous “incarnons” Bob, un nouvel employé d’une entreprise nommée PrivesCorp. Dans le premier challenge, nous devons réussir à retrouver le contenu d’un fichier verrouillé, dans le second nous sommes payés pour voler des données pour l’Hackonfrerie et dans le troisième nous devons savoir si nous sommes sur la liste des employés à licencier (Write-Ups officiels #1, #2, #3).

Nous essayons donc de nous connecter en SSH à la machine :

ssh bob@chall0.heroctf.fr -p 5004 # password : password123

Mais la connexion se ferme instantanément, nous répondant de ”demander poliment”. On réessaie donc avec un argument (n’importe lequel, disons ”please” pour le côté poli) :

ssh bob@chall0.heroctf.fr -p 5004 please # password : password123

Cette fois-ci, la réponse change :

That’s better, but what’s the magik word ?

Après quelques tests et des arguments plus ou moins longs, on récupère une erreur :

Traceback (most recent call last):
File “/home/bob/shell.py”, line 32, in <module>
if(ssh_arg[3]+ssh_arg[4]+ssh_arg[5] != chr(97)+chr(109)+chr(101) or re.match(re.compile(“^[^A-RT-Za-z0–9]e[s].$”), ssh_arg[0]+ssh_arg[1]+ssh_arg[2]+ssh_arg[3]) == None):
IndexError: string index out of range

Grâce à cette erreur, on va pouvoir déduire le ”mot magique”. La première condition demande à ce que les arguments 4, 5 et 6 soient les caractères 97, 109 et 101 (en unicode) soit ame. La deuxième condition vérifie le début du mot grâce à une regex : le premier caractère ne doit pas faire partie de [A-RT-Za-z0–9], c’est donc Ses. En combinant les deux on obtient :

ssh bob@chall0.heroctf.fr -p 5004 Sesame # password : password123

Nous sommes connectés ! Mais on découvre vite que l’épreuve n’est pas finie (évidemment, pour 300 points, ce n’est pas aussi simple) :

Hmmm I should have bariccaded myself better then that… Now that you are here, you can’t get out, so it doesn’t matter ;). I made sure the document is well hidden./===================\
|| Welcome to jail ||
=====================
|| | | | |H| | | | ||
=====================
bob@godeep >

C’est une pyjail, nous pouvons donc exécuter du code Python 3. Après avoir testé plusieurs choses, nous nous rendons vite compte que __builtins__, __globals__, [] (et d’autres) sont bannis. De même, en testant la commande dir(), on ne nous renvoie que les (), ce qui implique que le mot dir est remplacé par du vide. On teste également flag qui ne nous renvoie que du vide, de la même manière. Aucune variable que l’on définit n’est gardée. De plus, toute commande de plus de 39 caractères renverra une erreur et fermera la connexion SSH. Après plusieurs autres tests non concluants (notamment ().__class__.__base__.__subclasses__() qui nous renvoie une liste de méthodes qui nous sont inutiles), on essaie :

> didirr()
— -> dir()
[‘TO_KEEP’, ‘_86924’, ‘__annotations__’, ‘__builtins__’, ‘__cached__’, ‘__doc__’, ‘__file__’, ‘__loader__’, ‘__name__’, ‘__package__’, ‘__spec__’, ‘argv’, ‘clear_vars’, ‘cmd’, ‘flag’, ‘forbidden’, ‘re’, ‘res’, ‘ssh_arg’, ‘vars’]

On peut voir une variable nommée TO_KEEP. On essaie de la print, mais on ne nous renvoie que vide. On essaie à nouveau, avec la même technique que pour dir :

> TOTO_KEEP_KEEP
— -> TO_KEEP
{‘__name__’: ‘__main__’,
…,
‘ssh_arg’: ‘Sesame’, ‘forbidden’: [‘TO_KEEP’, ‘dir’, ‘flag’, ‘_86924’, ‘secret/879.txt’], ‘TO_KEEP’: {}, ‘flag’: ‘fake{NOP_LOL}’, ‘clear_vars’: <function clear_vars at 0x7f76e9e21040>}

Inutile de taper flaflagg, puisqu’on peut lire fake{NOP_LOL}. Il semble cependant y avoir un fichier nommé secret/879.txt, mais nous n’avons pas le droit de taper le nom de cette manière, plutôt secret/secret/879.txt879.txt (par exemple). Pour lire ce fichier, on exécute :

open(“secret/secret/879.txt879.txt”).read()

Mais cela ne fonctionne pas, car open est en réalité une string. On peut voir une autre variable donc le nom est interdit : _86924. Taper _869_8692424 nous renvoie <built-in function open> ! La fonction dont nous avons besoin.

_869_8692424(“secret/secret/879.txt879.txt”).read()

Et… ça fait plus de 39 caractères. Mais nous avions une variable, flag, qui est plus courte que _869_8692424. On exécute donc :

flaflagg = _869_8692424 # ou flaflagg = "secret/secret/879.txt879.txt"
---> flag=_86924
<built-in function open>
flaflagg("secret/secret/879.txt879.txt").read()
---> flag("secret/879.txt").read()
Hero{H0w_d1d_u_g3t_0u7}

On obtient le flag : __Hero{H0w_d1d_u_g3t_0u7}__

Contactez-moi :
Twitter de l’équipe : LesPiresHat

--

--