
CVE-2026-17106(CopyEscape)의 기능적 PoC: Docker 호스트에 임의 쓰기를 허용하는 docker cp의 TOCTOU 레이스. Docker 실습 환경 + inotify 모니터 + LD_PRELOAD. 무해한 macOS 변형 및 파괴적인 Linux 변형.
CopyEscape(CVE-2026-17106)에 대한 교육용 개념 증명(PoC)입니다.
docker cp의 TOCTOU 레이스 조건 취약점으로, 악성 컨테이너가 Docker 호스트에 임의의 파일을 쓸 수 있게 합니다. 이 취약점을 발견하고 "CopyEscape: Taking Over Docker Hosts with docker cp" 블로그에 문서화한 masasron의 원래 연구에서 영감을 받았습니다.
CVE-2026-17106은 docker cp <컨테이너>:<경로> <대상> 동안 Docker 데몬이 생성하는 tar 파일을 추출할 때 발생하는 TOCTOU(검사 시점/사용 시점) 레이스 조건 취약점입니다. Docker CLI 29.6.1 및 이전 버전에 영향을 미치며, 29.7.0부터 패치되었습니다. 버그는 데몬이 아닌 CLI(호스트에서 tar 추출)에 있습니다.
CopyEscape라고 명명된 이 공격 체인은 추가 권한이 없는 실행 중인 컨테이너 하나만으로 공격자가 다음을 수행할 수 있음을 보여줍니다:
LD_PRELOAD 라이브러리를 사용해 open, openat, openat2, fopen, stat, lstat, fstatat, statx 및 64 변형을 가로채어, 컨테이너 내부에서 실제 디렉터리 /watched/file.txt/를 여는 모든 프로세스에 일반 파일처럼 보이도록 위장합니다.aaa.txt를 유지하여 데몬이 tar를 생성하는 데 충분한 시간이 걸리게 만들고, 모니터가 레이스에서 이길 수 있게 합니다.inotify로 데몬의 접근을 감시하고, 레이스 윈도우 동안 두 번의 원자적 rename(2)(롤백 포함)로 하위 디렉터리 escape/를 호스트 대상의 절대 심링크(/Users/<사용자>(macOS) 또는 /usr/bin(Linux))로 교체합니다.escape와 그 뒤에 하위 항목 escape/<basename>이 포함됩니다. 취약한 CLI는 클라이언트에 심링크를 만든 다음 해당 심링크를 통해 하위 항목을 추출하여 사용자가 선택한 대상 밖에 파일을 씁니다.결과는 docker cp를 실행하는 프로세스의 권한으로 호스트에 임의의 파일 쓰기입니다. Linux 변형은 /usr/bin/runc를 덮어쓰고 다음 docker run/exec에서 호스트에서 루트 권한 코드 실행을 달성합니다.
이 저장소는 엄격히 교육 및 방어 목적의 자료를 포함합니다.
runc를 파괴합니다. 검증된 백업이 있는 폐기 가능한 VM에서만 사용하십시오.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/<사용자> 형식이어야 합니다.macOS 변형에는 root가 필요하지 않습니다. Linux 변형은 root와 폐기 가능한 VM이 필요합니다.
run-poc.sh 러너는 공식 Docker 사이트(download.docker.com)에서 취약한 CLI 29.6.1 바이너리를 자동으로 다운로드합니다. 시스템의 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를 실행합니다.~/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의 것입니다: tar 생성자를 상대로 한 TOCTOU 레이스, LD_PRELOAD를 통한 위장, 16MiB 미끼, 두 번의 rename(2)를 통한 피벗, escape/ + .swap-escape 구조. 그 설계가 없었다면 이 재작성은 존재하지 않았을 것입니다. 개념적 구성은 동일합니다.
약 15-20% 는 문자 그대로 일치하는 줄입니다(짧은 유틸리티 및 레이아웃 상수). 나머지는 새로운 코드입니다.
docker cp를 실행하지 말고, Linux에서 root로 절대 실행하지 마십시오.docker cp 대상 내에서 추출 중 절대 심링크 생성 여부를 감시하거나, 데몬에서 심링크 아래의 하위 항목이 포함된 tar를 차단하십시오.MIT 라이선스로 배포됩니다. 자세한 내용은 LICENSE 파일을 참조하세요.
| 리소스 | 예상 증거 |
|---|
~/pwnd | COPYESCAPE_MACOS_DEMO\n 내용이 포함된 파일 |
poc-v2/file.txt/ | 디렉터리(파일 아님). escape -> /Users/<사용자>(심링크) 및 aaa.txt(16MiB) 포함 |
docker logs <컨테이너> | pivoted /watched/file.txt/escape -> /Users/<사용자> 줄 |
CLI 29.7.2(패치된 CLI로 docker cp) | 마커는 생성되지 않습니다; PoC가 실패합니다 |
| 파일 | 원본 줄 수 | 새 줄 수 | 변경 사항 |
|---|
watched_preload.c | 89 | 257 | 다시 작성: openat2, fstatat, statx, stat64, lstat64, open64, fopen64 래퍼; 스레드 안전 생성자; dlsym == NULL 방지 가드; O_TMPFILE |
monitor.c (macos + linux) | 259 + 233 = 492 (중복) | 344 (통합) | -DCOPYESCAPE_TARGET_*로 다시 작성 및 통합; try_pivot의 롤백; exit_quiet_ms 및 트리거 설정 가능; 강력한 감지 |
entrypoint.sh | 3 | 5 | unset 대신 env -u LD_PRELOAD |
Dockerfile | 22 | 43 | 통합; digest pin; --platform; -Werror 제거; -static; LABEL |
demo-macos.sh → run-poc.sh | 88 | 166 | 다시 작성: 레이스 재시도; 실패 시 로그; --cleanup; 검증을 바이너리에 위임 |