
PoC: escape de contenedor completamente no privilegiado que permite la ejecución de código a nivel de nodo en Kubernetes mediante la corrupción de la caché de páginas de CVE-2026-31431 + capas de imagen compartidas. Validado en Alibaba Cloud ACK, Amazon EKS y Google GKE.
Una prueba de concepto que demuestra cómo un contenedor completamente no privilegiado puede lograr ejecución de código a nivel de nodo en Kubernetes explotando el bug de corrupción de la caché de páginas del kernel Linux CVE-2026-31431 mediante capas de imagen de contenedor compartidas.
La primitiva de ataque central es: cualquier DaemonSet privilegiado que comparta capas de imagen con un contenedor controlado por el atacante puede ser utilizado como arma para el escape de contenedor. Este PoC usa kube-proxy como un ejemplo concreto, pero la técnica se generaliza a cualquier carga de trabajo privilegiada del clúster.
Validado en Alibaba Cloud ACK, Amazon EKS y Google GKE: un pod no privilegiado escribe [*] success en el sistema de archivos del host a través del DaemonSet privilegiado kube-proxy:
| Alibaba Cloud ACK (kernel 6.6.88) | Amazon EKS (kernel 6.12.79) | Google GKE (kernel 6.12.68) |
|---|---|---|
![]() | ![]() | ![]() |
Aviso legal: 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 pruebas.
CVE-2026-31431 («Copy Fail») es una vulnerabilidad del kernel Linux en la ruta de Copy-on-Write (CoW) de la caché de páginas. Una condición de carrera de splice en AF_ALG permite que un proceso no privilegiado corrompa las páginas de la caché de páginas de un archivo de solo lectura. La corrupción persiste en la caché de páginas del kernel y es visible para todos los procesos que posteriormente lean o ejecuten el archivo, incluidos los procesos de otros contenedores o del host.
Para obtener más detalles sobre la vulnerabilidad original, consulta copy.fail.
El ataque explota tres propiedades que suelen coexistir en los clústeres de Kubernetes:
privileged: true, hostNetwork: true, capabilities amplias, etc.) que periódicamente ejecutan binarios de su imagen.Cuando estas condiciones confluyen, un pod no privilegiado puede corromper un binario en una capa de imagen compartida, y un DaemonSet privilegiado en el mismo nodo ejecutará sin saberlo el binario corrupto con sus privilegios elevados, logrando una ejecución de código completa a nivel de nodo.
El objetivo de la vulnerabilidad NO se limita a kube-proxy. Cualquier DaemonSet privilegiado (agentes de monitoreo, plugins CNI, recopiladores de logs, agentes de seguridad, etc.) cuya imagen de contenedor comparta capas con una imagen controlada por el atacante es un objetivo viable.
La cadena de ataque consta de tres etapas: corrupción de la caché de páginas, propagación entre contenedores y ejecución privilegiada.
El subsistema AF_ALG (crypto) del kernel expone una interfaz basada en sockets para operaciones criptográficas en el espacio de usuario. El exploit abusa de una condición de carrera en la forma en que el kernel maneja splice() desde un archivo hacia un socket AF_ALG:
authencesn(hmac(sha256),cbc(aes)).MSG_MORE, indicando al kernel que espere más datos.splice() para pasar el 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 quedan sobrescritas con un payload personalizado.
No se necesita permiso de escritura sobre el archivo. El archivo en disco no cambia; solo se corrompe la caché de páginas en memoria.
Los runtimes de contenedores usan sistemas de archivos overlay. Cuando dos contenedores comparten la misma capa de imagen, el kernel atiende sus lecturas de archivos desde las mismas páginas de la caché de páginas.
El atacante construye su imagen PoC FROM la misma imagen base que el DaemonSet privilegiado objetivo. Debido a que ambos contenedores comparten el mismo lower-dir del overlay, los binarios de la capa compartida se asignan a páginas idénticas de la caché de páginas.
Cuando el contenedor PoC no privilegiado corrompe la caché de páginas de un binario, la corrupción es inmediatamente visible para el contenedor privilegiado del mismo nodo, sin ninguna comunicación entre contenedores.
Cuando el DaemonSet privilegiado ejecuta a continuación cualquier binario corrupto (dentro de su ciclo normal de operación), el kernel carga las páginas corruptas de la caché de páginas. El payload del atacante se ejecuta con todos los privilegios del DaemonSet, lo que potencialmente incluye:
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 la ejecución de código a nivel de nodo.
┌──────────────────────────┐ ┌──────────────────────────┐
│ PoC Container │ │ Privileged DaemonSet │
│ (unprivileged) │ │ (e.g. kube-proxy, │
│ │ │ monitoring agent, etc.)│
│ 1. Open target binary │ │ │
│ (read-only) │ │ │
│ │ │ │
│ 2. AF_ALG splice race │ │ │
│ corrupts page cache │ │ │
│ │ │ │ │
└──────────┼───────────────┘ └──────────────────────────┘
│ │
▼ │
┌─────────────────────┐ │
│ Kernel Page Cache │ │
│ │◄────────────────────┘
│ Shared-layer binary │ 3. DaemonSet executes the
│ (CORRUPTED) │ corrupted binary
│ contains attacker's │ → loads corrupted pages
│ payload bytes │ → payload runs with
└─────────────────────┘ DaemonSet's privileges
El PoC se ha validado con éxito en las siguientes plataformas Kubernetes gestionadas:
| Propiedad | Valor |
|---|---|
| Plataforma | Alibaba Cloud Container Service for Kubernetes (ACK) |
| Kubernetes | v1.35.2 |
| Kernel del nodo | 6.6.88-4.2.alnx4.x86_64 |
| kube-proxy | registry-cn-*.ack.aliyuncs.com/acs/kube-proxy:v1.35.2-aliyun.1 |
| Imagen base | registry.k8s.io/kube-proxy:v1.35.2 (upstream) |
| Dispositivo raíz | /dev/vda3 (ext4) |

