ESPILON-CTF-2026-Writeups/Intro/The_Wired
2026-03-22 19:18:58 +01:00
..
README.md ESPILON CTF 2026 — Write-ups édition 1 (33 challenges) 2026-03-22 19:18:58 +01:00

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.

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).

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

python3 solve.py --host <HOST> --port 2626

Protobuf reference

The .proto is at wired/registry/c2.proto on the target:

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