
Доказательство концепции, демонстрирующее побег из контейнера в Kubernetes через повреждение page-cache ядра CVE-2026-31431, позволяющее достичь выполнения кода на уровне узла из непривилегированного контейнера.
Proof-of-concept, демонстрирующий, как полностью непривилегированный контейнер может достичь выполнения кода на уровне узла в Kubernetes, эксплуатируя уязвимость ядра Linux CVE-2026-31431 (повреждение page-cache) через общие слои образов контейнеров.
Дисклеймер: Этот репозиторий опубликован исключительно в образовательных и защитных целях. Используйте его только на системах, которыми вы владеете или на тестирование которых у вас есть явное разрешение.
CVE-2026-31431 («Copy Fail») — это уязвимость ядра Linux в пути Copy-on-Write (CoW) page-cache. Гонка AF_ALG splice позволяет непривилегированному процессу повредить страницы page-cache доступного только для чтения файла. Повреждение сохраняется в page-cache ядра и видно каждому процессу, который впоследствии читает или исполняет этот файл — включая процессы в других контейнерах или на хосте.
Полные подробности об исходной уязвимости см. на copy.fail.
Цепочка атаки состоит из трёх этапов: повреждение page-cache, распространение между контейнерами и привилегированное выполнение.
Подсистема AF_ALG (криптографическая) ядра предоставляет интерфейс на основе сокетов для пользовательских криптографических операций. Эксплойт злоупотребляет состоянием гонки в том, как ядро обрабатывает splice() из файла в сокет AF_ALG:
/usr/sbin/ipset) только для чтения.authencesn(hmac(sha256),cbc(aes)).MSG_MORE, сообщая ядру, что ожидаются дополнительные данные.splice() содержимого целевого файла из fd → pipe → сокет AF_ALG.Эксплойт повторяет это для каждого 4-байтового окна, пока все кэшированные страницы целевого бинарного файла не будут перезаписаны пользовательской полезной нагрузкой.
Права на запись в файл не требуются. Файл на диске не изменяется — повреждается только page-cache в памяти.
Среды выполнения контейнеров (containerd, CRI-O) используют overlay-файловые системы. Когда два контейнера используют один и тот же слой образа, ядро обслуживает их чтения файлов из одних и тех же страниц page-cache.
Этот образ PoC собран FROM registry.k8s.io/kube-proxy:v1.35.2. DaemonSet kube-proxy на каждом узле Kubernetes использует точно такой же базовый слой. В результате /usr/sbin/ipset в обоих контейнерах сопоставляется с идентичным набором страниц page-cache.
Когда непривилегированный контейнер PoC повреждает page-cache ipset, повреждение немедленно становится видимым привилегированному контейнеру kube-proxy на том же узле — без какой-либо связи между контейнерами.
kube-proxy работает как привилегированный DaemonSet с hostNetwork: true. Он периодически вызывает /usr/sbin/ipset для управления правилами iptables/ipset. При следующем выполнении ipset ядро загружает повреждённые страницы page-cache, выполняя полезную нагрузку атакующего с полными привилегиями kube-proxy:
Полезная нагрузка в этом PoC (payload/payload.c) просто монтирует корневую файловую систему хоста и записывает файл-маркер в /root/res как доказательство выполнения кода на уровне узла.
┌──────────────────────────┐ ┌──────────────────────────┐
│ PoC Container │ │ kube-proxy Container │
│ (unprivileged) │ │ (privileged) │
│ │ │ │
│ 1. Open /usr/sbin/ipset │ │ │
│ (read-only) │ │ │
│ │ │ │
│ 2. AF_ALG splice race │ │ │
│ corrupts page cache │ │ │
│ │ │ │ │
└──────────┼───────────────┘ └──────────────────────────┘
│ │
▼ │
┌─────────────────────┐ │
│ Kernel Page Cache │ │
│ /usr/sbin/ipset │◄────────────────────┘
│ (CORRUPTED) │ 3. kube-proxy executes ipset
│ contains attacker's │ → loads corrupted pages
│ payload bytes │ → payload runs as root
└─────────────────────┘ on the host
.
├── cmd/copyfail/main.go # Точка входа; встраивает скомпилированную полезную нагрузку
├── internal/
│ ├── exploit/
│ │ ├── exploit.go # Основной эксплойт: цикл гонки AF_ALG splice
│ │ └── patch.go # Разбивает полезную нагрузку на 4-байтовые окна патчей
│ └── alg/
│ └── alg.go # Абстракция сокета AF_ALG AEAD
├── payload/
│ ├── payload.c # Валидационная полезная нагрузка (монтирование ФС хоста, запись маркера)
│ └── nolibc/ # Минимальная libc ядра для статических полезных нагрузок без зависимостей
├── deploy/
│ └── poc.yaml # Манифест Deployment Kubernetes
├── Dockerfile # Собран FROM kube-proxy для совместного использования слоёв образа
├── Makefile # Оркестрация сборки
└── docs/ # Доказательства валидации из ACK (Alibaba Cloud)
x86_64-linux-gnu-gcc)imagePullPolicy: IfNotPresent (по умолчанию)# Сборка полезной нагрузки + Go-бинарника
make build
# Сборка Docker-образа
make docker-build
# Сборка и публикация в GHCR
make docker-push IMAGE=ghcr.io/<you>/copy-fail-poc TAG=latest
Для целей arm64:
make build CC=aarch64-linux-gnu-gcc GOARCH=arm64
kubectl apply -f deploy/poc.yaml
Deployment создаёт один непривилегированный под. Он:
/bin/copyfail -target /usr/sbin/ipset для повреждения page-cache.После того как kube-proxy в следующий раз выполнит ipset (обычно это происходит в течение секунд из-за его цикла согласования или при следующем перезапуске), проверьте узел:
# SSH на узел или используйте привилегированный debug-под
cat /root/res
# Ожидаемый вывод: [*] success
Наличие /root/res в файловой системе хоста доказывает, что код, предоставленный атакующим, выполнился с привилегиями уровня узла — записанный из контекста привилегированного контейнера kube-proxy.
kubectl delete -f deploy/poc.yaml
# На затронутых узлах удалите маркер и перезапустите kube-proxy:
rm -f /root/res
systemctl restart kubelet # или удалите под kube-proxy, чтобы принудительно перезагрузить образ
kube-proxy — идеальная цель, потому что он:
privileged: true, hostNetwork: true.imagePullPolicy: IfNotPresent — как только образ атакующего загружен и использует тот же базовый слой, страницы нижнего каталога overlay являются общими.Любой привилегированный DaemonSet, чей образ содержит предсказуемый бинарник, может быть атакован аналогичным образом.
Полезная нагрузка по умолчанию (payload/payload.c) — это программа только для валидации, которая записывает файл-маркер. Для создания пользовательской полезной нагрузки:
payload/payload.c. Программа собирается с nolibc (минимальной библиотекой C ядра) для статического бинарника без зависимостей.make payload для кросс-компиляции.//go:embed.tools/include/nolibc/)Go-код эксплойта в этом репозитории предоставляется как есть для исследовательских целей.
Полезная нагрузка (payload/payload.c) является производной от copy-fail-c и распространяется под двойной лицензией LGPL-2.1-or-later OR MIT. См. LICENSE-LGPL и LICENSE-MIT.