Skip to content

Pyjails – Les sandbox python restreintes

Tout d’abord, je tiens à vous donner un lien vers la présentation de Switch : https://www.youtube.com/watch?v=17hEGxFamNY

C’est cette présentation qui m’a un peu motivé à faire l’article. Je vais essayer de la compléter ou de la reformuler avec mes mots (Désolé copain, yaura un peu de plagiat).

Ceux qui me connaissent savent que j’aime beaucoup python, et si vous ne le savez pas autant vous le dire : J’ADORE python.

Je trouve ce langage de programmation simple à utiliser et extrêmement complet.

Du coup, il y a un type de challenge que j’aime beaucoup aussi. Que ça soit en CTF ou sur des plateformes permanentes : Les Pyjails.

Mais c’est quoi une pyjail ?

Une pyjail est une sandbox (un environnement) restreinte en python où le but est d’ouvrir un shell ou de lire une variable précise.

Si le principe est simple, son application l’est moins car si la sandbox est dite “restreinte” c’est parce qu’elle nous limite dans nos exécutions. Que ça soit l’impossibilité d’utiliser les caractères ” et ‘ (donc pas de string), ou les builtins python re-définis.

On peut voir un exemple de pyjail sur mon Write-Up du BreizhCTF 2018

Ça a une réelle utilité ?

En soit, savoir résoudre une pyjail est inutile comparé au fait de savoir exploiter une injection SQL ou toute autre faille.

Malgré tout on peut trouver quelques réelles applications si on a une RCE via un serveur Flask ou tout autre moteur utilisant python.

Mais ce qui m’intéresse le plus dans les pyjails ça n’est pas les quelques applications… C’est plutôt les méthodes utilisées pour les résoudre qui peuvent être utiles dans beaucoup d’autre cas.

En effet, d’après-moi le but d’une pyjail est, avant tout, de comprendre son fonctionnement via des exécutions réfléchies. En d’autres termes : On fait du “reverse engineering” afin de deviner le code de la sandbox puis on en déduit une méthode de résolution.

Et si on parle toujours de résoudre une pyjail et jamais de l’exploiter c’est parce que c’est bel et bien un puzzle.

Aussi, je me retrouve régulièrement lors de CTF ou de pentest à me demander “Comment ça fonctionne ?” ou au contraire “Pourquoi ça ne fonctionne pas ?”. Je pense que les pyjails constituent un bon entraînement à ce genre de réflexion.

Comment on peut résoudre une pyjail ?

Pour faire cet article j’ai créé une petite pyjail. On va devoir partir de l’idée que je ne connais pas le code de cette pyjail et que je vais le découvrir avec vous au fur et à mesure de l’article.

Vous pouvez trouver cette jail sur mon GitHub. J’essaierai d’en faire d’autres pour que vous puissiez entraîner votre esprit de déduction et la méthode que je vous propose.

Les prérequis

Il y a quelques prérequis évidents :

  • Connaître la programmation. Le plus évident de tous j’ai envie de dire… Si on ne connait rien de la programmation il va être difficile de comprendre comment fonctionne la pyjail.
  • Connaître le fonctionnement de python et de ses objets. Une connaissance minimum de la chose est obligatoire, bien souvent c’est en remontant dans les objets et en utilisant des méthodes propres à python que l’ont obtient tous les éléments nécessaires.
  • Savoir lire la documentation. Parce que oui, on ne peut pas forcément tout connaître. J’ai régulièrement dû lire les documentations pour comprendre comment fonctionnent certaines méthodes ou ce que contiennent certains objets.
  • Ne pas oublier de faire des tests. C’est toujours assez intelligent de faire des tests sur sa machine. On est libre de toute restriction et on peut ainsi essayer d’exploiter le fonctionnement de certaines méthodes ou de certains objets.

Quelques fonctions & méthodes utiles

