
Linux LPE - Escape fiable de jail/contenedor
Una prueba de concepto fiable de escape de contenedor / jaula sin privilegios para CentOS / RHEL 10.
Aprovecha un bug de fragmentación IPv6 ya corregido en __ip6_append_data() (cerrado upstream por 38becddc, sin CVE), un desbordamiento lineal dentro de la slab hacia skb_shared_info en la cola del objeto head del propio paquete. Este README documenta únicamente la cadena de explotación. No cubre el desencadenante.
6.12.0-242.el10).Esto es una prueba de concepto, deliberadamente no un arma llave en mano. El andamiaje de fiabilidad se omite a propósito: sin preparación de PCP por CPU, sin blindaje de SLUB entre cachés, y la cabeza del skb se coloca en una caché compartida, no tan silenciosa. Lo que se entrega acierta con la frecuencia suficiente para demostrar la cadena, y poco más. Está limitado únicamente a CentOS / RHEL 10.
CONFIG_INIT_ON_ALLOC_DEFAULT_ON desactivado, el valor predeterminado de RHEL / CentOS. Esta PoC planta un puntero obsoleto en bytes de slab sin inicializar; con init_on_alloc=1 esa ranura se pone a cero y esta técnica concreta solo provoca un fallo del kernel (una desreferencia NULL). Eso es una limitación de la técnica, no del bug: un exploit completo que también cubra esos kernels, con una técnica diferente, llegará cuando el parche y el CVE sean públicos./sys/kernel/btf/vmlinux presente y legible por todos (de serie en RHEL / CentOS).| Distribución | Kernel | Resultado |
|---|---|---|
| CentOS Stream 10 | 6.12.0-242.el10 | root, escape de contenedor |
| RHEL 10 | 6.12.0-228.el10 | escape del contexto httpd_t |
Desbordamiento dentro de la slab hacia un auto-UAF. El bug concede el control del único byte nr_frags en skb_shared_info. La ruta de liberación (skb_release_data()) recorre frags[0 .. nr_frags) y aplica put_page() a cada entrada, y frags[] nunca se inicializa. Plantamos previamente un struct page * para una página de búfer de pipe en esa ranura de la slab mediante reutilización controlada de la caché, y luego establecemos nr_frags a 1, de modo que el desmontaje libera una referencia sobre una página que aún poseemos. El desbordamiento lineal se convierte en un use-after-free de página. El desbordamiento nunca toca frags[0], por lo que no necesita sincronizarse temporalmente con el spray, que es en gran medida lo que lo hace tolerante.
De UAF de página a Dirty-Pagetable. La página de pipe liberada se reutiliza como tabla de páginas de último nivel al provocar un fallo de página en un mapeo anónimo nuevo. Una sola página física es ahora a la vez una tabla de páginas hoja activa y la página que el pipe sigue leyendo y escribiendo. Escribir ocho bytes en el pipe instala una PTE falsificada; leer el pipe devuelve el contenido de la tabla. Esto proporciona una lectura/escritura física arbitraria finita, del orden de 460 ventanas de PTE por tabla.
Derrotar KASLR. Con la lectura física escaneamos la tabla de páginas fija del trampolín SMP de memoria baja, que KASLR nunca reubica; su entrada de la mitad del kernel apunta a level4_kernel_pgt, la base física del kernel. Desde allí, init_top_pgt (reconocible por su entrada 511 auto-referenciada) es un traductor universal de virtual a físico, que recupera la base virtual del kernel y ata nuestro espacio de direcciones a la memoria física.
De lectura/escritura finita a infinita. Forjamos una PTE en la tabla hoja que apunta a la dirección física de la propia tabla. La ventana virtual correspondiente crea entonces un alias de la propia tabla de páginas, de modo que las PTE se convierten en memoria normal: lectura/escritura del kernel ilimitada y de acceso aleatorio, sin pipe en el bucle. La coherencia de TLB en anillo 3 se gestiona forzando un vaciado completo de mm con un mprotect() de tamaño excesivo.
Resolver offsets y tomar credenciales. Los offsets de los miembros de las estructuras se leen en tiempo de ejecución desde /sys/kernel/btf/vmlinux, no vienen integrados. Localizamos nuestro propio task_struct mediante el recorrido de struct page en el vmemmap y mm_struct.owner, ponemos a cero los ids de credenciales y rellenamos todos los conjuntos de capacidades en cred y real_cred. Las direcciones de los símbolos provienen de un resolvedor interno que analiza las tablas kallsyms del propio kernel a través de nuestra lectura arbitraria; solo si eso falla recurrimos a apuntar cred.user_ns a init_user_ns (de modo que CAP_SYSLOG se cumpla en el namespace inicial) y a leer las direcciones reales desde /proc/kallsyms.
Deshabilitar SELinux sin cambiar enforcing. Sobrescribimos el prólogo de avc_denied() con un stub xor eax, eax ; ret (saltando por encima de endbr64 bajo IBT). Toda denegación ahora devuelve concedido, en todo el proceso, mientras que getenforce sigue informando Enforcing. Esto también es un requisito previo para el escape: la re-ejecución entre dominios del manejador de core-dump, de otro modo, sería denegada.
Escape mediante core_pattern. Sobrescribimos el core_pattern global con un manejador prefijado con | que apunta de vuelta a nuestro propio binario a través de /proc/<PID>/root (el chroot del contenedor de la tarea que falla), y luego provocamos el fallo de un hijo. El kernel ejecuta el manejador como usermodehelper, root en los namespaces iniciales, y retransmitimos una shell root interactiva a través de un socket Unix enlazado dentro del contenedor. El manejador nace dentro de los namespaces init, por lo que no se necesita ninguna cirugía de namespaces in situ, ni ninguno de los peligros de sus PID namespaces.