ESPILON-CTF-2026-Writeups/Intro/The_Wired
2026-03-26 17:33:28 +00:00
..
README.md write-up: Intro/The_Wired/README.md 2026-03-26 17:33:28 +00:00

The Wired

Field Value
Category Intro
Difficulty Medium
Points 400
Author Eun0us
CTF Espilon 2026

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

nc <HOST> 1337
cat README_FIRST.txt
ls -la

Directories present: notes/, comms/, dumps/, logs/, tools/, journal/, wired/

Critical files:

  • 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

📸 [screenshot: notes/protocol.txt showing the frame format]

Step 2 — Identify the target device

Read notes/eiri.txt and tools/devices.json:

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

Both messages must go over the same TCP connection. The token is per-connection.

Step 4 — Extract the ChaCha20 key

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

Do not use the key found in notes/hardening.txt — it is a honeypot.

📸 [screenshot: strings output showing the real 32-character key]

Step 5 — Send the two-message handshake

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)

The server replies:

Command {
    command_name = "flag"
    argv         = ["ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}"]
}

📸 [screenshot: solver output showing the decrypted flag response]

Things that will get you silently dropped

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

Flag

ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}