Certaines fonctions et méthodes sont régulièrement utilisées pour résoudre des jails. Elle ne sont pas tout le temps toutes disponibles mais c’est toujours utile de les connaître.

  • dir() : Renvoie une liste qui contient le nom de tous les objets de l’environnement actuel
  • getattr() : permet de récupérer dans un objet l’attribut/méthode avec un nom donné.
  • globals() : Renvoie le dictionnaire de l’environnement global dans lequel vous vous trouvez. La sortie sera la même depuis une fonction ou l’environnement
  • locals() : Renvoie le dictionnaire de l’environnement local dans lequel vous vous trouvez. La sortie sera différente si elle est lancée depuis une fonction
  • print() : Souvent, les pyjails ne font pas l’affichage automatique de ce qui est renvoyé par les fonctions. Il faut donc utiliser la fonction print() pour savoir ce qu’on obtient
  • __dict__ :  Méthode qui permet de récupérer le dictionnaire d’un objet. Quand des mots sont interdits, passer par des strings peut être utile et un dictionnaire nous permet de faire ça.
  • Les “payloads magiques” : C’est les payloads qu’on revoit régulièrement dans un peu toutes les pyjails. Elle correspondent plus à un chemin à suivre dans les objets python pour aller jusqu’au module system. Selon les environnement les index utilisés (ici 59) peuvent changer
    • ().__class__.__base__.__subclasses__()[59]()._module.__builtins__['__import__']('os').system()
    • (Ne fonctionne qu’en python2)
      ().__class__.__base__.__subclasses__()[59].__init__.im_func.func_globals["linecache"].os.system()

Explication des “payloads magiques”

J’ai longtemps utilisé ces payloads sans savoir exactement pourquoi on mettait ces valeurs là, pourquoi on allait chercher tel ou tel module. Et c’est pas forcément facile d’obtenir des explications… Donc autant mettre à profit mon temps de recherche et vous fournir toutes les explications. Et on va procéder étape par étape !

La partie commune

().__class__.__base__.__subclasses__()[59]

Tout d’abord le () est un tuple. Avec le __class__ on va pouvoir récupérer sa classe “globale” c’est à dire la classe tuple.

Depuis cette classe on va utiliser __base__ qui nous permet de récupérer la classe dont elle est issue (le principe de l’héritage en POO). Et la classe dont elle est issue est tout simplement la classe object, c’est à dire “La classe la plus basique” (d’après la doc). On ne peux pas aller plus bas.

Et ensuite, on va utiliser la méthode __subclasses__() pour trouver toutes les classes qui en hérite. Donc les classes tuples, dictionnaires, liste, … Et dans tout ça on va choisir une classe précise avec le [59] : La classe catch_warnings.

Si on choisit cette classe c’est parce qu’elle est chargée par défaut et qu’elle a énormément de référence. Notre objectif étant d’obtenir les modules os ou system on sait qu’en remontant ses référence on peut tomber dessus.

La première payload

...()._module.__builtins__['__import__']('os').system()

On a donc notre classe, avec le () on va venir l’instancier. Avant nous avions la classe catch_warnings qui était de type type. Maintenant nous allons avoir un objet de type catch_warnings.

Depuis cette instance, on va utiliser _module pour retrouver le module dont est issue notre classe. Il s’agit du module warnings. L’objectif de récupérer un module est qu’ils emportent tous avec eux les builtins de base (Ils embarquent en gros leur propre environnement).

On va donc pouvoir récupérer ces builtins avec __builtins__. Puis depuis les builtins on récupère le module qui permet d’importer avec [‘__import__’] (puisqu’il s’agit d’un dictionnaire).

Ce module fonctionne comme une fonction : en mettant en argument le module que l’on veut importer, on le récupère. Notre objectif étant le module os, on le récupère avec (‘os’) et on a la méthode system() qui nous permettra d’exécuter n’importe quelle commande système

La seconde payload

... .__init__.im_func.func_globals["linecache"].os.system()

Une fois qu’on a obtenu notre classe catch_warnings, on va utiliser une “instance method” c’est à dire une méthode qui a été déclarée dans la classe. Une méthode qui est pratiquement toujours déclarée est la méthode __init__, mais on peut aussi parfois avoir la méthode __repr__ ou __str__ par exemple.

Une fois la méthode récupérée, on va aller récupérer l’objet correspondant à sa fonction avec im_func. Puis dans cette objet on va récupérer func_globals qui nous permettra d’avoir l’environnement du module d’où vient notre classe (ici le module warnings)

Et le module warnings fait appel à un autre module : le module linecache. C’est ce module que l’on va récupérer avec [‘linecache’]. Le fait est que ce module linecache contient d’autre modules dont os ! On a donc plus qu’à appeler os puis sa méthode system() !

Si cette payload n’est possible qu’en python2 c’est parce que les attributs im_func et func_globals ne sont plus disponibles en python3.

Étape 1 : Découvrir l’environnement

