
Artigo detalhado e exploit de prova de conceito para CVE-2021-4034 (PolKit pkexec escalada de privilégio local), incluindo um ambiente Docker lab para análise prática e depuração.
[toc]
ID da vulnerabilidade: CVE-2021-4034
Pontuação da vulnerabilidade:
Produto vulnerável: linux PolKit (pkexec)
Escopo de impacto: afeta versões de 2009 até o presente (atualmente 0.105) Referência http://its.dlut.edu.cn/info/1054/78309.htm
Condições de exploração: linux local; pkexec é um arquivo suid com permissão de execução
Obtenção do código-fonte: apt source policykit-1
ou https://launchpad.net/ubuntu/bionic/+package/policykit-1
Ambiente docker: chenaotian/cve-2021-4034
Docker que eu montei, fornece:
pkexec compilado por mim mesmo, depurável com código-fonteTudo está no diretório /root/:

su test para mudar para o usuário test e executarIniciar docker:
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
Testar exploit:
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
O produto onde a vulnerabilidade ocorre é o comando pkexec do polkit. pkexec é semelhante ao sudo, uma ferramenta que permite executar comandos como outro usuário (normalmente root). Pode-se verificar o pacote ao qual pkexec pertence com o comando dpkg:
dpkg -S /usr/bin/pkexec

Em seguida, obter o pacote fonte (também está no meu docker), depois compilar uma versão depurável a partir do pacote fonte para facilitar a depuração.
O princípio de ativação da vulnerabilidade é muito simples
/polkit-0.105/src/programs/pkexec.c : 386 main
int
main (int argc, char *argv[])
{
··· ···
··· ···
/* 这段的意思就是,循环遍历用户输入参数,根据输入的不同参数设置值
* 但问题在于,他循环遍历的起点是1,没有考虑用户没有输入任何参数的情况
*/
for (n = 1; n < (guint) argc; n++)
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
··· ···
else //如果是无法识别的参数则跳出循环,这里意味着该参数是想要执行的命令
{
break;
}
}
··· ···
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]); //获取执行命令具体字符串
if (path == NULL)
{
···
}
if (path[0] != '/')
{
/* g_find_program_in_path() is not suspectible to attacks via the environment */
//该函数会根据PATH环境变量寻找要执行命令的绝对地址
s = g_find_program_in_path (path);
if (s == NULL)
{
···
}
g_free (path);
argv[n] = path = s;//把获取到的绝对地址修改回命令行参数
}
··· ···
··· ···
Análise dos meus comentários no código:
pkexec).-- for encontrado, ele é considerado o comando a ser executado, e o loop é interrompido para prosseguir com a lógica seguinte.g_find_program_in_path para buscar o caminho absoluto do comando. Esta função usa a variável de ambiente PATH para localizar o caminho absoluto do argumento (comando). Por exemplo, se cat for passado, retorna /bin/cat.Ainda é fácil de entender, mas o problema é:
argv[] e as variáveis de ambiente environ[] são colocados no final da pilha, e argv[] e environ[] são contíguos. O último item de argv[] é null.pkexec for iniciado pela linha de comando sem nenhum argumento adicional, argv[0] será "pkexec" e argv[1] será \x00, sem problemas. Mas se for iniciado pela função execve sem argumentos adicionais, argv[0] será \x00 e argv[1] apontará para a variável de ambiente! Ao ler argv[1], será feita uma leitura fora dos limites de environ[0].Quando iniciado diretamente pela linha de comando, argc é 1 e argv[0] é o caminho do pkexec:

Iniciando pkexec com a função execve, argc é 0:

O que isso causa? Quando iniciado com execve sem argumentos adicionais, o comprimento de argv[] é 0, então argv[1] é environ[0]. Assim, a lógica analisada acima se torna: obter o valor da primeira variável de ambiente e procurar seu caminho absoluto na variável PATH. Se encontrado, escrever de volta na primeira variável de ambiente. Então o método de exploração é o seguinte:
Primeiramente, é importante deixar claro que pkexec é um arquivo privilegiado (suid):

Como usar variáveis de ambiente para fazer algo em um arquivo privilegiado? Primeiro, um pequeno detalhe:
O vinculador dinâmico do linux ld-linux-x86-64.so.2 limpa variáveis de ambiente sensíveis durante a execução de programas privilegiados:
glibc-2.27/elf/dl-support.c : 307
void
_dl_non_dynamic_init (void)
{
··· ···
··· ···
if (__libc_enable_secure) //特权模式的情况下
{
static const char unsecure_envvars[] =
UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
EXTRA_UNSECURE_ENVVARS
#endif
;
const char *cp = unsecure_envvars;
//循环将危险环境变量列表中的环境变量全部清空(unset)
while (cp < unsecure_envvars + sizeof (unsecure_envvars))
{
__unsetenv (cp);
cp = (const char *) __rawmemchr (cp, '\0') + 1;
}
#if !HAVE_TUNABLES
if (__access ("/etc/suid-debug", F_OK) != 0)
__unsetenv ("MALLOC_CHECK_");
#endif
}
··· ···
··· ···
}
A lista de variáveis de ambiente perigosas UNSECURE_ENVVARS é definida como:
glibc-2.27/sysdeps/generic/unsecvars.h : 10
#define GLIBC_TUNABLES_ENVVAR "GLIBC_TUNABLES\0"
#define UNSECURE_ENVVARS \
"GCONV_PATH\0" \
"GETCONF_DIR\0" \
GLIBC_TUNABLES_ENVVAR \
"HOSTALIASES\0" \
"LD_AUDIT\0" \
"LD_DEBUG\0" \
"LD_DEBUG_OUTPUT\0" \
"LD_DYNAMIC_WEAK\0" \
"LD_HWCAP_MASK\0" \
"LD_LIBRARY_PATH\0" \
"LD_ORIGIN_PATH\0" \
"LD_PRELOAD\0" \
"LD_PROFILE\0" \
"LD_SHOW_AUXV\0" \
"LD_USE_LOAD_BIAS\0" \
"LOCALDOMAIN\0" \
"LOCPATH\0" \
"MALLOC_TRACE\0" \
"NIS_PATH\0" \
"NLSPATH\0" \
"RESOLV_HOST_CONF\0" \
"RES_OPTIONS\0" \
"TMPDIR\0" \
"TZDIR\0"