Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
page_inject — CVE-2026-31431-killed page-cache exploit — ejecución de código en contenedores que comparten la misma capa de imagen | Kitploit
Herramientas/GitHubGitHub/sgkdev/page_inject
Análisis de VulnerabilidadesExplotaciónPost-ExplotaciónPruebas de PenetraciónRed TeamingEscape de ContenedoresExplotación de Binarios
GitHubsgkdev/page_inject

page_inject

CVE-2026-31431-killed page-cache exploit — ejecución de código en contenedores que comparten la misma capa de imagen

Ver Repositorio
7414hace 3 mesesRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

page_inject - Escape entre contenedores aead AF_ALG

Exploit de vulnerabilidad aead AF_ALG entre contenedores -- pivotar desde un contenedor comprometido a todos los contenedores hermanos que comparten la misma capa de imagen libc.so.6.

Esta es una primitiva de escape: se ejecuta desde dentro de un contenedor no privilegiado que el atacante ya ha comprometido, y utiliza el bug de escritura arbitraria de 4 bytes en la rotación ESN de authencesn de AF_ALG (CVE-2026-31431) para plantar un hook persistente de read() en las páginas de caché de página de libc.so.6. Debido a que Docker / containerd respaldan los archivos de la capa inferior de overlayfs con inodos compartidos, esas páginas son visibles para todos los contenedores hermanos instanciados desde la misma imagen -- el hook se activa también en sus procesos, y el atacante obtiene ejecución de comandos dentro de cada uno.

Modelo de amenaza

  • El atacante tiene acceso shell a un solo contenedor (llámelo victim) en un host que ejecuta otros contenedores (siblings) desde la misma imagen que victim.
  • victim se ejecuta con la configuración predeterminada de Docker/k8s: uid no privilegiado dentro del namespace de usuario del contenedor, perfil seccomp predeterminado, perfil AppArmor predeterminado, sin capacidades especiales, sin bind mounts del host.
  • victim tiene solo:
    • acceso de lectura a su propia libc (/usr/lib/x86_64-linux-gnu/libc.so.6 o donde la distribución la instale)
    • la familia de syscalls estándar socket(AF_ALG, ...)
    • las syscalls estándar splice / vmsplice
    • acceso de escritura a un directorio al que pueda hacer chmod +x (por ejemplo, /tmp)
  • El kernel debe ser vulnerable a CVE-2026-31431 (cualquier compilación de algif_aead + authencesn anterior a la corrección de reversión upstream).

Eso es todo. Sin CAP_* especial, sin acceso al sistema de archivos del host. El atacante coloca un binario autocontenido enlazado estáticamente dentro del contenedor, lo ejecuta, y la corrupción de la caché de página -- y por lo tanto el hook -- se vuelve visible para todos los hermanos.

Cómo se encadena el exploit

  1. Identidad de página de caché de página. Dentro de un contenedor overlayfs, /usr/lib/.../libc.so.6 es servido por el inodo ext4 de la capa inferior de la imagen. Cada contenedor iniciado desde la misma imagen comparte ese inodo subyacente, y la caché de página del kernel se clavea por el inodo subyacente -- no por el overlay ni por el namespace. Así, una sola escritura de 4 bytes en una página de caché de página es visible para todos los procesos de los contenedores hermanos que tengan esa página mapeada.

  2. La vulnerabilidad aead de AF_ALG convierte una escritura de este tipo en muchas. algif_aead encadena el iovec RX del usuario con los bytes authsize finales del SGL TX empalmado, y la rotación ESN de authencesn coloca 4 bytes del campo seq_high del AAD en dst[assoclen + cryptlen] -- que es el primer byte de esa cola externa encadenada. La página empalmada es una página de caché de página de un archivo al que el atacante solo tiene acceso de lectura, pero el cifrador copia bytes en ella de todos modos, sin contabilidad dirty. (Vea crypto/algif_aead.c y crypto/authencesn.c para la mecánica subyacente.)

  3. Inicialización de una primitiva invocable. Lo primero que hace page_inject es inicializar -- una reimplementación codificada en asm del mismo baile AF_ALG (), colocada dentro de la cueva de libc. Esto hace que la escritura de 4 bytes sea un regular desde dentro de cualquier payload de hook futuro, sin necesidad de configuración de socket por llamada.

Compilación

El inyector se compila fuera del contenedor víctima -- típicamente en la máquina de desarrollo del atacante -- porque la mayoría de las imágenes de contenedor de producción no incluyen un compilador. Un entorno de desarrollo Linux x86_64 estándar con gcc (con soporte de enlace -static) y nasm es suficiente.

root@kitploit:~
make            # assembles .asm sources via gen_arrays.sh, links static page_inject
make shellcode  # also produces inspectable .bin flat binaries
make clean      # removes generated files and the binary

La salida es un único ELF enlazado estáticamente (./page_inject) que se ejecuta en cualquier kernel Linux x86_64 moderno.

Entrega y uso (desde el contenedor víctima)

