ESPILON-CTF-2026-Writeups/Intro/The_Wired/solve/solve.md
Eun0us 6a0877384d [+] Writeups v2 — sync solves, real points, scoreboard stats, cleanup
- Remove undeployed challenges: Phantom_Byte, Cr4cK_w1f1, Lain_Br34kC0r3 V1,
  Lain_VS_Knights, Lets_All_Love_UART, AETHER_NET, Last_Train_451, Web3/
- Sync 24 solve/ files from main CTF-Espilon repo
- Update all READMEs with real CTFd final scores at freeze
- Add git-header.png banner
- Rewrite README: scoreboard top 10, edition stats (1410 users, 264 boards,
  1344 solves), correct freeze date March 26 2026
2026-03-27 21:27:45 +01:00

136 lines
3.6 KiB
Markdown

# The Wired — Writeup
**Difficulty:** Medium | **Flag:** `ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}`
## Recon
Connect to the Navi shell on port 1337 and start reading.
```bash
nc <HOST> 1337
cat README_FIRST.txt
ls -la
```
Directories: `notes/`, `comms/`, `dumps/`, `logs/`, `tools/`, `journal/`, `wired/`
The key files to understand the protocol:
- `notes/protocol.txt` — frame format is `base64( ChaCha20( protobuf(AgentMessage) ) ) + '\n'`
- `notes/derivation.txt` — ChaCha20 with 32-byte key, 12-byte nonce, counter=0
- `notes/hardening.txt` — keys are baked into the firmware ELF. **WARNING:** there's a dev key at the bottom (`Xt9Lm2Qw7KjP4rNvB8hYc3fZ0dAeU6sG`) — it's a trap planted by "the other Lain". The server drops you silently if you use it. The journal entry from Jan 17 warns about this.
## Identify the target
`notes/eiri.txt` and `tools/devices.json` tell you the interesting device is `ce4f626b` — alias "Eiri_Master", role `root-coordinator`, status `quarantine`. Regular devices just get a `heartbeat` back. This one triggers the flag path.
## Understand the handshake
`comms/msg_ops_20260114.txt` describes the 2-step session:
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
`comms/msg_lain_20260116.txt` confirms both messages have to go on the **same TCP connection**. Token is per-connection, can't reuse it.
## Extract the key
Any device ELF works since they all share the same key (see `notes/changelog.txt` about v0.9.0 migration).
```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`
Alternative: Ghidra → find `chacha_cd()` in crypto.c → follow xrefs to `CONFIG_CRYPTO_KEY` / `CONFIG_CRYPTO_NONCE`.
## Exploit
Single TCP connection to port 2626.
**MSG1 — AGENT_INFO:**
```
AgentMessage {
device_id = "ce4f626b"
type = 0 (INFO)
payload = b"ce4f626b"
}
```
Serialize as protobuf → ChaCha20 encrypt → base64 + `\n` → send.
Server replies with:
```
Command {
command_name = "session_init"
argv = ["<hex_token>"]
request_id = "handshake"
}
```
Decrypt + decode the response, grab the token from `argv[0]`.
**MSG2 — CMD_RESULT:**
```
AgentMessage {
device_id = "ce4f626b"
type = 4 (CMD_RESULT)
request_id = "<token>"
payload = b"ce4f626b"
}
```
Same connection. Encrypt, encode, send.
Server replies:
```
Command {
command_name = "flag"
argv = ["ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}"]
}
```
## Things that will get you dropped
- Using the fake dev key from `hardening.txt`
- Sending a device_id not in the allowlist
- Using a valid but non-master device (you get `heartbeat`, not `flag`)
- Sending MSG2 on a new connection (token is tied to the session)
- Wrong `type` in MSG2 (needs to be `4`)
- Wrong `request_id` (needs to match the token exactly)
## Solver
```bash
python3 solve.py --host <HOST> --port 2626
```
## Protobuf reference
The `.proto` is at `wired/registry/c2.proto` on the target:
```protobuf
message AgentMessage {
string device_id = 1;
AgentMsgType type = 2;
string source = 3;
string request_id = 4;
bytes payload = 5;
bool eof = 6;
}
message Command {
string device_id = 1;
string command_name = 2;
repeated string argv = 3;
string request_id = 4;
}
```
No need for `protoc` — manual varint encoding works fine. See `solve.py`.
Author: Eun0us