
Proof-of-concept che dimostra l'evasione dal container su Kubernetes tramite la corruzione della page-cache del kernel CVE-2026-31431, ottenendo l'esecuzione di codice a livello di nodo da un container non privilegiato.
Una prova di concetto che dimostra come un container completamente non privilegiato possa ottenere esecuzione di codice a livello di nodo su Kubernetes sfruttando il bug di corruzione della page-cache del kernel Linux CVE-2026-31431 attraverso i layer di immagine container condivisi.
Disclaimer: Questo repository è pubblicato esclusivamente a scopo educativo e difensivo. Utilizzalo solo su sistemi di tua proprietà o per i quali hai esplicita autorizzazione al test.
CVE-2026-31431 ("Copy Fail") è una vulnerabilità del kernel Linux nel percorso Copy-on-Write (CoW) della page-cache. Una race condition su AF_ALG splice consente a un processo non privilegiato di corrompere le pagine della page-cache di un file in sola lettura. La corruzione persiste nella page-cache del kernel ed è visibile a ogni processo che successivamente legge o esegue il file — inclusi i processi in altri container o sull'host.
Per tutti i dettagli sulla vulnerabilità originale, consulta copy.fail.
La catena di attacco ha tre fasi: corruzione della page-cache, propagazione tra container ed esecuzione privilegiata.
Il sottosistema AF_ALG (crypto) del kernel espone un'interfaccia basata su socket per operazioni crittografiche in userspace. L'exploit abusa di una race condition nel modo in cui il kernel gestisce splice() da un file verso un socket AF_ALG:
/usr/sbin/ipset) in sola lettura.authencesn(hmac(sha256),cbc(aes)).MSG_MORE, comunicando al kernel di aspettarsi altri dati.splice() del contenuto del file di destinazione da fd → pipe → socket AF_ALG.L'exploit ripete questa operazione per ogni finestra di 4 byte finché l'intera cache delle pagine del binario di destinazione non viene sovrascritta con un payload personalizzato.
Non è necessaria alcuna autorizzazione di scrittura sul file. Il file su disco rimane invariato — solo la page-cache in memoria viene corrotta.
I runtime container (containerd, CRI-O) utilizzano filesystem overlay. Quando due container condividono lo stesso layer di immagine, il kernel serve le loro letture dei file dalle stesse pagine della page-cache.
Questa immagine PoC è costruita FROM registry.k8s.io/kube-proxy:v1.35.2. Il DaemonSet kube-proxy su ogni nodo Kubernetes utilizza esattamente lo stesso layer di base. Di conseguenza, /usr/sbin/ipset in entrambi i container punta allo stesso insieme di pagine della page-cache.
Quando il container PoC non privilegiato corrompe la page-cache di ipset, la corruzione è immediatamente visibile al container kube-proxy privilegiato sullo stesso nodo — senza alcuna comunicazione tra container.
kube-proxy viene eseguito come DaemonSet privilegiato con hostNetwork: true. Invoca periodicamente /usr/sbin/ipset per gestire le regole iptables/ipset. Alla successiva esecuzione di ipset, il kernel carica le pagine della page-cache corrotte, eseguendo il payload dell'attaccante con tutti i privilegi di kube-proxy:
Il payload in questo PoC (payload/payload.c) monta semplicemente il filesystem root dell'host e scrive un file marcatore in /root/res come prova dell'esecuzione di codice a livello di nodo.
┌──────────────────────────┐ ┌──────────────────────────┐
│ Container PoC │ │ Container kube-proxy │
│ (non privilegiato) │ │ (privilegiato) │
│ │ │ │
│ 1. Apre /usr/sbin/ipset │ │ │
│ (sola lettura) │ │ │
│ │ │ │
│ 2. Race condition │ │ │
│ AF_ALG splice │ │ │
│ corrompe la │ │ │
│ page-cache │ │ │
│ │ │ │ │
└──────────┼───────────────┘ └──────────────────────────┘
│ │
▼ │
┌─────────────────────┐ │
│ Page-Cache Kernel │ │
│ /usr/sbin/ipset │◄────────────────────┘
│ (CORROTTA) │ 3. kube-proxy esegue ipset
│ contiene i byte │ → carica le pagine corrotte
│ del payload │ → il payload viene eseguito
│ dell'attaccante │ come root sull'host
└─────────────────────┘
.
├── cmd/copyfail/main.go # Punto di ingresso; incorpora il payload compilato
├── internal/
│ ├── exploit/
│ │ ├── exploit.go # Exploit principale: loop della race condition AF_ALG splice
│ │ └── patch.go # Divide il payload in finestre di patch da 4 byte
│ └── alg/
│ └── alg.go # Astrazione del socket AF_ALG AEAD
├── payload/
│ ├── payload.c # Payload di validazione (monta fs host, scrive marcatore)
│ └── nolibc/ # Mini libc del kernel per payload statici senza dipendenze
├── deploy/
│ └── poc.yaml # Manifest del Deployment Kubernetes
├── Dockerfile # Costruito FROM kube-proxy per condividere i layer di immagine
├── Makefile # Orchestrazione della build
└── docs/ # Prove di validazione da ACK (Alibaba Cloud)
x86_64-linux-gnu-gcc)imagePullPolicy: IfNotPresent (il valore predefinito)# Compila payload + binario Go
make build
# Compila immagine Docker
make docker-build
# Compila e pubblica su GHCR
make docker-push IMAGE=ghcr.io/<tuo-utente>/copy-fail-poc TAG=latest
Per target arm64:
make build CC=aarch64-linux-gnu-gcc GOARCH=arm64
kubectl apply -f deploy/poc.yaml
Il Deployment crea un singolo pod non privilegiato. Esso:
/bin/copyfail -target /usr/sbin/ipset per corrompere la page-cache.Dopo che kube-proxy esegue nuovamente ipset (in genere entro pochi secondi grazie al suo loop di riconciliazione, o al suo successivo riavvio), controlla il nodo:
# Accedi via SSH al nodo, oppure usa un pod di debug privilegiato
cat /root/res
# Output atteso: [*] success
La presenza di /root/res sul filesystem dell'host dimostra che il codice fornito dall'attaccante è stato eseguito con privilegi a livello di nodo — scritto dal contesto del container privilegiato di kube-proxy.
kubectl delete -f deploy/poc.yaml
# Sul nodo/i interessato/i, rimuovi il marcatore e riavvia kube-proxy:
rm -f /root/res
systemctl restart kubelet # oppure elimina il pod kube-proxy per forzare un nuovo pull
kube-proxy è un bersaglio ideale perché:
privileged: true, hostNetwork: true.imagePullPolicy: IfNotPresent — una volta che l'immagine dell'attaccante viene scaricata e condivide lo stesso layer di base, le pagine del lower-dir overlay sono condivise.Qualsiasi DaemonSet privilegiato la cui immagine contenga un binario prevedibile potrebbe essere preso di mira allo stesso modo.
Il payload predefinito (payload/payload.c) è un programma di sola validazione che scrive un file marcatore. Per creare un payload personalizzato:
payload/payload.c. Il programma è compilato contro nolibc (la libreria C minima del kernel) per ottenere un binario statico senza dipendenze.make payload per la cross-compilazione.//go:embed.tools/include/nolibc/)Il codice Go dell'exploit in questo repository è fornito così com'è a scopo di ricerca.
Il payload (payload/payload.c) deriva da copy-fail-c ed è concesso in doppia licenza LGPL-2.1-or-later OR MIT. Consulta LICENSE-LGPL e LICENSE-MIT.