Docker Socket Exposure: Finding Host Escape Paths in Scope
Mounting /var/run/docker.sock into a container is the classic "I trust this process with the host" move. From a bounty perspective it is gold when you can reach that socket—and radioactive when you prove it by launching privileged containers on someone else's laptop.
Stop. Scope first. Many programs allow reporting the mount or an open Docker API; almost none want you to spawn containers on shared runners.
How the exposure appears
Common shapes:
- A volume mount of
docker.sockin Compose, Swarm, or Kubernetes - Docker Engine listening on
tcp://0.0.0.0:2375without TLS - A reverse proxy that forwards
/v1.41/(or similar) to the engine - CI helpers, Portainer-like UIs, and "ops sidecars" that wrap the socket
Recon is straightforward. Hit interesting hosts for open 2375/2376, look for Docker-Client errors, and search public configs for docker.sock. Inside a foothold, ls -l /var/run/docker.sock answers the local question in one line.
Public compose files and Helm values are weirdly honest. Engineers paste the socket mount "so the app can restart siblings," then ship the same snippet to staging with a public ingress. Certificate transparency and GitHub code search still surface these faster than port scans alone.
If the API answers unauthenticated GET /version or GET /info, you already have a reportable control-plane leak. Capture engine version, OSType, and whether the daemon thinks it is Swarm-managed. That metadata alone often persuades triage.
Prove risk without becoming the incident
You do not need a reverse shell on the node. Prefer:
- Unauthenticated or weakly authenticated API responses you can replay.
- Evidence the app user can talk to the socket (file permissions, group membership, HTTP proxy).
- A dry explanation of the breakout: create a container, mount host
/, read/etc/shadow—described, not executed on production.
On a dedicated lab or your own VM, rehearse the breakout so your write-up is accurate. On the target, stay at detection. If the program explicitly permits a canary container, use a unique name and a no-network image, then remove it. Ask in the report comments if unsure.
Watch for "safe" wrappers. An HTTP API that only allows docker ps still expands to full engine access if the wrapper is thin. Try methods outside the UI: raw /containers/create, image pull restrictions, and auth bypass on the management port.
TLS on 2376 changes the conversation only if client certificates are required and you do not have one. A listener that accepts cleartext on a sibling port, or a proxy that strips auth, brings you right back to daemon trust.
Writing severity that sticks
Frame impact as host compromise equivalent to the daemon's privileges—often root on the node, plus access to sibling containers and secrets in env files. Mention multi-tenant risk when the host runs other customers' workloads.
Call out secrets that typically sit beside the daemon: .env files, bind-mounted cloud credentials, SSH keys in developer home directories, and CI caches. You do not need to steal them; naming the realistic follow-on paths is enough when the socket access is proven.
Fixes are blunt: do not mount the socket into apps; use the Docker API over TLS with client certs if remote management is required; prefer rootless where feasible; put break-glass tooling on bastions, not in the web app pod.
Docker's own engine security docs are clear that socket access equals daemon trust. Quote that model. Triage argues less when you speak their language.