
PoC educacional e análise da vulnerabilidade CVE-2021-4034 (PwnKit) de escalada de privilégios local no pkexec do polkit, com um laboratório baseado em Docker para prática hands-on de exploração e defesa.
🔗 Projeto original: berdav/CVE-2021-4034
Este projeto é uma versão analisada e modificada com base no original para fins educacionais.
Licença MIT | Tarefa educacional da White Hat School
CVE-2021-4034 é uma vulnerabilidade de escalação de privilégio local no policykit-1 (PolicyKit) do Linux. Um usuário comum pode executar pkexec sem argumentos, explorar uma falha na estrutura da memória do processo e, com isso, fazer com que o glib referencie novamente strings de variáveis de ambiente que deveriam ter sido filtradas, carregando um arquivo .so malicioso e, assim, obter privilégios de root.
⚠️ Apenas para fins educacionais: Este código deve ser usado apenas em sistemas modificados.
Usá-lo para atacar sistemas reais pode acarretar responsabilidade legal.
| Item | Conteúdo |
|---|---|
| ID CVE | CVE-2021-4034 |
| Nome da vulnerabilidade | PwnKit |
| Versões afetadas | Versões do polkit anteriores a 0.105 sem o patch aplicado (ambiente de teste: policykit-1 0.105-26ubuntu1 do Ubuntu 20.04) |
| Tipo de vulnerabilidade | Escalação de Privilégio Local (LPE) |
| Gravidade | Crítica (CVSS 7.8) |
| Versão corrigida | policykit-1 >= 0.105-26ubuntu1.1 |
| Data de descoberta | Junho de 2021 (divulgação em janeiro de 2022) |
É fácil interpretar erroneamente como um problema apenas do Ubuntu, mas como é uma falha de lógica no próprio pkexec, a maioria das distribuições que usam polkit é afetada. Como o ambiente de teste Docker é Ubuntu 20.04, incluí essa versão na tabela acima.
Quando analisei pela primeira vez, pensei que era um 'problema causado pela falta de validação de variáveis de ambiente', mas ao examinar o código-fonte e o commit do patch, percebi que a ordem era um pouco diferente. A verdadeira causa é outra, e o problema das variáveis de ambiente é mais uma consequência dessa causa. Abaixo está o fluxo organizado na ordem das causas.
pkexec é um programa SUID-root que solicita escalação de privilégios através do PolicyKit.
# Exemplo: executar um comando com privilégios root
pkexec /bin/id
pkexec systemctl restart service
É usado quando um usuário comum precisa realizar tarefas específicas com privilégios administrativos.
argc == 0A função main() do pkexec, na parte de processamento dos argumentos da linha de comando, não valida o caso em que é executado sem nenhum argumento (argc == 0). Este é o verdadeiro ponto de partida desta vulnerabilidade.
argv = {"pkexec", "comando", NULL} → argc >= 1execve("/usr/bin/pkexec", {NULL}, env) → argc == 0Se argc é 0, a lista argv contém apenas um NULL indicando término. No entanto, a lógica interna do pkexec tenta ler e escrever em argv[1], que não existe, mesmo nessa situação. O problema é que, quando o Linux executa um processo, ele coloca o array argv e o array envp (variáveis de ambiente) lado a lado na memória. Então, o argv[1] fora dos limites acaba apontando para envp[0], ou seja, para a primeira variável de ambiente.
Situação normal: argv = [ "pkexec" | NULL ]
Situação de ataque: argv = [ NULL ] ← argc = 0
↑
Acessa argv[1] inexistente
↓
Lê e escreve em envp[0] logo após na memória (out-of-bounds)
Por que isso é perigoso:
GCONV_PATH e LD_PRELOAD, considerando-as inseguras.argc < 1. (Corresponde a CWE-125 leitura fora dos limites, CWE-787 escrita fora dos limites)📌 Em resumo: a falta de validação de variáveis de ambiente é a 'condição para o ataque funcionar', e a verdadeira causa raiz é que o pkexec não tratou o caso argc == 0. O item 3 abaixo é a consequência dessa causa.
Graças ao comportamento OOB descrito no item 2, essa string acaba sendo reutilizada sem validação durante o processo de inicialização do glib pelo pkexec.
// CVE-2021-4034_exploit.c
char * const env[] = {
"GCONV_PATH=.", // 원래는 ld.so가 걸러냈어야 함
"CHARSET=PWNKIT", // 존재하지 않는 인코딩
};
execve("/usr/bin/pkexec", args, env); // argv는 비워서 argc=0을 만듦
Problema:
O importante aqui é que o glib não fez nada de errado. Se GCONV_PATH está definido, procurar um conversor nesse caminho é um comportamento normal do glib. O problema é que o pkexec já quebrou o estado de execução segura (estado onde as variáveis de ambiente perigosas foram removidas) — o glib apenas agiu normalmente, mas esse comportamento normal acaba sendo explorado.
Verificação da variável de ambiente CHARSET
CHARSET=PWNKIT
Pesquisa pela definição do conversor no arquivo gconv-modules
module UTF-8// PWNKIT// pwnkit 1
Carregamento do arquivo .so a partir de GCONV_PATH
GCONV_PATH=. → pesquisa por pwnkit.so no diretório atual
Execução automática da função de inicialização do arquivo .so
// pwnkit.c - .so 파일 로드 시 자동으로 실행됨
void gconv_init(void *step)
{
setuid(0); // root 권한 획득
setgid(0);
execve("/bin/sh"); // root shell 실행!
}
É fácil chamar gconv_init de 'função construtora', mas estritamente falando, é diferente do __attribute__((constructor)) do C. Exatamente, é uma função de inicialização definida na interface do módulo gconv, e o glib a chama explicitamente depois de carregar o .so com dlopen.
┌─────────────────────────────────────┐
│ Usuário comum (uid=1000) │
└─────────────────────────────────────┘
│
│ 1. Executa pkexec com argv vazio (argc=0)
│ + Define variáveis de ambiente maliciosas
│ GCONV_PATH=. / CHARSET=PWNKIT
↓
┌─────────────────────────────────────┐
│ Execução do pkexec │
│ Sem validação de argc → OOB → │
│ Re-referência da string │
└─────────────────────────────────────┘
│
│ 2. glib processa conforme comportamento normal
│ Pesquisa pela codificação CHARSET=PWNKIT
│ Encontra conversor em GCONV_PATH=.
↓
┌─────────────────────────────────────┐
│ Carregamento do pwnkit.so │
│ (arquivo .so malicioso no diretório │
│ atual) │
└─────────────────────────────────────┘
│
│ 3. Execução automática da função de inicialização
│ do gconv (privilégios root!)
↓
┌─────────────────────────────────────┐
│ Obtenção do shell root ✅ │
│ uid=0(root) gid=0(root) │
└─────────────────────────────────────┘