В этой статье тебя ждут: низкоуровневая эксплуатация веб-сервера со срывом стека и генерацией шелл-кода на лету с помощью древней магии pwntools; атака Padding Oracle на питоновское приложение для вскрытия шифртекста AES-CBC, а также реверс-инжиниринг исполняемого файла с атрибутом SUID для повышения привилегий в системе до локального суперпользователя.
Все это мы проделаем на пути к root-флагу виртуалки Smasher (уровень сложности Insane — 7,6 балла из 10) с CTF-площадки Hack The Box. Поскольку речь в основном пойдет о срыве стека, это будет отличным завершением для нашего цикла «В королевстве PWN».
В королевстве PWN
В этом цикле статей мы изучаем разные аспекты атак типа «переполнение стека». Читай также:
- «Препарируем классику переполнения буфера в современных условиях»
- «Обходим DEP и брутфорсим ASLR на виртуалке с Hack The Box»
- «ROP-цепочки и атака Return-to-PLT в CTF Bitterman»
Разведка
Сканирование портов
Я продолжаю извращаться с методами обнаружения открытых портов, и в этот раз будем пользоваться связкой из Masscan и Nmap. Masscan, к слову, на сегодняшний день самый быстрый из асинхронных сканеров портов. Ко всему прочему он опирается на собственное видение стека TCP/IP и, по словам разработчика, может просканировать весь интернет за шесть минут с одного хоста.
Первой командой я инициирую сканирование всего диапазона портов (в том числе UDP) IP-адреса, по которому живет Smasher, и перенаправляю результат в текстовый файл.

Клонирование /home/www с помощью wget
Три файла представляют для нас интерес: Makefile, tiny и tiny.c.

Листинг локальной копии /home/www
В Makefile содержатся инструкции для сборки исполняемого файла.
CC = c99
CFLAGS = -Wall -O2
## LIB = -lpthread
all: tiny
tiny: tiny.c
$(CC) $(CFLAGS) -g -fno-stack-protector -z execstack -o tiny tiny.c $(LIB)
clean:
rm -f *.o tiny *~
Флаги -g -fno-stack-protector -z execstack намекают нам на предполагаемый «по сюжету» вектор атаки — срыв стека, который, надеюсь, уже успел тебе полюбиться.
Файл tiny — сам бинарник, который развернут на Smasher.

