
PoC fonctionnel de CVE-2026-17106 (CopyEscape) : course TOCTOU dans docker cp qui permet une écriture arbitraire sur l'hôte Docker. Laboratoire Docker + moniteur inotify + LD_PRELOAD. Variante macOS inoffensive et Linux destructrice.
Preuve de concept pédagogique de CopyEscape (CVE-2026-17106), une vulnérabilité de course TOCTOU dans
docker cpqui permet à un conteneur malveillant d'écrire des fichiers arbitraires sur l'hôte Docker. Inspirée des travaux originaux de masasron, qui a découvert et documenté la vulnérabilité dans le billet "CopyEscape: Taking Over Docker Hosts with docker cp".
CVE-2026-17106 est une vulnérabilité de condition de course (TOCTOU) dans l'extraction de l'archive tar que le démon Docker produit lors de docker cp <contenedor>:<ruta> <destino>. Elle affecte Docker CLI 29.6.1 et versions antérieures ; corrigée à partir de la 29.7.0. Le bug réside dans la CLI (extraction du tar sur l'hôte), pas dans le démon.
La chaîne baptisée CopyEscape montre comment, à partir d'uniquement un conteneur en cours d'exécution sans capacités supplémentaires, un attaquant peut :
/watched/file.txt/ en fichier régulier pour tout processus qui l'ouvre à l'intérieur du conteneur, via une bibliothèque LD_PRELOAD qui intercepte open, openat, openat2, fopen, stat, lstat, fstatat, statx et les variantes 64.aaa.txt de 16 Mio qui force le démon à mettre suffisamment de temps à produire le tar pour que le moniteur gagne la course.inotify les accès du démon et, dans la fenêtre de course, remplacer un sous-répertoire escape/ par un symlink absolu vers la cible de l'hôte ( sur macOS ou sur Linux) via deux atomiques avec rollback.Le résultat est une écriture arbitraire de fichiers sur l'hôte avec les permissions du processus qui exécute docker cp. La variante Linux écrase /usr/bin/runc et obtient une exécution de code en tant que root sur l'hôte au prochain docker run / exec.
Ce dépôt contient du matériel strictement éducatif et défensif.
runc de l'hôte : utilisez-la uniquement dans une VM jetable avec une sauvegarde vérifiée.La PoC est fournie avec une variante macOS inoffensive (elle écrit ~/pwnd comme marqueur) dont l'unique but est de servir de démonstration reproductible.
┌─────────────────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────────────────┘
La PoC est conçue pour s'exécuter sur macOS avec :
curl, tar, git et docker dans le PATH.alpine:3.21.HOME au format /Users/<usuario>.Aucun root n'est nécessaire pour la variante macOS. La variante Linux nécessite root et une VM jetable.
Le runner run-poc.sh télécharge automatiquement le binaire de la CLI vulnérable 29.6.1 depuis le site officiel de Docker (download.docker.com). Il ne modifie pas l'installation de Docker Desktop du système : il utilise le démon actuel mais invoque le binaire téléchargé pour l'opération 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
Le répertoire
cli-vulnerable/(CLI 29.6.1 téléchargée) est exclu du dépôt via.gitignore. Le runner la télécharge automatiquement si elle n'est pas présente.
cd poc-v2
./run-poc.sh
Le runner :
copyescape-macos-pocv2:local.COPYESCAPE_HOST_HOME=$HOME./watched/file.txt soit lisible dans le conteneur.docker cp avec la CLI vulnérable.~/pwnd../run-poc.sh 10 # 10 intentos (la carrera es probabilística)
./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
Une fois la chaîne terminée, les éléments suivants servent de preuve :
L'idée et la stratégie sont de masasron : la course TOCTOU contre le producteur de tar, le camouflage via LD_PRELOAD, le leurre de 16 Mio, le pivot avec deux rename(2), et la structure escape/ + .swap-escape. Sans cette conception, cette réécriture n'existerait pas. Le schéma conceptuel est identique.
Environ 15 à 20 % des lignes coïncident littéralement (utilitaires courts et constantes du layout). Le reste est du nouveau code.
docker cp depuis un conteneur non fiable, et jamais en root sur Linux.docker cp pendant l'extraction, ou bloquer dans le démon les tars contenant des entrées enfants sous un symlink.Distribué sous licence MIT. Consultez le fichier LICENSE pour plus de détails.
/Users/<usuario>/usr/binrename(2)escape suivi d'une entrée enfant escape/<basename> ; la CLI vulnérable crée le symlink sur le client et extrait l'entrée enfant à travers celui-ci, écrivant en dehors de la destination choisie par l'utilisateur.| Ressource | Preuve attendue |
|---|
~/pwnd | Fichier avec contenu COPYESCAPE_MACOS_DEMO\n |
poc-v2/file.txt/ | Répertoire (pas un fichier) avec escape -> /Users/<tu-usuario> (symlink) et aaa.txt (16 Mio) |
docker logs <contenedor> | Ligne pivoted /watched/file.txt/escape -> /Users/<tu-usuario> |
CLI 29.7.2 (docker cp avec CLI corrigée) | Le marqueur n'est pas créé ; la PoC échoue |
| Fichier | Lignes originales | Lignes nouvelles | Modification |
|---|
watched_preload.c | 89 | 257 | Réécrit : wrappers openat2, fstatat, statx, stat64, lstat64, open64, fopen64 ; constructeur thread-safe ; garde anti dlsym == NULL ; O_TMPFILE |
monitor.c (macos + linux) | 259 + 233 = 492 (dupliqués) | 344 (unifié) | Réécrit et unifié avec -DCOPYESCAPE_TARGET_* ; rollback dans try_pivot ; exit_quiet_ms et trigger configurables ; détection robuste |
entrypoint.sh | 3 | 5 | env -u LD_PRELOAD au lieu de unset |
Dockerfile | 22 | 43 | Unifié ; digest pin ; --platform ; sans -Werror ; -static ; LABEL |
demo-macos.sh → run-poc.sh | 88 | 166 | Réécrit : nouvelles tentatives de course ; logs en cas d'échec ; --cleanup ; validation déléguée au binaire |