write-up: Intro/The_Wired/README.md
This commit is contained in:
parent
8bb0532b22
commit
fc6080ae90
@ -1,10 +1,64 @@
|
||||
# The Wired — Writeup
|
||||
# The Wired
|
||||
|
||||
**Difficulty:** Medium | **Flag:** `ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Category | Intro |
|
||||
| Difficulty | Medium |
|
||||
| Points | 400 |
|
||||
| Author | Eun0us |
|
||||
| CTF | Espilon 2026 |
|
||||
|
||||
## Recon
|
||||
---
|
||||
|
||||
Connect to the Navi shell on port 1337 and start reading.
|
||||
## 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
|
||||
@ -12,30 +66,46 @@ cat README_FIRST.txt
|
||||
ls -la
|
||||
```
|
||||
|
||||
Directories: `notes/`, `comms/`, `dumps/`, `logs/`, `tools/`, `journal/`, `wired/`
|
||||
Directories present: `notes/`, `comms/`, `dumps/`, `logs/`, `tools/`, `journal/`, `wired/`
|
||||
|
||||
The key files to understand the protocol:
|
||||
**Critical files:**
|
||||
|
||||
- `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.
|
||||
- `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
|
||||
|
||||
## Identify the target
|
||||
> 📸 `[screenshot: notes/protocol.txt showing the frame format]`
|
||||
|
||||
`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.
|
||||
### Step 2 — Identify the target device
|
||||
|
||||
## Understand the handshake
|
||||
Read `notes/eiri.txt` and `tools/devices.json`:
|
||||
|
||||
`comms/msg_ops_20260114.txt` describes the 2-step session:
|
||||
```
|
||||
Device: ce4f626b
|
||||
Alias: Eiri_Master
|
||||
Role: root-coordinator
|
||||
Status: quarantine
|
||||
```
|
||||
|
||||
Regular devices receive a `heartbeat` response. Only `ce4f626b` triggers the flag path.
|
||||
|
||||
> 📸 `[screenshot: devices.json showing the Eiri_Master entry with root-coordinator role]`
|
||||
|
||||
### 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
|
||||
|
||||
`comms/msg_lain_20260116.txt` confirms both messages have to go on the **same TCP connection**. Token is per-connection, can't reuse it.
|
||||
Both messages must go over the **same TCP connection**. The token is per-connection.
|
||||
|
||||
## Extract the key
|
||||
|
||||
Any device ELF works since they all share the same key (see `notes/changelog.txt` about v0.9.0 migration).
|
||||
### Step 4 — Extract the ChaCha20 key
|
||||
|
||||
```bash
|
||||
strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{32}$'
|
||||
@ -45,47 +115,68 @@ 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`.
|
||||
Do **not** use the key found in `notes/hardening.txt` — it is a honeypot.
|
||||
|
||||
## Exploit
|
||||
> 📸 `[screenshot: strings output showing the real 32-character key]`
|
||||
|
||||
Single TCP connection to port 2626.
|
||||
### Step 5 — Send the two-message handshake
|
||||
|
||||
**MSG1 — AGENT_INFO:**
|
||||
```
|
||||
AgentMessage {
|
||||
device_id = "ce4f626b"
|
||||
type = 0 (INFO)
|
||||
payload = b"ce4f626b"
|
||||
}
|
||||
```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)
|
||||
```
|
||||
|
||||
Serialize as protobuf → ChaCha20 encrypt → base64 + `\n` → send.
|
||||
The server replies:
|
||||
|
||||
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"
|
||||
@ -93,43 +184,19 @@ Command {
|
||||
}
|
||||
```
|
||||
|
||||
## Things that will get you dropped
|
||||
> 📸 `[screenshot: solver output showing the decrypted flag response]`
|
||||
|
||||
- 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)
|
||||
### Things that will get you silently dropped
|
||||
|
||||
## Solver
|
||||
- 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)
|
||||
|
||||
```bash
|
||||
python3 solve.py --host <HOST> --port 2626
|
||||
```
|
||||
---
|
||||
|
||||
## Protobuf reference
|
||||
## Flag
|
||||
|
||||
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
|
||||
`ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user