Découvrir l’environnement est très important, c’est en sachant à l’avance ce à quoi on a le droit que l’on pourra établir une approche pour résoudre notre sandbox. On se retrouve régulièrement avec des fonctions manquantes, des mots ou des caractères interdits ou certaines variables ou fonctions supplémentaires.

Verifier les mots & fonctions interdits

Dans un premier temps, il temps vérifier tous les mots clefs, caractères ou fonctions qui sont interdits.

La pyjail exemple a pour cela quelque chose de très pratique : Elle affiche ce qui va être exécuté.

On va donc tester en pagaille un peu tout ce qui pourrait nous être utile.

On remarque que les caractères sont remplacés par des espaces alors que les mots-clefs ont un —LOLNOP—. Les mots ne sont pas juste supprimés mais remplacés.

Ici on a pas le droit :

  • Aux quotes ( et )
  • Aux chiffres
  • Aux différents “crochets” ( { } [ ] )
  • Aux caractères de calcul ( – + / * ^ )
  • Aux backslash ( \ )
  • Aux opérateurs logiques ( ! ? && || )
  • A certains mots clefs (locals, import, os, sys)

Certaines fonctions intéressantes fonctionnent bien :

  • print()
  • dir()

On voit aussi la fonction getattr() qui n’a pas l’air utilisable vu que l’on a pas le droit aux strings.

Trouver la version de Python

Si en général les pyjails sont en python2 (parce que les objets sont moins “sécurisés” que le python3),  il reste assez intéressant de savoir laquelle des deux version est utilisée.

Une méthode plutôt simple peut se faire avec la fonction print.

En effet, dans les deux versions de python la fonction print() existe et fonctionne (plus ou moins) de la même façon. MAIS, en python2, il existe le statement print.

Ce statement nous permet de faire des print sans utiliser les parenthèses (ce qui au passage n’est pas très propre et peu lisible). Donc si un print fonctionne sans les parenthèses comme le code qui suit, nous sommes bien en python2 :

print dir()

Dans le cas contraire nous serons dans une jail en python3.

Preuve python2 jail

Ici le statement print a bien fonctionné, nous sommes bien dans une jail en python2.

Obtenir les objets, fonctions, et variables prédéfinies

En réalité on a déjà en partie effectué cette étape auparavant, mais dans certaines jails elle reste très importante.

On a vu dans la liste des fonctions utiles dir(), globals() et locals().

Si l’une d’entre elles est activée, elle pourra nous donner tout ce qui est déjà déclaré dans l’environnement. Donc, ce qui a été prédéfini par le créateur du challenge.

Par exemple si je déclare une variable dans un interpréteur python :

Test de l'environnement python

On a vu que la fonction dir() était disponible pour nous. On va donc inspecter ce qu’elle nous donne… Avec dir() !

Dans notre cas, il n’y a pas grand chose. Seul dir et les Booléens peuvent nous être utiles.

Savoir ce que l’on peut déclarer

Notre objectif ici est de savoir si on peut déclarer des variables ou des fonctions et de voir si celles-ci restent après une exécution. Rien de compliqué, il suffit juste de faire des déclarations et de les tester. Dans notre cas, les strings et digits étant interdits on va utiliser la fonction dir() qui ne s’affiche pas sans print().

Les définitions fonctionnent bien mais elles ne restent pas après exécution dans notre cas.

Étape 2 : Contourner les interdits

Malheureusement, on ne peut pas juste lancer nos fameuses “payload magiques” parce qu’elles contiennent des caractères interdits… Il faut donc trouver un moyen de contourner ces interdictions

Obtenir des integers

Quelque chose qui peut nous être très utile c’est les nombres. Malheureusement dans nos tests on peut voir que les chiffres ET les caractères de calculs sont interdits.

Dans un premier temps on va essayer d’obtenir un chiffre (n’importe lequel) différent de zéro. Comme ça, si on arrive à faire des calculs on pourra avoir le 1 et donc n’importe quel chiffre.

Il existe surement plusieures façon de faire, et chacun peut trouver sa méthode en fonction de l’environnement. L’idée est de se servir des fonctions à notre dispositions et des méthodes de tous les objets que l’on peut obtenir.

De mon côté j’ai opté pour la payload qui suit :

for i in dir(dir()):
	x=dir(dir()).index(i)