Una vez que el atacante tiene shell en victim, sube el binario a un directorio escribible (típicamente /tmp):

root@kitploit:~
# inside the compromised container, attacker session
victim$ ./page_inject

Sin argumentos, page_inject usa por defecto /usr/lib/x86_64-linux-gnu/libc.so.6 (la ubicación posterior a la fusión en Debian/Ubuntu). Para otras distribuciones, libc está en una ruta diferente; pásala explícitamente o usa --root / para escanear la tabla de búsqueda incorporada desde la raíz del contenedor:

root@kitploit:~
# Fedora / Rocky / CentOS
victim$ ./page_inject /usr/lib64/libc.so.6

# Arch
victim$ ./page_inject /usr/lib/libc.so.6

# Auto-detect, regardless of distro:
victim$ ./page_inject --root /

Cualquiera de las invocaciones hace lo mismo: analizar ELF de libc dentro del contenedor, instalar el hook en su caché de página, monitorear la tabla de slots durante ~30 s mientras los hermanos se registran, y ejecutar un id de una sola vez contra el primer hermano que se registró como verificación de cordura.

Después de la inicialización, ingresa al shell de comandos para manejar cualquier hermano registrado:

root@kitploit:~
victim$ ./page_inject --shell --no-bootstrap
=== page-cache shell ===
Containers (3):
  [0] 0x0018598d  <- target
  [1] 0x001859ab
  [2] 0x001859cd

inject:0018598d> exec id
uid=0(root) gid=0(root) groups=0(root)
inject:0018598d> target 0x001859ab
inject:001859ab> exec hostname
1ccd66abee9d
inject:001859ab> exec cat /etc/shadow
root:$6$.....
inject:001859ab> unhook
... read() prologue restored, slot table zeroed ...

unhook limpia el hook de todos los contenedores hermanos de una sola vez y permite que los hijos del hook se autofinalicen.

root@kitploit:~
Usage: page_inject [OPTIONS] [LIBC_PATH]

Options:
  --root <prefix>   Auto-resolve libc.so.6 under <prefix> using the
                    built-in fixed-path lookup table. Inside the
                    victim container that's normally --root / .
  --shell [0xKEY]   Drop into interactive command shell after
                    injection. Optional KEY pre-selects the target.
  --no-bootstrap    Skip injection (shell-only; hook must already
                    be live in the page cache).
  --timeout SEC     Slot monitoring timeout in --shell mode
                    (default 30 s).
  --help, -h        Show help.

Default libc (when no --root and no LIBC_PATH given):
  /usr/lib/x86_64-linux-gnu/libc.so.6

Ruta de inyección dual

Diferentes compilaciones de glibc dejan diferentes cantidades de espacio de cueva .text entre el segmento LOAD ejecutable y el siguiente LOAD de solo lectura. page_inject selecciona entre dos diseños en el momento de la inyección:

  • Path A -- solo libc (predeterminado). Tanto Zone C como Zone A residen en la cueva .text de libc. La tabla de slots + áreas CMD + OUTPUT residen en la sección .hash de libc -- datos hash SysV heredados que ld.so no lee en tiempo de ejecución ya que usa .gnu.hash en su lugar. Cuando .hash está ausente (toolchain moderna de Arch), page_inject talla la región de slots de la cola de .eh_frame_hdr en su lugar, después de reducir primero el campo fde_count para que el unwinder ya no considere los bytes liberados como parte del índice de búsqueda binaria de FDE (el unwinder pasa transparentemente a un escaneo lineal de .eh_frame para cualquier IP cuyo FDE solía estar en el rango truncado -- comportamiento exigido por LSB).

  • Path B -- trampolín de libc + payload de ld.so. Algunas compilaciones de glibc reducen la cueva de libc por debajo del tamaño necesario para el payload completo de Zone C + Zone A (Ubuntu 24.04 / glibc 2.39 incluye una cueva de 711 B). En ese caso, page_inject escribe un trampolín de 36 B en la cueva de libc -- hace la puerta de clave .bss de ruta rápida intra-libc -- y en la ruta lenta calcula la base en tiempo de ejecución de ld.so desde el slot GOT de libc para (un símbolo del lado de ld.so que cada glibc importa privadamente) y salta a una variante de Zone C con registro base en la cueva de . La tabla de slots + CMD + OUTPUT + clave permanecen en libc; la Zone C del lado de ld.so las alcanza a través de después de que el trampolín siembra .

Si ninguno de los diseños encaja, page_inject rechaza limpiamente sin escribir nada a libc o ld.so en disco o en la caché de página.

Manejo de prólogos de read()

Diferentes versiones de glibc emiten diferentes secuencias de apertura en read(). El inyector reconoce cada una, lee los bytes que el hook desplaza, y los emula en la ruta rápida de Zone C para que read() de un solo hilo se reanude correctamente en read+N:

El slot de emulación de ruta rápida en Zone C tiene el tamaño para el prólogo conocido más largo (8 bytes) más el jmp rel32 de 5 bytes; los prólogos más cortos llenan el byte final del slot con un relleno NOP para que la longitud total del slot sea constante.

