- Generated screenshots for all 33 challenges (ESP, Hardware, IoT, OT, Misc, Web3) - Replaced all 123 placeholder lines with actual PNG image references - Cleaned duplicate images from previously partial updates - All write-ups now have full illustrated solutions
203 lines
6.1 KiB
Markdown
203 lines
6.1 KiB
Markdown
# The Wired
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Category | Intro |
|
|
| Difficulty | Medium |
|
|
| Points | 400 |
|
|
| Author | Eun0us |
|
|
| CTF | Espilon 2026 |
|
|
|
|
---
|
|
|
|
## Description
|
|
|
|
Something was interrupted.
|
|
|
|
The agents are still flashed. The link is broken. The devices continue to boot, identify
|
|
themselves, and wait for instructions they never receive.
|
|
|
|
You have gained access to a machine that was once used to administer a fleet of ESP32-based
|
|
agents. Logs, firmware dumps, technical notes — everything is still there.
|
|
|
|
The coordination point is still listening.
|
|
|
|
If you can understand how the agents communicate, prove your identity, and complete the
|
|
handshake — the coordinator will tell you what it knows.
|
|
|
|
**Ports:**
|
|
- 1337: Navi Shell (investigation)
|
|
- 2626: C2 Coordinator
|
|
|
|
Format: **ESPILON{flag}**
|
|
|
|
*"No matter where you go, everyone's connected."*
|
|
|
|
---
|
|
|
|
## TL;DR
|
|
|
|
Read the files on the investigation machine (port 1337) to understand a 2-step ChaCha20 +
|
|
Protobuf C2 protocol. Extract the real crypto key from firmware ELF strings (avoid the
|
|
honeypot dev key). Impersonate device `ce4f626b` (the "Eiri_Master" root coordinator).
|
|
Send an `AGENT_INFO` message, capture the session token from the response, then send a
|
|
`CMD_RESULT` message with that token — all on the same TCP connection — to receive the flag.
|
|
|
|
---
|
|
|
|
## Tools
|
|
|
|
| Tool | Purpose |
|
|
|------|---------|
|
|
| `nc` | Connect to Navi Shell |
|
|
| Python 3 + `pycryptodome` | ChaCha20 encrypt/decrypt |
|
|
| `strings` / Ghidra | Extract key from ELF binary |
|
|
| Manual Protobuf encoding | Serialize AgentMessage |
|
|
|
|
---
|
|
|
|
## Solution
|
|
|
|
### Step 1 — Explore the investigation machine
|
|
|
|
```bash
|
|
nc <HOST> 1337
|
|
cat README_FIRST.txt
|
|
ls -la
|
|
```
|
|
|
|
Directories present: `notes/`, `comms/`, `dumps/`, `logs/`, `tools/`, `journal/`, `wired/`
|
|
|
|
**Critical files:**
|
|
|
|
- `notes/protocol.txt` — frame format:
|
|
```
|
|
base64( ChaCha20( protobuf(AgentMessage) ) ) + '\n'
|
|
```
|
|
- `notes/derivation.txt` — ChaCha20 key is 32 bytes, nonce is 12 bytes, counter=0
|
|
- `notes/hardening.txt` — warns about a trap dev key at the bottom; Jan 17 journal entry
|
|
confirms the key `Xt9Lm2Qw7KjP4rNvB8hYc3fZ0dAeU6sG` is planted bait
|
|
- `tools/devices.json` — lists all known device IDs with roles
|
|
|
|

|
|
|
|
### Step 2 — Identify the target device
|
|
|
|
Read `notes/eiri.txt` and `tools/devices.json`:
|
|
|
|
```
|
|
Device: ce4f626b
|
|
Alias: Eiri_Master
|
|
Role: root-coordinator
|
|
Status: quarantine
|
|
```
|
|
|
|
Regular devices receive a `heartbeat` response. Only `ce4f626b` triggers the flag path.
|
|
|
|

