- 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
136 lines
3.6 KiB
Markdown
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
|