Сводка безопасности исполняемого файла tiny (checksec)
У нас есть исполняемый стек, сегменты с возможностью записи и исполнения произвольных данных и активный механизм FORTIFY — последний, правда, ни на что не повлияет в нашей ситуации (подробнее о нем можно прочесть в первой части цикла, где мы разбирали вывод checksec). Плюс нужно помнить, что на целевом хосте, скорее всего, активен механизм рандомизации адресного пространства ASLR.
Прежде чем перейти непосредственно к сплоитингу, посмотрим, изменил ли как-нибудь автор машины исходный код tiny.c (сам файл я положу к себе на гитхаб, чтобы не загромождать тело статьи).
Изменения в исходном коде tiny.c
Если нужно построчно сравнить текстовые файлы, я предпочитаю расширение DiffTabs для Sublime Text, где — в отличие от дефолтного diff — есть подсветка синтаксиса. Однако, если ты привык работать исключительно из командной строки, colordiff станет удобной альтернативой.
Выдернем последнюю версию tiny.c с гитхаба (будем звать ее tiny-github.c) и сравним с тем исходником, который мы захватили на Smasher.
166c166
< sprintf(buf, "HTTP/1.1 200 OKrn%s%s%s%s%s",
---
> sprintf(buf, "HTTP/1.1 200 OKrnServer: shenfeng tiny-web-serverrn%s%s%s%s%s",
233a234,236
> int reuse = 1;
> if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) < 0)
> perror("setsockopt(SO_REUSEADDR) failed");
234a238,239
> if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuse, sizeof(reuse)) < 0)
> perror("setsockopt(SO_REUSEPORT) failed");
309c314
< sprintf(buf, "HTTP/1.1 %d %srn", status, msg);
---
> sprintf(buf, "HTTP/1.1 %d %srnServer: shenfeng tiny-web-serverrn", status, msg);
320c325
< sprintf(buf, "HTTP/1.1 206 Partialrn");
---
> sprintf(buf, "HTTP/1.1 206 PartialrnServer: shenfeng tiny-web-serverrn");
346c351,355
< void process(int fd, struct sockaddr_in *clientaddr){
---
> int process(int fd, struct sockaddr_in *clientaddr){
> int pid = fork();
> if(pid==0){
> if(fd < 0)
> return 1;
377a387,389
> return 1;
> }
> return 0;
407a420
> int copy_listen_fd = listenfd;
417,420c430
<
< for(int i = 0; i < 10; i++) {
< int pid = fork();
< if (pid == 0) { // child
---
> signal(SIGCHLD, SIG_IGN);
421a432
>
423c434,437
< process(connfd, &clientaddr);
---
> if(connfd > -1) {
> int res = process(connfd, &clientaddr);
> if(res == 1)
> exit(0);
424a439,440
> }
>
426,437d441
< } else if (pid > 0) { // parent
< printf("child pid is %dn", pid);
< } else {
< perror("fork");
< }
< }
<
< while(1){
< connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);
< process(connfd, &clientaddr);
< close(connfd);
< }
438a443
>
Незначительные изменения:
- добавлена обработка ошибок (
233a234,234a238); - в строчках баннеров веб-сервера появилось имя разработчика, что облегчает атакующему идентификацию ПО на этапе сканирования хоста (
166c166,320c325).
Важные изменения: модифицирована логика обработки запросов клиента (все, что касается функции process и создания форков). Если в tiny-github.c реализована многопоточность с помощью концепции PreFork, когда мастер-процесс спавнит дочерние в цикле от 0 до 9, то в tiny.c родитель форкается только один раз — и уже не в теле main, а в самой функции process. Полагаю, это было сделано, чтобы ослабить нагрузку на сервер — ведь ВМ атакует множество людей одновременно. Ну а нам это только на руку, потому что дебажить многопоточные приложения — то еще удовольствие.
Найти уязвимую строку
На одной из моих вузовских практик преподаватель поставил такую задачу: без доступа в Сеть с точностью до строки найти в исходном коде пакета OpenSSL место, ответственное за нашумевшую уязвимость Heartbleed (CVE-2014-0160). Разумеется, в большинстве случаев нельзя однозначно обвинить во всех бедах одну-единственную строку, но всегда можно (и нужно) выделить для себя место в коде, от которого ты будешь отталкиваться при атаке.
Найдем такую строку в tiny.c. В формате статьи трудно анализировать исходные коды без нагромождения повторяющейся информации — поэтому я представлю анализ в виде цепочки «прыжков» по функциям (начиная от main и заканчивая уязвимостью), а ты потом сам проследишь этот путь в своем редакторе.
main() { int res = process(connfd, &clientaddr); } ==> process() { parse_request(fd, &req); } ==> parse_request() { url_decode(filename, req->filename, MAXLINE); }
Функция url_decode принимает три аргумента: два массива строк (источник — filename и назначение — req->filename) и количество копируемых байтов из первого массива во второй. В нашем случае это константа MAXLINE, равная 1024.
void url_decode(char* src, char* dest, int max) {
char *p = src;
char code[3] = { 0 };
while(*p && --max) {
if(*p == '%') {
memcpy(code, ++p, 2);
*dest++ = (char)strtoul(code, NULL, 16);
p += 2;
} else {
*dest++ = *p++;
}
}
*dest = ' ';
}
Алгоритм функции тривиален. Клиент запрашивает у сервера файл. Если строка с именем этого файла содержит данные в Percent-encoding (их можно определить по символу %), функция выполняет декодирование и помещает соответствующий байт в массив назначения. В противном случае происходит простое побайтовое копирование. Однако проблема в том, что локальный массив filename имеет размер MAXLINE (то есть 1024 байт), а вот поле req->filename структуры http_request располагает лишь 512 байтами.
typedef struct {
char filename[512];
off_t offset; /* for support Range */
size_t end;
} http_request;
Налицо классический Out-of-bounds Write (CWE-787: запись за пределы доступной памяти) — он и делает возможным срыв стека.
В эпилоге мы посмотрим на анализ трассировки этого кода, а пока подумаем, как можно использовать уязвимое место tiny.c.
Разработка эксплоита
Сперва насладимся моментом, когда сервер tiny крашится. Так как с ошибкой сегментации упадет дочерний процесс программы, привычного алерта Segmentation fault в окне терминала мы не увидим. Чтобы убедиться, что процесс отработал некорректно и завершился сегфолтом, я открою журнал сообщений ядра dmesg (с флагом -w) и запрошу у сервера (несуществующий) файл с именем из тысячи букв A.

Подтверждение ошибки сегментации процесса tiny
Класс: видим, что запрос выбивает child-процесс c general protection fault (или segmentation fault в нашем случае).
Поиск точки перезаписи RIP
Запустим исполняемый файл сервера в отладчике GDB.
Теперь важный момент: я не могу пользоваться циклическим паттерном де Брёйна, который предлагает PEDA, ведь он содержит символы '%' — а они, если помнишь, трактуются сервером как начало URL-кодировки.

Циклическая последовательность, сгенерированная при помощи GDB PEDA
Значит, нам нужен другой генератор. Можно пользоваться msf-pattern_create -l <N> и msf-pattern_offset -q <0xFFFF>, чтобы создать последовательность нужной длины и найти смещение. Однако я предпочитаю модуль pwntools, который работает в разы быстрее.

Циклические последовательности, сгенерированные при помощи MSF и pwntools
Как мы видим, ни один из инструментов не использует «плохие» символы, поэтому для генерации вредоносного URL можно юзать любой из них.
Мы отправили запрос на открытие несуществующей страницы при помощи curl — а теперь смотрим, какое значение осело в регистре RSP, и рассчитываем величину смещения до RIP.
Ответ: 568.
После выхода из отладчика хорошо бы принудительно убить все инстансы веб-сервера — ведь однозначно завершился только child-процесс.