
Функциональный PoC для CVE-2026-17106 (CopyEscape): гонка TOCTOU в docker cp, допускающая произвольную запись на хосте Docker. Лаборатория Docker + монитор inotify + LD_PRELOAD. Вариант для macOS безвреден, для Linux — разрушителен.
Образовательный PoC для CopyEscape (CVE-2026-17106), уязвимость состояния гонки TOCTOU в
docker cp, которая позволяет вредоносному контейнеру записывать произвольные файлы на хост Docker. Вдохновлено оригинальной работой masasron, который обнаружил и задокументировал уязвимость в блоге "CopyEscape: Taking Over Docker Hosts with docker cp".
CVE-2026-17106 — это уязвимость состояния гонки (TOCTOU) при извлечении tar-файла, который демон Docker создаёт во время docker cp <contenedor>:<ruta> <destino>. Затрагивает Docker CLI 29.6.1 и более ранние версии; исправлена начиная с 29.7.0. Ошибка находится в CLI (извлечение tar на хосте), а не в демоне.
Цепочка, названная CopyEscape, демонстрирует, как, имея только запущенный контейнер без дополнительных возможностей, атакующий может:
/watched/file.txt/ под обычный файл для любого процесса, открывающего его внутри контейнера, с помощью библиотеки LD_PRELOAD, перехватывающей open, openat, openat2, fopen, stat, lstat, fstatat, statx и варианты 64.aaa.txt размером 16 МиБ, вынуждающий демон тратить достаточно времени на создание tar, чтобы монитор успевал выиграть гонку.inotify обращения демона и в окне гонки заменять подкаталог escape/ на абсолютную символическую ссылку к целевой директории хоста ( в macOS или в Linux) с помощью двух атомарных с откатом.Результат — произвольная запись файлов на хост с правами процесса, выполняющего docker cp. Вариант Linux перезаписывает /usr/bin/runc и добивается выполнения кода с правами root на хосте при следующем docker run / exec.
Этот репозиторий содержит строго образовательные и защитные материалы.
runc хоста: используйте его только в одноразовой ВМ с проверенной резервной копией.PoC поставляется с безвредным вариантом для macOS (записывает ~/pwnd в качестве маркера), единственная цель которого — служить воспроизводимой демонстрацией.
┌─────────────────────────────────────────────────────────────────────┐
│ 1. LD_PRELOAD camufla /watched/file.txt como archivo regular │
│ open/openat/openat2/fopen/stat/lstat/fstatat/statx → decoy │
│ docker exec ... cat /watched/file.txt → "top-level file" │
└──────────────────────────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 2. Layout real dentro del contenedor │
│ /watched/file.txt/ │
│ ├── aaa.txt (16 MiB de 'B', cebo lento) │
│ ├── escape/ (directorio real inicial) │
│ ├── escape/<basename> (payload: runc o pwnd) │
│ └── .swap-escape (symlink → /Users/<usr> o /usr/bin) │
└──────────────────────────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 3. docker cp pide tar al daemon │
│ daemon recorre /watched/file.txt/ y abre aaa.txt │
│ inotify detecta IN_OPEN sobre aaa.txt │
└──────────────────────────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 4. Monitor pivot: dos rename(2) atomicos con rollback │
│ escape/ → .old-escape/ (backup) │
│ .swap-escape → escape/ (symlink al target del host) │
└──────────────────────────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 5. Tar resultante contiene: │
│ escape → /Users/<usr> (symlink absoluto) │
│ escape/<basename> (entrada hija que atraviesa el symlink) │
└──────────────────────────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 6. CLI vulnerable extrae symlink + hija │
│ crea escape → /Users/<usr> en el destino local │
│ extrae escape/<basename> A TRAVES del symlink │
│ escribe /Users/<usr>/<basename> en el host │
└──────────────────────────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ macOS: ~/pwnd con "COPYESCAPE_MACOS_DEMO" (demo inocua) │
│ Linux: /usr/bin/runc sobrescrito (RCE como root) │
└─────────────────────────────────────────────────────────────────────┘
PoC предназначена для запуска в macOS со следующими требованиями:
curl, tar, git и docker в PATH.alpine:3.21.HOME в виде /Users/<usuario>.Для варианта macOS root не требуется. Вариант Linux требует root и одноразовую VM.
Скрипт run-poc.sh автоматически загружает бинарный файл уязвимой CLI 29.6.1 с официального сайта Docker (download.docker.com). Он не изменяет установленную в системе Docker Desktop: используется текущий демон, но для операции docker cp вызывается загруженный бинарный файл.
POC CopyEscape (CVE-2026-17106)/
├── README.md ← Este archivo
├── LICENSE ← Licencia MIT
├── .gitignore
├── Dockerfile ← Dockerfile unificado (macos | linux)
├── entrypoint.sh ← Arranque del monitor sin LD_PRELOAD
├── monitor.c ← Monitor inotify unificado (una sola fuente)
├── watched_preload.c ← LD_PRELOAD con cobertura ampliada
└── run-poc.sh ← Runner robusto con reintentos
Каталог
cli-vulnerable/(загруженная CLI 29.6.1) исключается из репозитория через.gitignore. Скрипт автоматически загружает его, если он отсутствует.
cd poc-v2
./run-poc.sh
Скрипт:
copyescape-macos-pocv2:local.COPYESCAPE_HOST_HOME=$HOME./watched/file.txt станет читаемым внутри контейнера.docker cp с помощью уязвимой CLI.~/pwnd../run-poc.sh 10 # 10 intentos (la carrera es probabilistica)
./run-poc.sh --cleanup # 5 intentos y borra la imagen al final
[run-poc] CLI vulnerable: 29.6.1
[run-poc] Intento 1/5 (contenedor copyescape-pocv2-NNNN-RRRR)
[run-poc] /watched/file.txt lee: top-level file
[run-poc] docker cp (CLI 29.6.1)...
[run-poc] EXITO en intento 1: se creo /Users/<tu-usuario>/pwnd
Contenido del marcador: COPYESCAPE_MACOS_DEMO
[run-poc] Logs del monitor:
[monitor] plataforma=macos target_dir=/Users/<tu-usuario> target_file=/Users/<tu-usuario>/pwnd quiet_ms=400 trigger=aaa.txt
[monitor] pivoted /watched/file.txt/escape -> /Users/<tu-usuario>
[run-poc] Resumen: 1 exito en 1 intento(s).
rm -f -- "$HOME/pwnd"
rm -rf -- poc-v2/file.txt
docker image rm copyescape-macos-pocv2:local
После завершения цепочки следующие элементы служат доказательством:
Идея и стратегия принадлежат masasron: гонка TOCTOU против генератора tar, маскировка через LD_PRELOAD, приманка на 16 МиБ, переключение с помощью двух rename(2) и структура escape/ + .swap-escape. Без этого дизайна данная переработка не существовала бы. Концептуальная схема идентична.
Примерно 15-20 % составляют строки, совпадающие дословно (короткие утилиты и константы раскладки). Остальное — новый код.
docker cp из контейнера, которому не доверяете, и никогда от root в Linux.docker cp во время извлечения или блокировать на демоне tar-файлы, содержащие дочерние записи под символьной ссылкой.Распространяется под лицензией MIT. Подробнее см. в файле LICENSE.
/Users/<usuario>/usr/binrename(2)escape, за которой следует дочерняя запись escape/<basename>; уязвимая CLI создаёт символическую ссылку на клиенте и извлекает дочернюю запись через неё, записывая за пределы выбранного пользователем места назначения.| Ресурс | Ожидаемое доказательство |
|---|
~/pwnd | Файл с содержимым COPYESCAPE_MACOS_DEMO\n |
poc-v2/file.txt/ | Каталог (не файл) с escape -> /Users/<tu-usuario> (символическая ссылка) и aaa.txt (16 МиБ) |
docker logs <contenedor> | Строка pivoted /watched/file.txt/escape -> /Users/<tu-usuario> |
CLI 29.7.2 (docker cp con CLI parcheada) | Маркер не создаётся; PoC завершается неудачей |
| Файл | Исходные строки | Новые строки | Изменение |
|---|
watched_preload.c | 89 | 257 | Переписан: обёртки openat2, fstatat, statx, stat64, lstat64, open64, fopen64; потокобезопасный конструктор; защита от dlsym == NULL; O_TMPFILE |
monitor.c (macos + linux) | 259 + 233 = 492 (duplicados) | 344 (unificado) | Переписан и унифицирован с -DCOPYESCAPE_TARGET_*; откат в try_pivot; настраиваемые exit_quiet_ms и триггер; надёжное обнаружение |
entrypoint.sh | 3 | 5 | env -u LD_PRELOAD вместо unset |
Dockerfile | 22 | 43 | Унифицирован; закрепление digest; --platform; без -Werror; -static; LABEL |
demo-macos.sh → run-poc.sh | 88 | 166 | Переписан: повторы гонки; логи при сбое; --cleanup; проверка делегирована бинарному файлу |