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
|
```bash
|
||||||
nc <HOST> 1337
|
nc <HOST> 1337
|
||||||
@ -12,30 +66,46 @@ cat README_FIRST.txt
|
|||||||
ls -la
|
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/protocol.txt` — frame format:
|
||||||
- `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.
|
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]`
|
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
|
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
|
### Step 4 — Extract the ChaCha20 key
|
||||||
|
|
||||||
Any device ELF works since they all share the same key (see `notes/changelog.txt` about v0.9.0 migration).
|
|
||||||
|
|
||||||
```bash
|
```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]{32}$'
|
||||||
@ -45,47 +115,68 @@ strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{12}$'
|
|||||||
- Key (32 bytes): `7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG`
|
- Key (32 bytes): `7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG`
|
||||||
- Nonce (12 bytes): `X3kW7nR9mPq2`
|
- 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:**
|
```python
|
||||||
```
|
import base64, socket
|
||||||
AgentMessage {
|
from Crypto.Cipher import ChaCha20
|
||||||
device_id = "ce4f626b"
|
|
||||||
type = 0 (INFO)
|
HOST = "<HOST>"
|
||||||
payload = b"ce4f626b"
|
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 {
|
||||||
command_name = "flag"
|
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`
|
### Things that will get you silently dropped
|
||||||
- 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
|
- 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:
|
`ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}`
|
||||||
|
|
||||||
```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
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user