0xnhl

The Leaky Router

/ Update
4 min read

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_unzipped
bash

Then 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)
python

Important 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_ID values
    • INNER_PROTO values

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

Sender#

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()
python

3) Controlled Fuzzing / Enumeration#

Step A: Confirm version and basic validity#

  • VERSION=0 returned ERR_VERSION
  • VERSION=1 accepted
  • 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=1 was reachable (Node1)
  • TUNNEL_ID=2 existed but required auth (ERR_AUTH: session token mismatch)
  • TUNNEL_ID=3 existed 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 payload STATUS
  • 0x03: flag request mode (FLAG_REQ ...)
  • 0x04: echo mode requiring non-empty payload
  • 0x05+: unknown protocol

Step D: Identify auth bypass condition#

Critical test:

  • Sending to Node2 with FLAGS in 0..254 => always ERR_AUTH: session token mismatch
  • Sending with FLAGS=255 (0xff) => auth bypass and OK 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 = 1
  • FLAGS = 0xFF
  • TUNNEL_ID = 3
  • INNER_PROTO = 3
  • PAYLOAD = b"GIVE_FLAG"

Server response:

RTUN/1.0 OK FLAG=apoorvctf{tun3l_v1s10n_byp4ss}


5) Full Solver Code#

Saved as: solve.py


6) Reproduction#

Run:

python3 solve.py --mode flag
bash

Expected output:

RTUN/1.0 OK FLAG=apoorvctf{tun3l_v1s10n_byp4ss}
text

Optional protocol demonstration:

python3 solve.py --mode probe
bash

7) Why the exploit works#

  • Server uses FLAGS as a bitmask but appears to perform unsafe auth logic for 0xff.
  • 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 exactly GIVE_FLAG, returning the flag.

Final Flag#

apoorvctf{tun3l_v1s10n_byp4ss}

The Leaky Router
https://nahil.xyz/vault/writeups/apoorvctf2026/misc/the-leaky-router/
Author Nahil Rasheed
Published at March 24, 2026
Disclaimer This content is provided strictly for educational purposes only.