| Propiedad | Valor |
|---|---|
| Plataforma | Amazon Elastic Kubernetes Service (EKS) |
| Kubernetes | v1.35.4 |
| Kernel del nodo | 6.12.79-101.147.amzn2023.x86_64 |
| kube-proxy | ***.dkr.ecr.***.amazonaws.com.cn/eks/kube-proxy:v1.35.3-eksbuild.2 |
| Imagen base | public.ecr.aws/eks-distro-build-tooling/eks-distro-minimal-base-iptables:2026-03-11-1773190710.2023 |
| Dispositivo raíz | /dev/nvme0n1p1 (xfs) |

| Propiedad | Valor |
|---|---|
| Plataforma | Google Kubernetes Engine (GKE) |
| Kubernetes | v1.35.3-gke.1234000 |
| SO del nodo | Container-Optimized OS (COS) 125, BUILD_ID 19216.220.72 |
| Kernel del nodo | 6.12.68+ x86_64 |
| kube-proxy | us-central1-artifactregistry.gcr.io/gke-release/gke-release/kube-proxy:v1.35.3-gke.1234000 |
| Imagen base | Igual que kube-proxy (imagen de Artifact Registry gestionada por el proveedor GKE) |
| Dispositivo raíz | /dev/dm-0 (ext2, de solo lectura); /dev/sda1 (ext4, partición de estado escribible) |
| Ruta del marcador | /mnt/stateful_partition/copyfail-res |

