Writeup: The Wired - EspilonCTF (Intro)
Overview
We're given access to a machine that administered a fleet of ESP32-based agents running a custom C2 framework called ESPILON. The agents communicate with a coordinator over TCP using encrypted protobuf messages. Our goal is to impersonate an agent, complete the authentication handshake, and retrieve the flag from the coordinator.
Step 1: Reconnaissance via Navi Shell
Connecting to port 33402 gives us a restricted bash shell on "Lain's machine." The filesystem contains:
~/
├── README_FIRST.txt # Challenge intro
├── notice.txt # Auth requirements
├── comms/ # Operator messages (protocol handshake docs)
├── dumps/ # Firmware ELF/BIN images per device
├── journal/ # Lain's personal notes (trust indicators)
├── logs/ # Boot logs, flash logs, network attempts
├── notes/ # Protocol spec, crypto notes, changelog
├── tools/ # devices.json, probe.py, flash.py
└── wired/ # Network captures, decoy messages
Key Findings
Protocol (notes/protocol.txt):
- Transport: TCP, one message per frame
- Framing:
base64( encrypt( protobuf( AgentMessage ) ) ) + \n - Encryption: ChaCha20 (32-byte key, 12-byte nonce, counter=0)
- Auth: Implicit - if the coordinator can decrypt and parse your message, you're authenticated
AgentMessage Protobuf Schema:
message AgentMessage {
string device_id = 1;
enum Type { INFO=0, ERROR=1, DATA=2, LOG=3, CMD_RESULT=4 }
Type type = 2;
string source = 3;
string request_id = 4;
bytes payload = 5;
bool eof = 6;
}
Handshake Flow (comms/msg_ops_20260114.txt):
- Agent sends
AGENT_INFO(type=0) with payload=device_id - Coordinator responds with
session_init, argv[0] = session token - Agent sends
CMD_RESULT(type=4) with request_id=session_token, payload=device_id - Coordinator responds with privileged data
Device Registry (tools/devices.json):
- 7 devices registered, crypto keys marked
[STRIPPED] - Core node:
ce4f626b(hostname:navi-core) - the privileged "Eiri_Master" role - Only the core node triggers the privileged response path
Step 2: Identifying the Decoy
Lain's journal warns that "the other Lain" tampered with files:
notes/hardening.txtcontains a development key:Xt9Lm2Qw7KjP4rNvB8hYc3fZ0dAeU6sG/ nonce:W4kR7nM9pQx2- Journal entry 2026-01-17: "Someone appended a section to my hardening notes. I didn't write it. The key listed there is not the one I deployed."
- The core binary (
dumps/ce4f626b/bot-lwip.elf) was stripped of symbols
This means the key in hardening.txt is a planted decoy. The real key must be extracted from the firmware.
Step 3: Extracting the Real Key from Firmware
Since the core binary is stripped, we use a non-stripped ELF from another device (all devices share the same key from the same build):
# Extract 32-byte alphanumeric strings (key candidates)
strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{32}$'
Result: 7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG
# Extract 12-byte alphanumeric strings (nonce candidates)
strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{12}$'
Result: X3kW7nR9mPq2
Real credentials:
- Key:
7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG - Nonce:
X3kW7nR9mPq2
Step 4: Building the Exploit
The exploit script performs the two-step handshake:
#!/usr/bin/env python3
import socket, base64
from Crypto.Cipher import ChaCha20
KEY = b"7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG"
NONCE = b"X3kW7nR9mPq2"
DEVICE_ID = "ce4f626b"
def chacha20_crypt(data):
cipher = ChaCha20.new(key=KEY, nonce=NONCE)
return cipher.encrypt(data)
def encode_varint(value):
result = []
while value > 0x7f:
result.append((value & 0x7f) | 0x80)
value >>= 7
result.append(value & 0x7f)
return bytes(result)
def encode_string_field(field_num, value):
tag = (field_num << 3) | 2
encoded = value.encode() if isinstance(value, str) else value
return encode_varint(tag) + encode_varint(len(encoded)) + encoded
def encode_varint_field(field_num, value):
tag = (field_num << 3) | 0
return encode_varint(tag) + encode_varint(value)
def build_agent_message(device_id, msg_type, request_id="", payload=b""):
msg = encode_string_field(1, device_id)
if msg_type != 0:
msg += encode_varint_field(2, msg_type)
if request_id:
msg += encode_string_field(4, request_id)
if payload:
msg += encode_string_field(5, payload)
return msg
def send_frame(sock, plaintext):
sock.sendall(base64.b64encode(chacha20_crypt(plaintext)) + b"\n")
def recv_frame(sock):
buf = b""
while True:
c = sock.recv(1)
if not c: return None
if c == b"\n": break
buf += c
return chacha20_crypt(base64.b64decode(buf))
def parse_protobuf(data):
fields, i = {}, 0
while i < len(data):
tag, consumed = parse_varint(data[i:]); i += consumed
field_num, wire_type = tag >> 3, tag & 0x7
if wire_type == 0:
val, consumed = parse_varint(data[i:]); i += consumed
fields[field_num] = val
elif wire_type == 2:
length, consumed = parse_varint(data[i:]); i += consumed
fields[field_num] = data[i:i+length]; i += length
return fields
def parse_varint(data):
result, shift = 0, 0
for i, byte in enumerate(data):
result |= (byte & 0x7f) << shift; shift += 7
if not (byte & 0x80): return result, i + 1
return result, len(data)
# === Main exploit ===
sock = socket.create_connection(("espilon.net", 33403), timeout=10)
sock.settimeout(10)
# Step 1: AGENT_INFO (type=0)
info_msg = build_agent_message(DEVICE_ID, 0, payload=DEVICE_ID.encode())
send_frame(sock, info_msg)
# Receive session_init, extract token from field 3 (argv[0])
resp = parse_protobuf(recv_frame(sock))
session_token = resp[3].decode()
# Step 2: CMD_RESULT (type=4) with session token
cmd_msg = build_agent_message(DEVICE_ID, 4, request_id=session_token,
payload=DEVICE_ID.encode())
send_frame(sock, cmd_msg)
# Receive flag
resp2 = parse_protobuf(recv_frame(sock))
print(resp2[3].decode()) # ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}
sock.close()
Step 5: Execution
$ python3 solve.py
=== Step 1: Sending AGENT_INFO ===
[RX] decrypted: ce4f626b | session_init | ea42273c95e631d0c602f1afbff36279 | handshake
=== Step 2: Sending CMD_RESULT with token ===
[RX] decrypted: ce4f626b | flag | ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}
Key Takeaways
- Don't trust convenient answers - The key in
hardening.txtwas a deliberate trap. Lain's journal warned about planted data. - Firmware analysis - The real key was embedded as a plaintext constant in the unstripped ELF binary, extractable with
strings. - Protocol reconstruction - Understanding the exact framing (protobuf -> ChaCha20 -> base64 -> newline) and two-step handshake was essential.
- Identity matters - Only the core node (
ce4f626b/ Eiri_Master) triggered the privileged response containing the flag.
Flag
ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}