## https://sploitus.com/exploit?id=25FB4F14-D5EF-52FA-A282-EDDD3ACBC28B
# CopyEscape β PoC de CVE-2026-17106
> Proof of Concept educativa de **CopyEscape** (CVE-2026-17106), vulnerabilidad de carrera TOCTOU en `docker cp` que permite a un contenedor malicioso escribir archivos arbitrarios en el host Docker. Inspirada en el trabajo original de **masasron**, quien descubrio y documento la vulnerabilidad en el blog *"CopyEscape: Taking Over Docker Hosts with docker cp"*.
---
## Tabla de contenidos
1. [Resumen ejecutivo](#resumen-ejecutivo)
2. [Aviso legal](#aviso-legal)
3. [Arquitectura de la cadena](#arquitectura-de-la-cadena)
4. [Requisitos](#requisitos)
5. [Estructura del repositorio](#estructura-del-repositorio)
6. [Guia rapida de uso](#guia-rapida-de-uso)
7. [Verificacion de la explotacion](#verificacion-de-la-explotacion)
8. [Comparativa con el original](#comparativa-con-el-original)
9. [Mitigacion](#mitigacion)
10. [Creditos](#creditos)
11. [Licencia](#licencia)
---
## Resumen ejecutivo
**CVE-2026-17106** es una vulnerabilidad de condicion de carrera (TOCTOU) en la extraccion del archivo tar que el daemon de Docker produce durante `docker cp : `. Afecta a Docker CLI **29.6.1** y versiones anteriores; parcheada a partir de la **29.7.0**. El bug reside en la **CLI** (extraccion del tar en el host), no en el daemon.
La cadena bautizada **CopyEscape** demuestra como, partiendo unicamente de un contenedor en ejecucion sin capacidades extra, un atacante puede:
1. Camuflar un directorio real `/watched/file.txt/` como un archivo regular ante cualquier proceso que lo abra dentro del contenedor, mediante una biblioteca `LD_PRELOAD` que intercepta `open`, `openat`, `openat2`, `fopen`, `stat`, `lstat`, `fstatat`, `statx` y las variantes `64`.
2. Mantener un archivo cebo `aaa.txt` de 16 MiB que fuerza al daemon a tardar lo suficiente en producir el tar para que el monitor gane la carrera.
3. Vigilar con `inotify` los accesos del daemon y, en la ventana de carrera, sustituir un subdirectorio `escape/` por un symlink absoluto al target del host (`/Users/` en macOS o `/usr/bin` en Linux) mediante dos `rename(2)` atomicos con rollback.
4. El tar resultante contiene el symlink `escape` seguido de una entrada hija `escape/`; la CLI vulnerable crea el symlink en el cliente y extrae la entrada hija *a traves* de el, escribiendo fuera del destino elegido por el usuario.
El resultado es **escritura arbitraria de archivos en el host** con los permisos del proceso que ejecuta `docker cp`. La variante Linux sobrescribe `/usr/bin/runc` y consigue **ejecucion de codigo como root** en el host al siguiente `docker run` / `exec`.
---
## Aviso legal
Este repositorio contiene material estrictamente **educativo y defensivo**.
- **Proposito:** demostrar la cadena de explotacion con fines de formacion, auditoria propia y validacion de *patches*.
- **Uso autorizado:** ejecutalo **unicamente** contra sistemas que te pertenezcan o sobre los que dispongas de autorizacion escrita explicita.
- **Prohibido:** cualquier uso contra sistemas de terceros sin consentimiento. El autor del repositorio no se responsabiliza del uso indebido del material aqui incluido.
- **Responsabilidad:** el operador es el unico responsable del cumplimiento de la legislacion aplicable (LOPD/RGPD en Espana, CFAA en EE. UU., etc.).
- La variante Linux **destruye** el `runc` del host: usala unicamente en una VM desechable con copia de seguridad verificada.
La PoC se entrega con una variante macOS inocua (escribe `~/pwnd` como marcador) cuya unica finalidad es servir como demostracion reproducible.
---
## Arquitectura de la cadena
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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/ (payload: runc o pwnd) β
β βββ .swap-escape (symlink β /Users/ 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/ (symlink absoluto) β
β escape/ (entrada hija que atraviesa el symlink) β
ββββββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 6. CLI vulnerable extrae symlink + hija β
β crea escape β /Users/ en el destino local β
β extrae escape/ A TRAVES del symlink β
β escribe /Users// en el host β
ββββββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β macOS: ~/pwnd con "COPYESCAPE_MACOS_DEMO" (demo inocua) β
β Linux: /usr/bin/runc sobrescrito (RCE como root) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
---
## Requisitos
La PoC esta construida para ejecutarse en **macOS** con:
- **Docker Desktop** 4.81+ en marcha (el daemon puede ser cualquier version; el bug esta en la CLI).
- `curl`, `tar`, `git` y `docker` en `PATH`.
- Conexion a internet para descargar la CLI vulnerable 29.6.1 (unos 18 MB) y la imagen base `alpine:3.21`.
- `HOME` con la forma `/Users/`.
No se necesita root para la variante macOS. La variante Linux requiere root y una VM desechable.
El runner `run-poc.sh` descarga automaticamente el binario de la CLI vulnerable 29.6.1 desde el sitio oficial de Docker (`download.docker.com`). No modifica la instalacion de Docker Desktop del sistema: usa el daemon actual pero invoca el binario descargado para la operacion `docker cp`.
---
## Estructura del repositorio
```
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
```
> El directorio `cli-vulnerable/` (CLI 29.6.1 descargada) se excluye del
> repositorio via `.gitignore`. El runner la descarga automaticamente si
> no esta presente.
---
## Guia rapida de uso
### 1. Ejecutar la PoC (variante macOS, inocua)
```console
cd poc-v2
./run-poc.sh
```
El runner:
1. Descarga la CLI vulnerable 29.6.1 si no esta presente.
2. Construye la imagen Docker `copyescape-macos-pocv2:local`.
3. Arranca el contenedor preparado con `COPYESCAPE_HOST_HOME=$HOME`.
4. Espera a que `/watched/file.txt` sea legible dentro del contenedor.
5. Ejecuta `docker cp` con la CLI vulnerable.
6. Verifica la creacion del marcador `~/pwnd`.
### 2. Opciones del runner
```console
./run-poc.sh 10 # 10 intentos (la carrera es probabilistica)
./run-poc.sh --cleanup # 5 intentos y borra la imagen al final
```
### 3. Salida esperada
```text
[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//pwnd
Contenido del marcador: COPYESCAPE_MACOS_DEMO
[run-poc] Logs del monitor:
[monitor] plataforma=macos target_dir=/Users/ target_file=/Users//pwnd quiet_ms=400 trigger=aaa.txt
[monitor] pivoted /watched/file.txt/escape -> /Users/
[run-poc] Resumen: 1 exito en 1 intento(s).
```
### 4. Limpieza
```console
rm -f -- "$HOME/pwnd"
rm -rf -- poc-v2/file.txt
docker image rm copyescape-macos-pocv2:local
```
---
## Verificacion de la explotacion
Una vez completada la cadena, los siguientes elementos sirven de evidencia:
| Recurso | Evidencia esperada |
| --- | --- |
| `~/pwnd` | Archivo con contenido `COPYESCAPE_MACOS_DEMO\n` |
| `poc-v2/file.txt/` | Directorio (no archivo) con `escape -> /Users/` (symlink) y `aaa.txt` (16 MiB) |
| `docker logs ` | Linea `pivoted /watched/file.txt/escape -> /Users/` |
| CLI 29.7.2 (`docker cp` con CLI parcheada) | El marcador **no** se crea; la PoC falla |
---
## Comparativa con el original
### Que queda del original
La **idea y la estrategia** son de masasron: la carrera TOCTOU contra el productor de tar, el camuflaje via `LD_PRELOAD`, el cebo de 16 MiB, el pivot con dos `rename(2)`, y la estructura `escape/` + `.swap-escape`. Sin ese diseno, esta reescritura no existiria. El esquema conceptual es identico.
### Que se ha reescrito
| Fichero | Lineas original | Lineas nuevo | Cambio |
| --- | --- | --- | --- |
| `watched_preload.c` | 89 | 257 | Reescrito: wrappers `openat2`, `fstatat`, `statx`, `stat64`, `lstat64`, `open64`, `fopen64`; constructor thread-safe; guard anti `dlsym == NULL`; `O_TMPFILE` |
| `monitor.c` (macos + linux) | 259 + 233 = 492 (duplicados) | 344 (unificado) | Reescrito y unificado con `-DCOPYESCAPE_TARGET_*`; rollback en `try_pivot`; `exit_quiet_ms` y trigger configurables; deteccion robusta |
| `entrypoint.sh` | 3 | 5 | `env -u LD_PRELOAD` en lugar de `unset` |
| `Dockerfile` | 22 | 43 | Unificado; digest pin; `--platform`; sin `-Werror`; `-static`; `LABEL` |
| `demo-macos.sh` β `run-poc.sh` | 88 | 166 | Reescrito: reintentos de carrera; logs en fallo; `--cleanup`; validacion delegada al binario |
Aproximadamente un **15-20 %** son lineas que coinciden literalmente (utilidades cortas y constantes del layout). El resto es codigo nuevo.
---
## Mitigacion
- **Actualizar la CLI de Docker a 29.7.0 o superior.** La PoC deja de funcionar (verificado con 29.7.2).
- No ejecutar `docker cp` desde un contenedor en el que no se confia, y nunca como root en Linux.
- Deteccion simple: vigilar la creacion de symlinks absolutos dentro del destino de `docker cp` durante la extraccion, o bloquear en el daemon los tars que contienen entradas hijas bajo un symlink.
- La raiz del bug es la misma clase de fallo que TarSlip / ZipSlip: la CLI no valida que las entradas siguientes no escapen via un symlink ya creado.
---
## Creditos
- Investigacion original y divulgacion: **masasron** β
- PoC y empaquetado de este repositorio: trabajo propio con fines educativos, reescrito desde cero sobre la idea original.
---
## Licencia
Distribuido bajo licencia MIT. Consulta el archivo `LICENSE` para mas detalles.