|
|
|
|
### Step 3 — Understand the handshake
|
|
|
|
From `comms/msg_ops_20260114.txt`:
|
|
|
|
1. Agent sends `AGENT_INFO` → coordinator replies `session_init` with a random token in `argv[0]`
|
|
2. Agent sends `AGENT_CMD_RESULT` with `request_id` = that token → coordinator replies with the flag
|
|
|
|
Both messages must go over the **same TCP connection**. The token is per-connection.
|
|
|
|
### Step 4 — Extract the ChaCha20 key
|
|
|
|
```bash
|
|
strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{32}$'
|
|
strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{12}$'
|
|
```
|
|
|
|
- Key (32 bytes): `7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG`
|
|
- Nonce (12 bytes): `X3kW7nR9mPq2`
|
|
|
|
Do **not** use the key found in `notes/hardening.txt` — it is a honeypot.
|
|
|
|

|
|
|
|
### Step 5 — Send the two-message handshake
|
|
|
|
```python
|
|
import base64, socket
|
|
from Crypto.Cipher import ChaCha20
|
|
|
|
HOST = "<HOST>"
|
|
PORT = 2626
|
|
KEY = b"7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG"
|
|
NONCE = b"X3kW7nR9mPq2"
|
|
|
|
def encrypt_frame(plaintext):
|
|
cipher = ChaCha20.new(key=KEY, nonce=NONCE)
|
|
return base64.b64encode(cipher.encrypt(plaintext)) + b"\n"
|
|
|
|
def decrypt_frame(frame):
|
|
raw = base64.b64decode(frame.strip())
|
|
cipher = ChaCha20.new(key=KEY, nonce=NONCE)
|
|
return cipher.decrypt(raw)
|
|
|
|
# Manually encode AgentMessage as protobuf
|
|
# field 1 (device_id) = "ce4f626b", field 2 (type) = 0 (INFO), field 5 (payload) = b"ce4f626b"
|
|
def encode_agent_info():
|
|
dev_id = b"ce4f626b"
|
|
msg = b""
|
|
msg += b"\x0a" + bytes([len(dev_id)]) + dev_id # field 1 = device_id
|
|
msg += b"\x10\x00" # field 2 = type 0 (INFO)
|
|
msg += b"\x2a" + bytes([len(dev_id)]) + dev_id # field 5 = payload
|
|
return msg
|
|
|
|
with socket.create_connection((HOST, PORT)) as s:
|
|
# MSG1 — AGENT_INFO
|
|
s.sendall(encrypt_frame(encode_agent_info()))
|
|
resp_raw = s.recv(4096)
|
|
resp_pb = decrypt_frame(resp_raw)
|
|
|
|
# Extract token from argv[0] in the Command protobuf response
|
|
# (parse manually or use protobuf library)
|
|
token = extract_token(resp_pb)
|
|
|
|
# MSG2 — CMD_RESULT (type=4)
|
|
def encode_cmd_result(token):
|
|
dev_id = b"ce4f626b"
|
|
msg = b""
|
|
msg += b"\x0a" + bytes([len(dev_id)]) + dev_id
|
|
msg += b"\x10\x04" # type 4 = CMD_RESULT
|
|
msg += b"\x22" + bytes([len(token)]) + token.encode() # field 4 = request_id
|
|
msg += b"\x2a" + bytes([len(dev_id)]) + dev_id
|
|
return msg
|
|
|
|
s.sendall(encrypt_frame(encode_cmd_result(token)))
|
|
flag_resp = s.recv(4096)
|
|
flag_pb = decrypt_frame(flag_resp)
|
|
print(flag_pb)
|
|
```
|
|
|
|
The server replies:
|
|
|
|
```
|
|
Command {
|
|
command_name = "flag"
|
|
argv = ["ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}"]
|
|
}
|
|
```
|
|
|
|

|
|
|
|
### Things that will get you silently dropped
|
|
|
|
- Using the honeypot dev key from `hardening.txt`
|
|
- Sending a `device_id` not in the allowlist
|
|
- Using a valid but non-master device (returns `heartbeat`, not `flag`)
|
|
- Sending MSG2 on a **new** TCP connection (token is session-scoped)
|
|
- Wrong `type` in MSG2 (must be `4`)
|
|
- Wrong `request_id` (must match the token exactly)
|
|
|
|
---
|
|
|
|
## Flag
|
|
|
|
`ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}`
|