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

hacking tools

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

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

Еще по теме: Повышение привилегий в Windows

Стенд

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

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

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

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

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

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

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

Уязвимость wget

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

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

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

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

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

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

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

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

/wget-1.19.1/src/http.c

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

/wget-1.19.1/src/http.c

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

/wget-1.19.1/src/http.c

Обратите внимание на строку 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

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

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

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

/wget-1.19.1/src/http.c

эксплуатация уязвимости wget
Значение переменной contlen

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

/wget-1.19.1/src/http.c

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

/wget-1.19.1/src/http.c

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

/wget-1.19.1/src/http.c

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

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

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

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

/wget-1.19.1/src/http.c

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

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

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

/wget-1.19.1/src/http.c

/wget-1.19.1/src/connect.c

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

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

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

/wget-1.19.1/src/connect.c

/wget-1.19.1/src/connect.c

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

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

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

/wget-1.19.1/src/connect.c

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

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

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

/wget-1.19.1/src/http.c

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

/wget-1.19.1/src/http.c

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

эксплуатация уязвимости wget
Стек перезаписан. Wget в отключке

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

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

shellcode.py

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

shellcode.py

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

После запуска можно наблюдать следующую картину:

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

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

Заключение

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

/wget-1.19.2/src/http.c

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

aLLy

ВКонтакте
OK
Telegram
WhatsApp
Viber

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *