GoAhead — это популярный встраиваемый веб-сервер компании Embedthis, который используется в продуктах Oracle, IBM, HP и других известных производителей. Я думаю, не нужно говорить, какие перспективы открывает возможность исполнять произвольный код на сотнях тысяч устройств, где они работают. Давай разберемся, как это делается.

Баг обнаружил австралийский исследователь Дэниел Ходсон (Daniel Hodson) из компании Elttam. Причина уязвимости — в некорректной обработке HTTP-запросов при включенной поддержке интерфейса CGI. При инициализации окружения некоторых CGI-скриптов существует возможность использования специальных переменных окружения типа LD_PRELOAD, с помощью которых можно выполнить код.

INFO

Уязвимость получила идентификатор CVE-2017-17562.

 

Готовим стенд

Для начала определимся с версией самого веб-сервера. Уязвимы все версии приложения ниже 3.6.5. Так что будем использовать самую последнюю до патча — 3.6.4. В качестве системы я буду использовать Debian 9 в докер-контейнере.

docker run --rm -ti --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --name=goahead --hostname=goahead -p80:80 debian /bin/bash
apt-get update && apt-get install -y build-essential git file

Теперь ставим веб-сервер. Будем собирать из исходников, взятых в гит-репозитории.

git clone --branch=v3.6.4 --depth=1 https://github.com/embedthis/goahead.git
cd goahead
DEBUG="debug" make

Теперь скомпилим тестовое CGI-приложение.

cd test
gcc cgitest.c -o cgi-bin/cgitest

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

../build/linux-x64-default/bin/goahead

Скомпилированное приложение будет доступно на запущенном сервере по ссылке /cgi-bin/cgitest.

Тестовое CGI-приложение на сервере GoAhead 6.3.4
Тестовое CGI-приложение на сервере GoAhead 6.3.4

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

 

Особенности компиляции и линкования

Для начала посмотрим, как устроен бинарник goahead. Для этого можно воспользоваться утилитой readelf или file.

readelf -hl goahead
Детальная информация о бинарнике goahead
Детальная информация о бинарнике goahead

Обрати внимание на секцию INTERP. Она говорит нам о том, что ELF-файл был скомпилирован с динамической линковкой библиотек, а в качестве интерпретатора для ее выполнения используется библиотека ld-linux-x86-64.so.2.

root@goahead:~/goahead/build/linux-x64-default/bin# file goahead
goahead: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=c29d5dc68c84bafc997dd15d4e22c3dab634afae, not stripped

Эта библиотека — динамический компоновщик, который выполняется в первую очередь при запуске исполняемого файла и отвечает за линковку и загрузку разделяемых библиотек (so) и символов. Чтобы узнать, какие библиотеки подгружаются при старте веб-сервера, можно воспользоваться утилитой ldd или запустить goahead, установив переменную окружения LD_TRACE_LOADED_OBJECTS в 1.

Информация о подгружаемых библиотеках при запуске веб-сервера GoAhead
Информация о подгружаемых библиотеках при запуске веб-сервера GoAhead

Как ты, наверное, знаешь, переменных окружения великое множество, но среди них есть специальные, которые могут изменить дефолтное поведение ступеней загрузки исполняемого файла. Подробнее о них ты можешь прочитать в мануале к файлу ld.so.

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

Первая остановка — это функция dl_main, которая выполняется сразу после запуска линкера.

/glibc/master/elf/rtld.c
733: static void
734: dl_main (const ElfW(Phdr) *phdr,
735:     ElfW(Word) phnum,
736:     ElfW(Addr) *user_entry,
737:     ElfW(auxv_t) *auxv)
738: {
...
772:   /* Process the environment variable which control the behaviour.  */
773:   process_envvars (&mode);

Здесь вызывается функция process_envvars. Да, она делает то, что и следует из ее названия, — парсит и обрабатывает переменные окружения.

/glibc/master/elf/rtld.c
2341: static void
2342: process_envvars (enum mode *modep)
2343: {
2344:   char **runp = _environ;
2345:   char *envline;
2346:   enum mode mode = normal;
2347:   char *debug_output = NULL;
...
2353:   while ((envline = _dl_next_ld_env_entry (&runp)) != NULL)
2354:     {
2355:       size_t len = 0;
2356: 
2357:       while (envline[len] != '\0' && envline[len] != '=')
2358:   ++len;

Перебирается массив, состоящий из переменных окружения. Для каждого элемента вычисляется его размер, а дальше функция switch, в зависимости от длины параметра, передает управление на различные ветки кода, где выясняется, не передана ли одна из специальных переменных. Нас больше всего интересует длина 7, где расположилась проверка на LD_PRELOAD в переданной командной строке.

/glibc/master/elf/rtld.c
2366:       switch (len)
2367:   {
...
2385:   case 7:
...
2393:     /* List of objects to be preloaded.  */
2394:     if (memcmp (envline, "PRELOAD", 7) == 0)
2395:       {
2396:         preloadlist = &envline[8];
2397:         break;
2398:       }

Если эта переменная окружения передана, то инициализируется preloadlist. По сути, в ней хранится список библиотек, которые нужно загрузить перед тем, как передавать управление главному коду из бинарника. Поэтому вскоре после того, как отработала process_envvars, выполнение переходит к той части кода, что начинает обработку preloadlist.

/glibc/master/elf/rtld.c
1478:   assert (*first_preload == NULL);
1479:   struct link_map **preloads = NULL;
1480:   unsigned int npreloads = 0;
1481: 
1482:   if (__glibc_unlikely (preloadlist != NULL))
1483:     {

Если список непустой, то функция do_preload выполняет загрузку каждой переданной библиотеки.

/glibc/master/elf/rtld.c
1495:       while ((p = (strsep) (&list, " :")) != NULL)
1496:   if (p[0] != '\0'
1497:       && (__builtin_expect (! __libc_enable_secure, 1)
1498:       || strchr (p, '/') == NULL))
1499:     npreloads += do_preload (p, main_map, "LD_PRELOAD");

Теперь мы знаем, что, если указать в переменной LD_PRELOAD путь до библиотеки, линкер выполнит ее загрузку перед исполнением самого бинарника. Однако нам этого недостаточно, ведь задача не в том, чтобы просто подцепить либу, требуется еще и выполнить нужный нам код. В этом нам помогут секции .init и .fini. Если функция помещена в секцию .init, то система выполняет ее до главной (main), а если в .fini, то после того, как main отработает и вернет результат. Своеобразная имплементация конструкторов и деструкторов классов, только глобальная.

Теперь воспользуемся атрибутами функций. Нужный нам называется constructor.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


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

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

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии