
Exploit de prova de conceito baseado em C para CVE-2021-4034 (PwnKit) que permite escalada local de privilégios através de escrita fora dos limites no pkexec. Inclui instruções de compilação e explicação da vulnerabilidade.
CVE-2021-4034
debian@debian:~/PwnKit-Exploit$ make
cc -Wall exploit.c -o exploit
debian@debian:~/PwnKit-Exploit$ whoami
debian
debian@debian:~/PwnKit-Exploit$ ./exploit
Current User before execute exploit
hacker@victim$whoami: debian
Exploit written by @luijait (0x6c75696a616974)
[+] Enjoy your root if exploit was completed succesfully
root@debian:/home/debian/PwnKit-Exploit# whoami
root
root@debian:/home/debian/PwnKit-Exploit#
| Comando | Uso |
|---|---|
sudo chmod 0755 pkexec | Corrige CVE 2021-4034 |
git clone https://github.com/luijait/PwnKit-Exploit
cd PwnKit-Exploit
make
./exploit
whoami
| Comando | Utilidade |
|---|---|
make clean | Limpa a build para testar código modificado |
Baseado no blog.qualys.com
O início da função main() do pkexec processa os argumentos da linha de comando (linhas 534-568) e procura pelo programa a ser executado, se seu caminho não for absoluto, nos diretórios da variável de ambiente PATH (linhas 610-640):
435 main (int argc, char *argv[])
436 {
...
534 for (n = 1; n < (guint) argc; n++)
535 {
...
568 }
...
610 path = g_strdup (argv[n]);
...
629 if (path[0] != '/')
630 {
...
632 s = g_find_program_in_path (path);
...
639 argv[n] = path = s;
640 }
Infelizmente, se o número de argumentos da linha de comando argc for 0 – ou seja, se a lista de argumentos argv que passamos para execve() estiver vazia, por exemplo {NULL} – então argv[0] é NULL. Este é o terminador da lista de argumentos. Portanto:
na linha 534, o inteiro n é permanentemente definido como 1; na linha 610, o ponteiro path é lido fora dos limites a partir de argv[1]; na linha 639, o ponteiro s é escrito fora dos limites em argv[1]. Mas o que exatamente é lido e escrito nesse argv[1] fora dos limites?
Para responder a esta pergunta, devemos fazer uma breve digressão. Quando executamos execve() em um novo programa, o kernel copia nossos argumentos, strings de ambiente e ponteiros (argv e envp) para o final da pilha do novo programa; por exemplo:
|---------+---------+-----+------------|---------+---------+-----+------------|
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------|
V V V V V V
"program" "-option" NULL "value" "PATH=name" NULL
Claramente, como os ponteiros argv e envp são contíguos na memória, se argc for 0, então argv[1] fora dos limites é na verdade envp[0], o ponteiro para nossa primeira variável de ambiente, "value". Consequentemente:
Na linha 610, o caminho do programa a ser executado é lido fora dos limites a partir de argv[1] (ou seja, envp[0]), e aponta para "value"; Na linha 632, esse caminho "value" é passado para g_find_program_in_path() (porque "value" não começa com barra, na linha 629); Então, g_find_program_in_path() procura por um arquivo executável chamado "value" nos diretórios da nossa variável de ambiente PATH; Se tal arquivo executável for encontrado, seu caminho completo é retornado para a função main() do pkexec (na linha 632); Finalmente, na linha 639, esse caminho completo é escrito fora dos limites em argv[1] (ou seja, envp[0]), sobrescrevendo nossa primeira variável de ambiente. Então, dito de forma mais precisa:
Se nossa variável de ambiente PATH for "PATH=name", e se o diretório "name" existir (no diretório de trabalho atual) e contiver um arquivo executável chamado "value", então um ponteiro para a string "name/value" é escrito fora dos limites em envp[0]; OU
Se nosso PATH for "PATH=name=.", e se o diretório "name=." existir e contiver um arquivo executável chamado "value", então um ponteiro para a string "name=./value" é escrito fora dos limites em envp[0]. Em outras palavras, essa escrita fora dos limites nos permite reintroduzir uma variável de ambiente "insegura" (por exemplo, LD_PRELOAD) no ambiente do pkexec. Essas variáveis "inseguras" são normalmente removidas (pelo ld.so) do ambiente de programas SUID antes que a função main() seja chamada. Exploraremos essa primitiva poderosa na próxima seção.
Nota de última hora: o polkit também suporta sistemas operacionais não Linux, como Solaris e *BSD, mas não investigamos a explorabilidade deles. No entanto, notamos que o OpenBSD não é explorável, pois seu kernel se recusa a executar execve() em um programa se argc for 0.