
Exploit de prueba de concepto para la escalada local de privilegios que aprovecha una condición de carrera en la tabla de tasas de qdisc de Linux, utilizando heap grooming con BPF y una fuga de pipe para obtener root.

Este repositorio contiene un exploit de prueba de concepto de escalada local de privilegios para CVE-2026-68138, una condición de carrera en el código de la tabla de tasas del control de tráfico de Linux. En el entorno QEMU probado, el PoC escala desde un proceso común con UID externo 1000 hasta una shell con UID 0 en el espacio de nombres de usuario inicial.
Advertencia
Este código corrompe intencionalmente el estado del heap del kernel. Úsalo únicamente en una VM aislada y desechable que sea tuya. Una condición de carrera fallida o una limpieza prematura pueden provocar un pánico en el invitado. No lo ejecutes en una estación de trabajo, servidor o sistema de terceros.
qdisc_get_rtab() y qdisc_put_rtab() gestionan una lista simplemente
enlazada global al proceso, qdisc_rtab_list, y un simple int refcnt no
atómico. Históricamente, los llamadores mantenían el mutex RTNL, que
serializaba el acceso a la lista y al contador de referencias.
El clasificador flower establece TCF_PROTO_OPS_DOIT_UNLOCKED. Una solicitud
RTM_NEWTFILTER para una regla flower puede, por tanto, alcanzar una acción
police y llamar a los helpers de la tabla de tasas qdisc sin RTNL:
tc_new_tfilter()
-> fl_change()
-> tcf_exts_validate_ex()
-> tcf_action_init()
-> tcf_police_init()
-> qdisc_get_rtab()/qdisc_put_rtab()
Las solicitudes concurrentes que usan la misma tabla de tasas pueden provocar
una condición de carrera en la lista global y su contador de referencias. El
resultado es un use-after-free o un double-free de struct qdisc_rate_table,
un objeto de 1056 bytes asignado desde kmalloc-2k en el kernel x86-64
probado.
Debido a que la lista es global y no por espacio de nombres de red, las solicitudes de espacios de nombres de red separados siguen provocando una condición de carrera sobre el mismo objeto.
El registro CNA de Linux identifica el commit de introducción
470502de5bdb,
publicado en Linux 5.1.
Los kernels de las distribuciones suelen aplicar backports de las correcciones sin cambiar a la versión upstream mostrada arriba. Comprueba si alguno de los commits de corrección—o el cambio equivalente de spinlock en la tabla de tasas qdisc—está presente en la fuente exacta del kernel que usa el sistema.
El exploit se desarrolló y validó contra el commit vulnerable
92d3817649df2b0b6a008a686c8275c88d7ef594, el padre directo de la corrección
de mainline. El control de kernel corregido usó
f43ee0c0730d6191629b5ee1ceae27b1ebfdc047.
A fecha de 2026-08-12, la
búsqueda en el rastreador de CVEs de Ubuntu
no devolvió una entrada para este CVE. Por lo tanto, la tabla siguiente es una
inspección directa de la fuente, no una determinación del estado de seguridad
de Canonical. Cada etiqueta de Ubuntu enlazada todavía tiene qdisc_rtab_list
sin bloquear y carece del qdisc_rtab_lock correctivo.
No se identificó ningún paquete de Ubuntu corregido en esa fecha de inspección. Los futuros paquetes de Ubuntu deberían comprobarse para ver si contienen un backport equivalente a las correcciones upstream enlazadas, en lugar de juzgarlos solo por su número de versión.
El exploit específico de la compilación de Ubuntu 22.04, el laboratorio QEMU,
la suma de verificación exacta de la imagen y las condiciones están
documentados en ubuntu/README.md. Se validó contra el
kernel oficial 5.15.0-187-generic #197-Ubuntu con la contabilidad de
memory-cgroup habilitada.
El bug subyacente y esta cadena de exploit en particular tienen requisitos diferentes. El PoC se validó con:
CONFIG_USER_NS=y y CONFIG_NET_NS=y;CONFIG_NET_CLS=y, CONFIG_NET_CLS_FLOWER=y,
CONFIG_NET_CLS_ACT=y y CONFIG_NET_ACT_POLICE=y;CONFIG_TMPFS_XATTR=y para el heap spray simple_xattr;CONFIG_MODULES=y y un /sbin/modprobe utilizable para el helper root final;CONFIG_MEMCG=n, de modo que las asignaciones qdisc/BPF/pipe/xattr usadas
por esta cadena compartan la caché kmalloc-2k esperada;CONFIG_SLAB_BUCKETS=y, la aleatorización de freelist y el endurecimiento de
freelist estaban habilitados en el kernel de prueba que tuvo éxito. KASLR no se
evade inherentemente con una dirección fija: el PoC obtiene los punteros page
y ops necesarios de la fuga de pipe. El laboratorio suministrado usó
nokaslr para simplificar la depuración.
Los kernels con contabilidad de memory-cgroup habilitada o con diferentes diseños de asignador/caché requieren una estrategia de recuperación diferente. El PoC se niega deliberadamente a afirmar portabilidad entre configuraciones arbitrarias de distribuciones.
La variante separada para Ubuntu implementa esa estrategia de recuperación
diferente; sus requisitos son intencionadamente más reducidos y se enumeran en
ubuntu/README.md.
Cuatro hilos trabajadores entran en espacios de nombres de red distintos y crean filtros flower con acciones police. Tres trabajadores toman la ruta de acción exitosa; uno suministra un estimador deliberadamente inválido después de adquirir ambas referencias a la tabla de tasas, forzando la ruta de limpieza. Esta combinación hace reproducible la manipulación concurrente del contador de referencias y de la lista sin compartir el estado del clasificador flower entre los trabajadores.
qdisc_rate_table con BPF clásicoDespués de cada solicitud netlink, la misma CPU adjunta inmediatamente un
filtro BPF clásico de 133 instrucciones. Su matriz de instrucciones de 1064
bytes se asigna desde kmalloc-2k y está conformada para que los bytes que se
superponen con qdisc_rate_table.next y qdisc_rate_table.refcnt sigan siendo
válidos inicialmente.
La instrucción final contiene un marcador por socket. SO_GET_FILTER permite
al PoC detectar cuándo el puntero orig_prog->filter de un socket ha sido
redirigido a otra asignación BPF viva. Esto produce dos objetos de socket que
se refieren al mismo búfer de instrucciones.
Cerrar uno de los propietarios difiere la liberación real mediante
sk_filter_release_rcu(). Después del período de gracia, el PoC asigna anillos
de pipe de 32 ranuras:
32 * sizeof(struct pipe_buffer) = 32 * 40 = 1280 bytes -> kmalloc-2k
Uno de los anillos recupera el búfer BPF con alias. Leer el otro socket con
SO_GET_FILTER revela un pipe_buffer vivo, incluidos sus punteros page y
ops y el desplazamiento de la entrada de caché de páginas de /sbin/modprobe.
Cerrar el segundo socket libera el anillo de pipe vivo después de otro período
de gracia de RCU, mientras que el pipe_inode_info correspondiente todavía lo
referencia. Un spray de simple_xattr recupera el anillo de 1280 bytes y
coloca el pipe_buffer filtrado en la ranura esperada con
PIPE_BUF_FLAG_CAN_MERGE establecido.
Escribir en cada pipe candidato acaba añadiendo bytes controlados por el
atacante a la página de caché de páginas que respalda el punto de entrada ELF
de /sbin/modprobe. El PoC vuelve a leer el archivo y continúa solo tras
confirmar la sobrescritura exacta.
Los kernels recientes ya no usan la alternativa histórica de autocarga de
módulos por binfmt inválido en search_binary_handler(). Este PoC, en cambio,
crea un socket de datagramas AF_INET con el protocolo no soportado 253.
inet_create() solicita el módulo faltante net-pf-2-proto-253-type-2, lo que
hace que el kernel ejecute el /sbin/modprobe sobrescrito como root del
espacio de nombres inicial.
La carga útil inyectada en el punto de entrada escribe un archivo de prueba y abre una shell root en la consola serie de QEMU.
El binario invitado está enlazado estáticamente:
./build.sh
Comando equivalente:
gcc -O2 -static -pthread -Wall -Wextra -Werror \
-o build/exploit exploit.c
El binario resultante debe exponerse dentro del invitado como
/exploit/CVE-2026-68138. El laboratorio probado usó el recurso compartido 9p
de lectura-escritura de QEMU. El helper root espera ese recurso compartido en
/chroot/exploit desde el espacio de nombres de montaje inicial; ajusta
helper_command en exploit.c si el laboratorio usa otra disposición.
Las opciones relevantes del kernel de prueba están registradas en
config.fragment. Son un fragmento, no una configuración
completa de kernel de producción.
Desde la shell invitada sin privilegios:
id
/exploit/CVE-2026-68138
La condición de carrera es probabilística. Si falla, el PoC imprime:
[-] no BPF alias found in this attempt
Reinicia la VM antes de volver a intentarlo. No lo vuelvas a ejecutar en el mismo invitado: la lista global de qdisc puede ya estar corrompida.
Una ejecución exitosa alcanza:
[+] 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 transcripción completa saneada está en
docs/example-output.txt.
La cadena completa se completó tres veces desde arranques limpios de la VM vulnerable. El binario final produjo:
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)
El mismo binario se ejecutó contra el commit corregido con la misma topología de cuatro vCPU. Se completaron aproximadamente 126 000 solicitudes de carrera sin un alias BPF, y no se alcanzó ninguna etapa de pipe ni de caché de páginas.
La variante específica de Ubuntu también se ejecutó contra el kernel genérico
oficial exacto 5.15.0-187.197. El invitado arrancó con cuatro vCPU,
contabilidad normal de memory-cgroup y nokaslr como único argumento de kernel
específico del exploit.
El kernel de Ubuntu no se recompiló: la prueba usó el
vmlinuz-5.15.0-187-generic sin cambios de la distribución y los módulos
oficiales correspondientes. La
tabla de requisitos previos de Ubuntu
separa las características del kernel estándar de los ajustes de tiempo de
ejecución y del laboratorio QEMU.
La cuenta inicial no tenía acceso sudo ni grupos suplementarios:
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)
Desde una instantánea nueva, el exploit obtuvo el alias BPF, lo recuperó
mediante objetos simple_xattr de cgroup-v2 y mapas de bits de select()
bloqueante, verificó que /proc/sys/kernel/modprobe contenía /tmp/x, y
alcanzó:
[+] 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)
Consulta la transcripción real en QEMU y
las instrucciones del laboratorio de Ubuntu. La variante
fija la dirección de modprobe_path para este paquete y, por lo tanto, requiere
nokaslr; no afirma una evasión de KASLR ni compatibilidad con otra compilación
de Ubuntu.
El parche ya forma parte del kernel de Linux y, por lo tanto, no se duplica en este repositorio. Los commits de corrección canónicos de la rama estable y de mainline están enlazados en las referencias siguientes.
Investigación e implementación del exploit: A. Ramos <[email protected]>
(Twitter: @aramosf).
| Línea | Estado | Commit/versión |
|---|
| Linux anterior a 5.1 | No afectado | El cambio de introducción está ausente |
| Linux 5.1 hasta 7.1.5 | Afectado a menos que esté presente un backport del proveedor | 470502de5bdb hasta el commit anterior a la corrección estable |
| Linux 7.1.y | Corregido | 7.1.6, fb29e1b41052 |
| Serie de desarrollo de Linux 7.2 | Afectado antes de rc5 | desde rc1 hasta rc4 |
| Mainline | Corregido | 7.2-rc5, f43ee0c0730d |
| Línea de Ubuntu | Paquete/etiqueta inspeccionado | Resultado de la fuente |
|---|
| Ubuntu 22.04 GA | 5.15.0-187.197 | Código vulnerable presente; exploit completo reproducido en QEMU |
| Ubuntu 22.04 HWE | 6.8.0-136.136~22.04.1 | Código vulnerable presente; cadena de exploit no probada |
| Ubuntu 24.04 HWE | 7.0.0-28.28~24.04.1 | Código vulnerable presente; esta cadena de exploit no es compatible con su endurecimiento del asignador |
| Ubuntu 26.04 | 7.0.0-28.28 | Código vulnerable presente; esta cadena de exploit no es compatible con su endurecimiento del asignador |