L’idée est de parcourir une liste – ici, dir(dir()) – et de récupérer l’index de l’objet en cours de traitement (i). En utilisant la méthode index() pour rechercher l’index de i dans la liste, on obtient un nombre.

Je ne l’ai pas fait sur la liste de dir() simplement parce qu’il n’y avait qu’une seule valeur et qu’on obtiendrait que l’index 0 (c’est aussi la raison pour laquelle je n’ai pas mis de break).

On obtient avec ça une variable x avec un valeur de 44.

Bon, c’est bien beau, mais il ne faut pas juste un nombre, il faut pouvoir en calculer d’autre et les +, -, /, * et compagnie on y a pas le droit. On va donc fouiller dans les méthodes des integers en python (dans un interpréteur à côté histoire que ça soit moins galère).

Certaines méthodes nous interpellent par leur nom :

  • __add__
  • __div__
  • __mul__
  • __sub__

et tous leurs dérivés. On va regarder dans la doc et on trouve le fonctionnement.

a.__add__(b) == a + b
a.__sub__b) == a - b
a.__mul__(b) == a * b
a.__div__(b) == a / b

Et heureusement pour nous, ces méthodes sont autorisées dans la jail. On peut donc obtenir un 1 et donc à partir de celui-ci, tous les nombres nécessaires.

Récupérer une valeur dans une liste

Là encore on va fouiller dans les méthodes des listes.

On trouve une méthode très intéressante qui est __getitem__. Cette méthode, d’après la doc, nous permet d’appeler self[key] avec key notre argument. Donc elle correspond parfaitement à nos besoins.

my_list.__getitem__(5) == my_list[5]

De manière plus simple on peut aussi utiliser la méthode pop() qui prend en argument un index et nous renvoie l’élément correspondant dans la liste (mais va l’enlever de la liste si celle-ci n’est pas “temporaire”, attention à vos variables).

Récupérer une valeur dans un dictionnaire

Pas besoin de partir très loin pour avoir les valeurs d’un dictionnaire. Puisque dans nos prérequis se trouvent “Connaître le python”, on sait qu’il existe pour les dictionnaires les méthodes keys() et values().

  • keys() : Renvoie la liste des clefs d’un dictionnaire
  • values() : Renvoie la liste des valeurs d’un dictionnaire

La méthode qui va nous intéresser ici est la méthode values(). Pour faire simple on va juste considérer que notre dictionnaire sera converti en liste. Ainsi pour récupérer une valeur on se référera à la partie “Récupérer une valeur dans une liste”

Obtenir une chaîne de caractères

Pour cette partie on ne veut pas juste se contenter d’obtenir n’importe quelle chaîne de caractères. Nous ce qu’on veut c’est forger notre propre chaîne (pour faire une commande système par exemple).

En voulant récupérer une valeur dans une liste on a trouvé la méthode __getitem__. Et cette méthode n’est pas spéciale aux listes mais elle se trouve sur tous les objets (d’après la documentation toujours). L’idée est donc de pouvoir récupérer n’importe quelle lettre dans une string avec cette méthode.

Malgré tout, récupérer une lettre ne nous aidera pas, il faut former un mot et les caractères comme + pour concaténer sont interdit… Et là encore on va utiliser une méthode déjà vue plus haut : la méthode __add__

'abcd'.__getitem__(2) == 'c'
'a'.__add__('b') == 'ab'

C’est bien beau tout ça, mais vous allez me dire «Comment qu’on fait pour récuperer la lettre d’une chaîne de caractères sans chaîne de caractères ?» et vous avez totalement raison !

En fait on a plein de fonction et méthodes qui nous renvoient une chaîne de caractères ou une liste de chaîne de caractères. Par exemple notre belle fonction dir() nous renvoie une liste de strings ! Pour ce qui est des caractères vraiment spéciaux comme les slashs ( / ), on peut les trouver dans l’attribut __file__ des modules.

Bypass l’interdiction des mots clefs

Ici on peut avoir plusieurs méthodes l’objectif étant de ne pas avoir à écrire le mot clef ou de l’écrire de façon fractionnée. Mais il y a 2 méthodes qui sont plus facilement utilisables :

  • Utiliser la fonction getattr() : Il faut que la fonction soit dans les builtins (ce qui n’est pas notre cas) mais elle est plutôt simple d’utilisation. Par exemple pour obtenir la méthode system() à partir de l’objet os on va faire
    • getattr(os,'system')
  • Utiliser la méthode __dict__ : Le principe est quasiment le même mais plus souvent utilisable. On va transformer notre objet en dictionnaire, les clefs seront les noms des méthodes/attributs et les valeurs les-dites méthodes et attributs. Toujours pour récupérer la méthode system() depuis os on fera
    • os.__dict__['system']