En los tres casos, un pod PoC no privilegiado escribió correctamente el archivo marcador [*] success en el sistema de archivos del host, lo que demuestra la ejecución de código a nivel de nodo a través del DaemonSet privilegiado kube-proxy.
Para las guías completas (análisis de capas de imagen, pasos de compilación, despliegue):
Este PoC usa kube-proxy como objetivo porque es uno de los DaemonSets privilegiados más comunes en los clústeres de Kubernetes. Se proporcionan tres variantes:
FROM registry.k8s.io/kube-proxy:v1.35.2 (ver Dockerfile)FROM public.ecr.aws/eks-distro-build-tooling/eks-distro-minimal-base-iptables:2026-03-11-1773190710.2023 (ver Dockerfile.eks)FROM us-central1-artifactregistry.gcr.io/gke-release/gke-release/kube-proxy:v1.35.3-gke.1234000 (ver Dockerfile.gke)Todas las variantes corrompen binarios como /usr/sbin/ipset, /usr/sbin/nft, /usr/sbin/xtables-legacy-multi y /usr/sbin/xtables-nft-multi.
Advertencias importantes:
ipset cuando está configurado en modo ipvs. El modo predeterminado (iptables) no usa ipset. Consulta kubernetes/enhancements#5495 para conocer el plan de deprecación de ipvs.ipset, nft, xtables-legacy-multi, xtables-nft-multi) para cubrir diferentes modos de proxy, pero el que se invoquen o no depende de la configuración del clúster.Si kube-proxy no es privilegiado en tu clúster, el principio del ataque sigue siendo válido: solo necesitas identificar otro DaemonSet privilegiado que comparta capas de imagen con una imagen base a partir de la cual puedas construir.
Para adaptar este PoC a otro DaemonSet privilegiado:
FROM la misma imagen base que usa ese DaemonSet..
├── cmd/copyfail/main.go # Entry point; embeds compiled payload
├── internal/
│ ├── exploit/
│ │ ├── exploit.go # Core exploit: AF_ALG splice race loop
│ │ └── patch.go # Splits payload into 4-byte patch windows
│ └── alg/
│ └── alg.go # AF_ALG AEAD socket abstraction
├── payload/
│ ├── payload.c # ACK/upstream payload (mount /dev/vda3 ext4)
│ ├── payload-eks.c # EKS payload (NVMe/Xen device auto-detection)
│ ├── payload-gke.c # GKE payload (COS/Ubuntu device auto-detection)
│ └── nolibc/ # Kernel's tiny libc for static, no-dependency payloads
├── deploy/
│ ├── poc.yaml # Kubernetes Deployment manifest (ACK/upstream)
│ ├── poc-eks.yaml # EKS Deployment manifest
│ └── poc-gke.yaml # GKE Deployment manifest
├── Dockerfile # ACK/upstream: FROM registry.k8s.io/kube-proxy
├── Dockerfile.eks # EKS: FROM eks-distro-minimal-base-iptables
├── Dockerfile.gke # GKE: FROM gke-release/kube-proxy
├── Makefile # Build orchestration (includes *-eks and *-gke targets)
└── docs/
├── eks-poc.md # EKS PoC full walkthrough
├── gke-poc.md # GKE PoC full walkthrough
├── ack-poc-res.png # ACK validation screenshot
├── eks-poc-res.png # EKS validation screenshot
└── gke-poc-res.png # GKE validation screenshot
x86_64-linux-gnu-gcc)imagePullPolicy: IfNotPresent en el DaemonSet objetivo (el valor predeterminado de Kubernetes)# Build payload + Go binary
make build
# Build Docker image
make docker-build
# Build and push to GHCR
make docker-push IMAGE=ghcr.io/<you>/copy-fail-poc TAG=latest
# Build EKS payload + Go binary + Docker image
make docker-build-eks
# Build and push to GHCR
make docker-push-eks IMAGE=ghcr.io/<you>/copy-fail-poc
Para objetivos arm64 (Graviton):
make build-eks CC=aarch64-linux-gnu-gcc GOARCH=arm64
# Build GKE payload + Go binary + Docker image
make docker-build-gke
# Build and push to GHCR
make docker-push-gke IMAGE=ghcr.io/<you>/copy-fail-poc
Para nodos arm64:
make docker-build-gke CC=aarch64-linux-gnu-gcc GOARCH=arm64 PLATFORM=linux/arm64
# ACK / upstream Kubernetes
kubectl apply -f deploy/poc.yaml
# Amazon EKS
kubectl apply -f deploy/poc-eks.yaml
# Google GKE
kubectl apply -f deploy/poc-gke.yaml
El Deployment crea un único pod no privilegiado. Este pod:
/bin/copyfail para corromper la caché de páginas de los binarios objetivo en la capa de imagen compartida.La próxima vez que el DaemonSet privilegiado objetivo ejecute un binario corrupto (en kube-proxy, esto suele ocurrir en cuestión de segundos debido a su bucle de reconciliación), verifica el nodo:
# SSH into the node, or use a privileged debug pod
# ACK / EKS (writable root filesystem)
cat /root/res
# Expected output: [*] success
# GKE COS nodes (read-only root, writable stateful partition)
cat /mnt/stateful_partition/copyfail-res
# Expected output: [*] success
La presencia del archivo marcador en el sistema de archivos del host demuestra que el código suministrado por el atacante se ejecutó con privilegios a nivel de nodo, desde el contexto del contenedor del DaemonSet privilegiado.
kubectl delete -f deploy/poc.yaml # or poc-eks.yaml / poc-gke.yaml
# On the affected node(s), remove the marker and restart the target DaemonSet:
rm -f /root/res # ACK / EKS
rm -f /copyfail-res /mnt/stateful_partition/copyfail-res # GKE COS nodes
# For kube-proxy: delete the pod to force image layer re-read
kubectl delete pod -n kube-system -l k8s-app=kube-proxy --field-selector spec.nodeName=<node>
El payload predeterminado (payload/payload.c) es un programa únicamente de validación que escribe un archivo marcador. Para crear 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 y sin dependencias.make payload para compilar de forma cruzada.//go:embed.privileged: true.AF_ALG. La regla está disponible a través de los enforcers de AppArmor y BPF.tools/include/nolibc/)El código del exploit en Go de 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: LGPL-2.1-or-later O MIT. Consulta LICENSE-LGPL y LICENSE-MIT.