
Prova di concetto di un exploit di escalation dei privilegi locali per una race condition nella rate-table della qdisc di Linux, che utilizza il grooming dell'heap BPF e un leak tramite pipe per ottenere i privilegi di root.

Questo repository contiene un proof-of-concept di elevazione dei privilegi locali per CVE-2026-68138, una race condition nel codice delle rate-table del traffic-control di Linux. Nell'ambiente QEMU testato, il PoC eleva i privilegi da un processo ordinario con UID esterno 1000 a una shell con UID 0 nel namespace utente iniziale.
Avvertenza
Questo codice corrompe intenzionalmente lo stato dell'heap del kernel. Usalo solo in una VM isolata e monouso di tua proprietà. Una race mancata o una pulizia prematura può causare il panic del guest. Non eseguirlo su workstation, server o sistemi di terze parti.
qdisc_get_rtab() e qdisc_put_rtab() gestiscono una lista concatenata singolarmente globale al processo, qdisc_rtab_list, e un semplice int refcnt non atomico. Storicamente, i chiamanti detenevano il mutex RTNL, che serializzava l'accesso alla lista e al contatore di riferimento.
Il classificatore flower imposta TCF_PROTO_OPS_DOIT_UNLOCKED. Una richiesta RTM_NEWTFILTER per una regola flower può quindi raggiungere un'azione police e chiamare gli helper delle rate-table qdisc senza RTNL:
tc_new_tfilter()
-> fl_change()
-> tcf_exts_validate_ex()
-> tcf_action_init()
-> tcf_police_init()
-> qdisc_get_rtab()/qdisc_put_rtab()
Richieste concorrenti che usano la stessa rate table possono entrare in race sulla lista globale e sul suo contatore di riferimento. Il risultato è un use-after-free o double-free di struct qdisc_rate_table, un oggetto di 1056 byte allocato da kmalloc-2k sul kernel x86-64 testato.
Poiché la lista è globale e non per-network-namespace, le richieste provenienti da namespace di rete separati fanno comunque race sullo stesso oggetto.
Il record CNA di Linux identifica il commit di introduzione 470502de5bdb, rilasciato in Linux 5.1.
I kernel delle distribuzioni spesso applicano backport delle correzioni senza portarsi alla versione upstream mostrata sopra. Verifica se uno dei due commit di correzione, o la modifica equivalente allo spinlock delle rate-table qdisc, sia presente nel codice sorgente esatto del kernel usato dal sistema.
L'exploit è stato sviluppato e validato contro il commit vulnerabile 92d3817649df2b0b6a008a686c8275c88d7ef594, padre diretto della correzione mainline. Il controllo su kernel corretto ha usato f43ee0c0730d6191629b5ee1ceae27b1ebfdc047.
Alla data del 2026-08-12, la ricerca nel tracker CVE di Ubuntu non restituiva alcuna voce per questa CVE. La tabella seguente è quindi un'ispezione diretta del sorgente, non una determinazione dello stato di sicurezza di Canonical. Ogni tag Ubuntu collegato ha ancora la qdisc_rtab_list senza lock e non presenta la qdisc_rtab_lock correttiva.
Nessun pacchetto Ubuntu corretto è stato identificato a quella data di ispezione. I futuri pacchetti Ubuntu dovrebbero essere verificati per un backport equivalente alle correzioni upstream collegate, piuttosto che giudicati solo dal numero di versione.
L'exploit specifico per la build Ubuntu 22.04, il laboratorio QEMU, il checksum esatto dell'immagine e le condizioni sono documentati in ubuntu/README.md. È stato validato contro il kernel ufficiale 5.15.0-187-generic #197-Ubuntu con il memory-cgroup accounting abilitato.
Il bug sottostante e questa particolare catena di exploit hanno requisiti diversi. Il PoC è stato validato con:
CONFIG_USER_NS=y e CONFIG_NET_NS=y;CONFIG_NET_CLS=y, CONFIG_NET_CLS_FLOWER=y, CONFIG_NET_CLS_ACT=y e CONFIG_NET_ACT_POLICE=y;CONFIG_TMPFS_XATTR=y per l'heap spray simple_xattr;CONFIG_MODULES=y e un /sbin/modprobe utilizzabile per l'helper root finale;CONFIG_MEMCG=n, così le allocazioni qdisc/BPF/pipe/xattr usate da questa catena condividono la cache kmalloc-2k prevista;CONFIG_SLAB_BUCKETS=y, la randomizzazione della freelist e l'hardening della freelist erano abilitati nel kernel di test riuscito. Il KASLR non viene aggirato intrinsecamente con un indirizzo hard-coded: il PoC ottiene i puntatori necessari alla pagina e alle operazioni dalla pipe leak. Il laboratorio fornito usava nokaslr per semplificare il debug.
I kernel con memory-cgroup accounting abilitato o con layout di allocatore/cache diversi richiedono una strategia di reclaim differente. Il PoC rifiuta deliberatamente di dichiarare portabilità attraverso configurazioni di distribuzione arbitrarie.
La variante Ubuntu separata implementa quella diversa strategia di reclaim; i suoi requisiti sono intenzionalmente più ristretti e sono elencati in ubuntu/README.md.
Quattro thread worker entrano in namespace di rete distinti e creano filtri flower con azioni police. Tre worker seguono il percorso di azione riuscito; uno fornisce deliberatamente uno stimatore non valido dopo aver acquisito entrambi i riferimenti alla rate table, forzando il percorso di pulizia. Questa combinazione rende riproducibile la manipolazione concorrente del contatore di riferimento e della lista senza condividere lo stato del classificatore flower tra i worker.
qdisc_rate_table con BPF classicoDopo ogni richiesta netlink, la stessa CPU associa immediatamente un filtro classic-BPF di 133 istruzioni. Il suo array di istruzioni di 1064 byte è allocato da kmalloc-2k ed è strutturato in modo che i byte sovrapposti a qdisc_rate_table.next e qdisc_rate_table.refcnt rimangano inizialmente validi.
L'ultima istruzione contiene un marcatore per-socket. SO_GET_FILTER consente al PoC di rilevare quando il puntatore orig_prog->filter di un socket è stato reindirizzato a un'altra allocazione BPF attiva. Questo produce due oggetti socket che fanno riferimento allo stesso buffer di istruzioni.
La chiusura di un proprietario differisce la liberazione effettiva tramite sk_filter_release_rcu(). Dopo il periodo di grazia, il PoC alloca pipe ring a 32 slot:
32 * sizeof(struct pipe_buffer) = 32 * 40 = 1280 bytes -> kmalloc-2k
Una ring rioccupa il buffer BPF aliasato. Leggere l'altro socket con SO_GET_FILTER rivela un pipe_buffer attivo, inclusi i puntatori page e ops e l'offset della voce page-cache di /sbin/modprobe.
La chiusura del secondo socket libera la pipe ring attiva dopo un altro periodo di grazia RCU, mentre il corrispondente pipe_inode_info continua a riferirla. Uno spray simple_xattr rioccupa la ring da 1280 byte e colloca il pipe_buffer trapelato nello slot previsto con PIPE_BUF_FLAG_CAN_MERGE impostato.
Scrivere su ogni pipe candidata alla fine aggiunge byte controllati dall'attaccante alla pagina della page-cache che supporta il punto di ingresso ELF di /sbin/modprobe. Il PoC rilegge il file e prosegue solo dopo aver confermato la sovrascrittura esatta.
I kernel recenti non usano più il fallback storico del module-autoload per binfmt non valido in search_binary_handler(). Questo PoC crea invece un socket datagramma AF_INET con protocollo non supportato 253. inet_create() richiede il modulo mancante net-pf-2-proto-253-type-2, facendo sì che il kernel esegua il /sbin/modprobe sovrascritto come root del namespace iniziale.
Il payload del punto di ingresso iniettato scrive un file di prova e apre una shell root sulla console seriale QEMU.
Il binario del guest è linkato staticamente:
./build.sh
Comando equivalente:
gcc -O2 -static -pthread -Wall -Wextra -Werror \
-o build/exploit exploit.c
Il binario risultante deve essere esposto all'interno del guest come /exploit/CVE-2026-68138. Il laboratorio testato usava la condivisione 9p di QEMU in lettura-scrittura. L'helper root si aspetta quella condivisione in /chroot/exploit dal namespace di mount iniziale; modifica helper_command in exploit.c se il laboratorio usa un altro layout.
Le opzioni rilevanti del kernel di test sono riportate in config.fragment. Sono un frammento, non una configurazione completa del kernel di produzione.
Dalla shell non privilegiata del guest:
id
/exploit/CVE-2026-68138
La race è probabilistica. In caso di insuccesso il PoC stampa:
[-] no BPF alias found in this attempt
Riavvia la VM prima di riprovare. Non rieseguirlo nello stesso guest: la lista qdisc globale potrebbe essere già stata corrotta.
Un'esecuzione riuscita raggiunge:
[+] confirmed dangling orig_prog->filter pointer
[+] pipe leak: page=<kernel pointer> ops=<kernel pointer> off=0x60f len=1 flags=0
[+] page-cache overwrite via dangling pipe
[+] controlled page-cache write confirmed; triggering helper
[+] CVE-2026-68138 initial-namespace root shell
uid=0(root) gid=0(root) groups=0(root)
[+] ROOT PROOF: uid=0(root) gid=0(root) groups=0(root)
La trascrizione completa e ripulita è in docs/example-output.txt.
L'intera catena è stata completata tre volte partendo da boot freschi di VM vulnerabile. Il binario finale ha prodotto:
outer identity: uid=1000(user)
alias: classic-BPF orig_prog->filter overlap
leak: live pipe_buffer page and ops pointers
write: verified /sbin/modprobe page-cache modification
final identity: uid=0(root) gid=0(root) groups=0(root)
Lo stesso binario è stato eseguito contro il commit corretto con la stessa topologia a quattro vCPU. Circa 126.000 richieste di race sono state completate senza alcun alias BPF, e nessuna fase pipe o page-cache è stata raggiunta.
La variante specifica per Ubuntu è stata eseguita anche contro il kernel generico ufficiale esatto 5.15.0-187.197. Il guest è stato avviato con quattro vCPU, memory-cgroup accounting normale e nokaslr come unico argomento del kernel specifico per l'exploit.
Il kernel Ubuntu non è stato ricompilato: il test ha usato vmlinuz-5.15.0-187-generic invariato della distribuzione e i moduli ufficiali corrispondenti. La tabella dei prerequisiti Ubuntu separa le funzionalità del kernel stock dalle modifiche runtime e del laboratorio QEMU.
L'account iniziale non aveva accesso sudo né gruppi supplementari:
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)
Da uno snapshot fresco, l'exploit ha ottenuto l'alias BPF, lo ha rioccupato tramite oggetti simple_xattr cgroup-v2 e bitmap di select() bloccante, ha verificato che /proc/sys/kernel/modprobe contenesse /tmp/x, e ha raggiunto:
[+] CVE-2026-68138 Ubuntu initial-namespace root shell
uid=0(root) gid=0(root) groups=0(root)
root@cve-2026-68138-jammy:/# id
uid=0(root) gid=0(root) groups=0(root)
Vedi la trascrizione QEMU reale e le istruzioni del laboratorio Ubuntu. La variante codifica in modo fisso l'indirizzo di modprobe_path per questo pacchetto e quindi richiede nokaslr; non dichiara un bypass KASLR né compatibilità con un'altra build Ubuntu.
La patch fa già parte del kernel Linux e pertanto non è duplicata in questo repository. I commit di correzione canonici stabili e mainline sono collegati nei riferimenti sotto.
Ricerca e implementazione dell'exploit: A. Ramos <[email protected]> (Twitter: @aramosf).
| Serie | Stato | Commit/versione |
|---|
| Linux precedente alla 5.1 | Non interessato | La modifica introduttiva è assente |
| Linux dalla 5.1 alla 7.1.5 | Interessato salvo presenza di un backport del fornitore | da 470502de5bdb fino al commit precedente la correzione stabile |
| Linux 7.1.y | Corretto | 7.1.6, fb29e1b41052 |
| Serie di sviluppo di Linux 7.2 | Interessato prima della rc5 | dalla rc1 alla rc4 |
| Mainline | Corretto | 7.2-rc5, f43ee0c0730d |
| Linea Ubuntu | Pacchetto/tag ispezionato | Esito dell'ispezione del sorgente |
|---|
| Ubuntu 22.04 GA | 5.15.0-187.197 | Codice vulnerabile presente; exploit completo riprodotto in QEMU |
| Ubuntu 22.04 HWE | 6.8.0-136.136~22.04.1 | Codice vulnerabile presente; catena di exploit non testata |
| Ubuntu 24.04 HWE | 7.0.0-28.28~24.04.1 | Codice vulnerabile presente; questa catena di exploit non è compatibile con il suo hardening dell'allocatore |
| Ubuntu 26.04 | 7.0.0-28.28 | Codice vulnerabile presente; questa catena di exploit non è compatibile con il suo hardening dell'allocatore |