L’avantage de ces 2 méthodes ces qu’elles utilises des strings. Ainsi, si les mots system et sys sont interdits ont peut faire

'sy'+'stem'

Étape 3 : L’exploitation

Ici on est à l’étape la plus longue et la plus fastidieuse, où l’on va mettre bout à bout les éléments de puzzle qu’on a obtenu dans les étapes précédentes. Au vu des méthodes auxquelles on a le droit, on peut partir sur la première payload.

().__class__.__base__.__subclasses__()[59]()._module.__builtins__['__import__']('os').system()

Récupération de la classe catch_warnings

Dans notre cas, toute la partie de la payload jusqu’aux subclasses fonctionne sans problème. En l’affichant, on trouve que la classe voulue se trouve bien à l’index 59 comme à son habitude. On va devoir alors obtenir le nombre 59.

Avec notre petite boucle for pour obtenir un integer, on récupère le chiffre 44

On a donc une variable qui contient 44. On va jouer avec afin de créer d’autre variable contenant d’autre nombre pour arriver jusqu’à 59.

A cela il nous suffit d’ajouter notre début de payload

catch_warnings=().__class__.__base__.__subclasses__().pop(wanted)

Obtenir le module d ‘Import

Ici, de la classe catch_warnings aux builtins du module il n’y a aucun problème. C’est au moment de récupérer le module que c’est plus compliqué. Heureusement pour nous, les builtins que l’on va récupérer seront sous forme de dictionnaire (et pas sous forme de module builtins).
On va donc, récupérer la liste des valeurs du dictionnaire et récupérer la méthode __import__.

On va utiliser la méthode values() pour avoir cette liste et notre module est (dans notre cas) à l’index 109. Là encore on va devoir s’amuser avec nos petits nombres.

Récupérer le module os

Déjà, on sait que l’on peut utiliser la méthode __import__. Il nous suffit donc de récuperer les lettre o et s pour écrire “os”.

Là encore rien de très intéréssant, je vais trouver une chaîne de caractères avec les 2 lettres si possible facilement récupérables. Au niveau de la chaîne de caractères je vais me rabattre sur __sizeof__ que j’ai trouvé en antépénultième position (pour ne pas dire avant-avant-dernière) du dir(__import__).

Une petite subtilité malgré tout : Je vais utiliser des index négatifs pour pouvoir facilement récupérer les valeurs en fin de chaîne/liste.

Obtenir la méthode system()

Ici, pas besoin de s’embêter à recréer une chaîne. Par contre, il va falloir recalculer. En effet, on va utiliser la même méthode pour obtenir system() depuis os que pour obtenir __import__ depuis __builtins__.

Ouvrir un shell

Une fois notre méthode obtenue, on va simplement chercher à éxecuter “/bin/bash” pour obtenir un shell. Malheureusement les strings ne sont toujours pas acceptée, il va falloir créer la notre en fouillant un peut partout.

La majorité des lettres/caractères nécessaires se trouves dans le __file__ de os (tous sauf le a). On va toutes les récupérer, forger notre chaîne de caractères et la passer en argument

Et voilà ! On à réussi à résoudre la Pyjail !

Pour finir

J’ai bien conscience qu’on ne tombe pas aussi souvent sur une pyjail que sur une exploitation de binaire.

Malgré tout je sais qu’utiliser la même méthodologie est globalement efficace dans la plupart des challenges/pentests. L’avantage qu’ont les pyjails sur eux c’est que la partie Reconnaissance du terrain/Réflexion sur le bypass, qui en général est le plus fastidieux chez les autre, est la plus intéressante.

En tout cas, j’espère que ce petit article vous a été utile et vous aura appris quelque chose. Si jamais vous tomberez sur une pyjail en CTF vous ne pourrez plus dire “Je ne sais pas comment faire” ça sera même l’occasion de vous lancer dans ces petits casse-tête.

Je vais essayer de créer d’autre jails et de les ajouter à mon dépôt git. En attendant si vous voulez vous entraîner vous pouvez tester les pyjails de root-me ou même celles de ringZer0Team (que je trouve un peu plus intéressantes).

Published inArticle

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *