
Prueba de concepto que demuestra la fuga de contenedores en Kubernetes mediante la corrupción de la caché de páginas del kernel CVE-2026-31431, logrando ejecución de código a nivel de nodo desde un contenedor sin privilegios.
Una prueba de concepto que demuestra cómo un contenedor completamente sin privilegios puede lograr ejecución de código a nivel de nodo en Kubernetes explotando el bug de corrupción de page-cache del kernel Linux CVE-2026-31431 a través de capas de imagen de contenedor compartidas.
Aviso: Este repositorio se publica únicamente con fines educativos y defensivos. Úsalo exclusivamente en sistemas que poseas o para los que tengas autorización explícita de prueba.
CVE-2026-31431 ("Copy Fail") es una vulnerabilidad del kernel Linux en la ruta de Copy-on-Write (CoW) del page-cache. Una condición de carrera en splice de AF_ALG permite que un proceso sin privilegios corrompa las páginas del page-cache de un archivo de solo lectura. La corrupción persiste en el page-cache del kernel y es visible para todos los procesos que posteriormente lean o ejecuten el archivo, incluidos procesos en otros contenedores o en el host.
Para más detalles sobre la vulnerabilidad original, consulta copy.fail.
La cadena de ataque tiene tres etapas: corrupción del page-cache, propagación entre contenedores y ejecución privilegiada.
El subsistema AF_ALG (cripto) del kernel expone una interfaz basada en sockets para operaciones criptográficas en espacio de usuario. El exploit abusa de una condición de carrera en cómo el kernel maneja splice() desde un archivo hacia un socket AF_ALG:
/usr/sbin/ipset) en modo solo lectura.authencesn(hmac(sha256),cbc(aes)).MSG_MORE, indicando al kernel que espera más datos.splice() del contenido del archivo objetivo desde un fd → pipe → socket AF_ALG.El exploit repite esto para cada ventana de 4 bytes hasta que todas las páginas cacheadas del binario objetivo se sobrescriben con un payload personalizado.
No se necesita permiso de escritura sobre el archivo. El archivo en disco no cambia; solo se corrompe el page-cache en memoria.
Los runtimes de contenedores (containerd, CRI-O) usan sistemas de archivos overlay. Cuando dos contenedores comparten la misma capa de imagen, el kernel sirve sus lecturas de archivos desde las mismas páginas del page-cache.
Esta imagen PoC está construida FROM registry.k8s.io/kube-proxy:v1.35.2. El DaemonSet de kube-proxy en cada nodo de Kubernetes usa exactamente la misma capa base. Como resultado, /usr/sbin/ipset en ambos contenedores se asigna al mismo conjunto de páginas del page-cache.
Cuando el contenedor PoC sin privilegios corrompe el page-cache de ipset, la corrupción es inmediatamente visible para el contenedor privilegiado de kube-proxy en el mismo nodo, sin comunicación alguna entre contenedores.
kube-proxy se ejecuta como un DaemonSet privilegiado con hostNetwork: true. Invoca periódicamente /usr/sbin/ipset para gestionar reglas de iptables/ipset. Cuando ejecuta ipset la próxima vez, el kernel carga las páginas corruptas del page-cache, ejecutando el payload del atacante con todos los privilegios de kube-proxy:
El payload de este PoC (payload/payload.c) simplemente monta el sistema de archivos raíz del host y escribe un archivo marcador en /root/res como prueba de ejecución de código a nivel de nodo.
┌──────────────────────────┐ ┌──────────────────────────┐
│ Contenedor PoC │ │ Contenedor kube-proxy │
│ (sin privilegios) │ │ (privilegiado) │
│ │ │ │
│ 1. Abre /usr/sbin/ipset │ │ │
│ (solo lectura) │ │ │
│ │ │ │
│ 2. Condición de carrera │ │ │
│ en splice de AF_ALG │ │ │
│ corrompe page-cache │ │ │
│ │ │ │ │
└──────────┼───────────────┘ └──────────────────────────┘
│ │
▼ │
┌─────────────────────┐ │
│ Page-Cache Kernel │ │
│ /usr/sbin/ipset │◄────────────────────┘
│ (CORRUPTO) │ 3. kube-proxy ejecuta ipset
│ contiene los bytes │ → carga páginas corruptas
│ del payload del │ → el payload se ejecuta como
│ atacante │ root en el host
└─────────────────────┘
.
├── cmd/copyfail/main.go # Punto de entrada; incrusta el payload compilado
├── internal/
│ ├── exploit/
│ │ ├── exploit.go # Exploit principal: bucle de condición de carrera en splice de AF_ALG
│ │ └── patch.go # Divide el payload en ventanas de parcheo de 4 bytes
│ └── alg/
│ └── alg.go # Abstracción del socket AF_ALG AEAD
├── payload/
│ ├── payload.c # Payload de validación (monta fs del host, escribe marcador)
│ └── nolibc/ # Mini libc del kernel para payloads estáticos sin dependencias
├── deploy/
│ └── poc.yaml # Manifiesto de Deployment de Kubernetes
├── Dockerfile # Construido FROM kube-proxy para compartir capas de imagen
├── Makefile # Orquestación de compilación
└── docs/ # Evidencia de validación de ACK (Alibaba Cloud)
x86_64-linux-gnu-gcc)imagePullPolicy: IfNotPresent (el valor por defecto)# Compilar payload + binario Go
make build
# Compilar imagen Docker
make docker-build
# Compilar y publicar en GHCR
make docker-push IMAGE=ghcr.io/<tu-usuario>/copy-fail-poc TAG=latest
Para objetivos arm64:
make build CC=aarch64-linux-gnu-gcc GOARCH=arm64
kubectl apply -f deploy/poc.yaml
El Deployment crea un único pod sin privilegios. Este:
/bin/copyfail -target /usr/sbin/ipset para corromper el page-cache.Después de que kube-proxy ejecute ipset la próxima vez (esto suele ocurrir en cuestión de segundos debido a su bucle de reconciliación, o en su próximo reinicio), verifica el nodo:
# Conéctate por SSH al nodo, o usa un pod de depuración privilegiado
cat /root/res
# Salida esperada: [*] success
La presencia de /root/res en el sistema de archivos del host demuestra que el código proporcionado por el atacante se ejecutó con privilegios a nivel de nodo, escrito desde el contexto del contenedor privilegiado de kube-proxy.
kubectl delete -f deploy/poc.yaml
# En el/los nodo(s) afectado(s), elimina el marcador y reinicia kube-proxy:
rm -f /root/res
systemctl restart kubelet # o elimina el pod de kube-proxy para forzar un nuevo pull
kube-proxy es un objetivo ideal porque:
privileged: true, hostNetwork: true.imagePullPolicy: IfNotPresent — una vez que la imagen del atacante se descarga y comparte la misma capa base, las páginas del directorio inferior del overlay se comparten.Cualquier DaemonSet privilegiado cuya imagen contenga un binario predecible podría ser atacado de la misma manera.
El payload por defecto (payload/payload.c) es un programa de solo validación que escribe un archivo marcador. Para construir un payload personalizado:
payload/payload.c. El programa se compila contra nolibc (la biblioteca C mínima del kernel) para obtener un binario estático sin dependencias.make payload para compilar de forma cruzada.//go:embed.tools/include/nolibc/)El código Go del exploit en este repositorio se proporciona tal cual con fines de investigación.
El payload (payload/payload.c) se deriva de copy-fail-c y tiene doble licencia bajo LGPL-2.1-or-later OR MIT. Consulta LICENSE-LGPL y LICENSE-MIT.