Estructura de archivos

root@kitploit:~
page_inject/
  page_inject.c           Inyector principal: análisis ELF, primitiva de vulnerabilidad, selección de diseño de ruta dual, inyección + deshacer.
  zone_c.asm              Shellcode del despachador de hook Path-A.
  zone_c_ld.asm           Shellcode del despachador de hook Path-B (variante de base rbp).
  trampoline.asm          Stub de 36 bytes del lado de libc Path-B.
  write_cache.asm         Zone A (shellcode de primitiva de escritura de vulnerabilidad).
  gen_arrays.sh           Ensambla .asm -> asm_bytecode.c.
  asm_bytecode.c          [generado] arreglos de bytes de shellcode.
  Makefile                Sistema de compilación.

Matriz probada y compatible

El exploit ha sido verificado de extremo a extremo en las siguientes distribuciones snap de contenedor. Cada entrada ha tenido page_inject inyectado desde dentro de un contenedor y ha visto su hook activarse en un contenedor hermano iniciado desde la misma imagen; los comandos se ejecutaron correctamente a través del canal de caché de página; y unhook restauró limpiamente el estado de página de libc.

Notas operativas

  • page_inject está enlazado estáticamente deliberadamente para que el propio proceso del atacante no se vea afectado por el hook que instala.
  • page_inject reconoce una libc "ya enganchada" (E9 + nops en el prólogo de read()) y se niega a reinyectar. Si estás en un entorno de prueba y tu caché de página está atascada en ese estado, detén todos los contenedores que usen la imagen y ejecuta drop_caches para reiniciar.
Descargar herramienta
Zone A
write_cache.asm
.text
call
  • Instalación del hook. El inyector luego escribe Zone C (zone_c.asm) en la cueva .text de libc y parchea los primeros 7-12 bytes de read() con un salto E9 disp32 hacia ella. Los bytes desplazados del prólogo se emulan fielmente en la ruta rápida de Zone C (se reconocen tres prólogos diferentes de glibc -- vea "Manejo de prólogos" más abajo). El hook ahora está activo en la caché de página de libc.

  • Propagación del hook. Cada contenedor hermano ejecuta procesos que llaman a read() constantemente (demonios de registro, healthchecks, cat /etc/hostname, cualquier cosa). En la primera llamada de este tipo dentro de un contenedor hermano, el prólogo secuestrado salta a Zone C, que:

    • hace stat("/") del inodo raíz del contenedor (un ID estable por namespace), lo usa como clave del slot del contenedor,
    • escanea la tabla de slots en busca de una entrada existente con esa clave,
    • si está ausente, registra la clave y hace fork() de un hijo de bucle de comandos de larga duración que sondea el área CMD en busca de órdenes,
    • retorna a read()+N para que el llamante no se dé cuenta. El proceso hermano original sigue ejecutándose. A partir de ahora, el atacante tiene un demonio dentro de ese contenedor.
  • Canal de comandos. El atacante usa el mismo binario page_inject en modo --shell para escribir comandos en el área CMD de la región de slots. El hijo del hook de cada contenedor hermano registrado sondea, hace fork de /bin/sh -c <cmd>, captura stdout/stderr en el área OUTPUT, señala finalización, y vuelve a sondear. El shell muestra la salida. Debido a que cada escritura CMD/OUTPUT también pasa por la primitiva de la vulnerabilidad, no se necesita ningún privilegio especial.

  • Deshacer hook. Cuando se termina, unhook restaura los bytes originales del prólogo de read() y pone a cero la tabla de slots; los hijos del hook ven un slot vacío en su próxima iteración y se autofinalizan. Las modificaciones de la caché de página en sí son limpias (el kernel nunca marcó las páginas modificadas como dirty), así que una vez que se detienen todos los contenedores que tienen libc mapeada, un drop_caches revierte completamente la caché -- no queda ningún artefacto en disco.

  • _rtld_global
    .text
    ld.so
    .bss
    rbp + offset
    rbp = libc_base
    Rango de glibcPrólogo (después de endbr64 opcional)Notas
    2.36 / 2.39cmpb $0x0, __libc_single_threaded(%rip)7 bytes; cmpb emulado establece ZF para el jne .Lthreaded original.
    2.43push rbp; movsxd rdi,edi; xor r9d,r9d7 bytes; emulado byte por byte.
    2.31 / 2.35mov eax, fs:[0x18]8 bytes; emulado byte por byte (el [disp32] con prefijo FS es absoluto, no relativo a RIP, por lo que la copia de bytes es fiel).
    ImagenglibcRuta de inyecciónPrólogo de read()Región de slot
    debian:bookworm2.36Acmpb.hash
    ubuntu:24.042.39Bcmpb.hash (abordado via rbp desde ld.so)
    ubuntu:22.042.35ATLS-fs.hash
    fedora:402.39Acmpb.hash
    archlinux:latest2.43Apush-rbp.eh_frame_hdr (cola truncada)