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

Если тебе захочется вместе со мной заглянуть в исходники веб-сервера, то их ты всегда сможешь найти в официальном репозитории Embedthis.
Особенности компиляции и линкования
Для начала посмотрим, как устроен бинарник goahead
. Для этого можно воспользоваться утилитой readelf
или file
.
readelf -hl 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
.

Как ты, наверное, знаешь, переменных окружения великое множество, но среди них есть специальные, которые могут изменить дефолтное поведение ступеней загрузки исполняемого файла. Подробнее о них ты можешь прочитать в мануале к файлу 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»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»