Эксплуатация уязвимости в PHP-FPM Nginx

Компьютерная безопасность icon

Недавно была обнаружена опасная уязвимость в связке из Nginx и PHP-FPM, которая нередко встречается на веб-серверах. Появившийся эксплоит позволяет изменить настройки PHP путем внедрения переменных окружения, а цепочка таких настроек приведет к удаленному выполнению кода. Давайте разбираться в уязвимости в PHP-FPM Nginx и узнаем где накосячили разработчики PHP.

Еще по теме: Эксплуатация бэкдора PHP

Впервые аномальное поведение было обнаружено Андреем @d90pwn Данау во время квалификации Real World CTF 2019. Сервер странно реагировал на отправленный в URL символ перевода строки (%0a). Этой идеей заинтересовались Омар @beched Ганиев и Эмиль @neex Лернер. Эмиль разобрался, почему так происходит, нашел способ эксплуатации и написал рабочий эксплоит, а Омар довел этот баг до получения RCE.

Суть проблемы сводится к тому, что в некоторых конфигурациях FPM хакер может выполнить атаку типа buffer underflow и осуществить запись в адресное пространство, зарезервированное для данных протокола FastCGI. Это позволит выполнять произвольные команды на удаленной системе.

Уязвимость получила идентификатор CVE-2019-11043 и провокационное для русскоязычного человека название PHuiP-FPizdaM. Для эксплуатации злоумышленику не нужно никаких прав, поэтому уязвимость PHP-FPM Nginx имеет критический статус. Проблема присутствует в обеих ветках PHP — 5 и 7, однако ввиду особенностей оптимизации эксплуатация возможна только в PHP седьмой версии.

Уязвимость затрагивает версий:

  • PHP ветки 7.1.x — все версии ниже 7.1.33
  • PHP ветки 7.2.x — все версии ниже 7.2.24
  • PHP ветки 7.3.x — все версии ниже 7.3.11

Стенд

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

docker-compose.yml

Запускается простой командой: docker-compose up -d.

Я хочу посмотреть на уязвимость поближе, поэтому давайте собирем PHP из исходников. Для начала стартуем Debian.

Теперь надо позаботиться об установке нужных пакетов.

Вытяним последнюю уязвимую версию php — 7.3.10.

Настроем PHP с поддержкой php-fpm.

Теперь зайемемся компиляцией и установкой. Здесь все тривиально.

Меняем имена стандартных конфигурационных файлов.

После этого изменяем путь до папки, где находятся конфиги.

В конфиге php-fpm (/usr/local/etc/php-fpm.d/www.conf) настраиваем количество дочерних процессов. Чтобы было проще отлаживать, рекомендую поставить 1.

Теперь дело за файлами конфигурации для Nginx.

/etc/nginx/sites-enabled/default

В fastcgi_pass указан адрес нашего PHP-FPM, по умолчанию он висит на 9000 порту. Некоторые части конфига я поясню в процессе разбора уязвимости.

Затем нужно создать файл PHP в корне веб-сервера (/var/www/html/). Тут подойдет даже пустой скрипт, главное, чтобы он имел расширение .php, и Nginx отправлял его к PHP. Я создал index.php, который выводит приветствие.

/var/www/html/index.php

Теперь все готово, можно запускать Nginx.

А затем и PHP-FPM через отладчик.

Для GDB включаем возможность отлаживать дочерние процессы и стартуем сервис.

Готовый к работе стенд с PHP-FPM и Nginx
Готовый к работе стенд с PHP-FPM и Nginx

Детали уязвимости

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

Коммит, который патчит уязвимость CVE-2019-11043 в PHP 7.3.10
Коммит, который патчит уязвимость CVE-2019-11043 в PHP 7.3.10
/php-src-php-7.3.10/sapi/fpm/fpm/fpm_main.c

/php-src-php-7.3.11/sapi/fpm/fpm/fpm_main.c

Как видишь, добавлены дополнительные проверки для переменных path_info и tflag. В этой части кода происходит обработка путей вида /info.php/test.

Поставим брекпоинт чуть выше запатченных строк, на строке 1143, и попробуем отправить GET-запрос с байтом переноса строки (%0a).

/php-src-php-7.3.10/sapi/fpm/fpm/fpm_main.c

Отработал брекпойнт в функции init_request_info после отправки байта 0x0a
Отработал брекпойнт в функции init_request_info после отправки байта 0x0a

Разумеется, сначала URL попадает в Nginx. Напомню строчку из конфига.

default

Переданный байт переноса строки ломает логику регулярного выражения в директиве fastcgi_split_path_info. В результате переменная env_path_info принимает пустое значение, и pilen становится равной 0. Затем на основе этих значений высчитывается path_info.

/php-src-php-7.3.10/sapi/fpm/fpm/fpm_main.c

Когда запрашиваемый файл не найден, PHP-FPM идет выше по URI и пытается заново отправить запрос на вышестоящий скрипт, если таковой имеется. Определяется по знакам /. Таким образом, если я запрошу http://phprce.vh/index.php/info.php, то отработает скрипт index.php, так как info.php у меня отсутствует.

PHP-FPM передает управление вышестоящему скрипту если не найден запрашиваемый
PHP-FPM передает управление вышестоящему скрипту если не найден запрашиваемый
/php-src-php-7.3.10/sapi/fpm/fpm/fpm_main.c

Поэтому есть две переменные: script_path_translated — это полный URI, и pt — путь до скрипта выше. В нашем случае они имеют длину 37 (0x25) и 23 (0x17) байта соответственно.

  • script_path_translated — /var/www/html/index.php/path\ninfo.php
  • pt — /var/www/html/index.php
Переменные пути до скриптов и их длина
Переменные пути до скриптов и их длина

На основе этих переменных считается slen.

/php-src-php-7.3.10/sapi/fpm/fpm/fpm_main.c

А затем высчитывается path_info.

Переменные на основе которых высчитывается path_info
Переменные на основе которых высчитывается path_info
/php-src-php-7.3.10/sapi/fpm/fpm/fpm_main.c

Сейчас в выражении env_path_info + pilen — slen, только slen отлична от нуля (0xe). Поэтому path_info будет указывать не туда куда нужно, а на адрес, который располагается выше. В моем случае это строка 200 по адресу 0x555556618672. Такая уязвимость называется buffer underflow.

Оригинальный адрес значения path_info и адрес после buffer underflow
Оригинальный адрес значения path_info и адрес после buffer underflow

Если посмотреть на код функции init_request_info дальше, то увидим любопытный кусок.

/php-src-php-7.3.10/sapi/fpm/fpm/fpm_main.c

Так как мы управляем длиной slen, то сможем записать null-байт в любую позицию выше PATH_INFO. Чтобы понять, что из этого можно извлечь, нужно разобраться, как PHP-FPM работает с переменными окружения. Если посмотреть код чуть дальше, то мы увидим, что там устанавливается одна из них — ORIG_SCRIPT_NAME.

/php-src-php-7.3.10/sapi/fpm/fpm/fpm_main.c

Функция FCGI_PUTENV инициализирована как fcgi_quick_putenv.

/php-src-php-7.3.10/main/fastcgi.h

Если ты посмотришь на её код, то увидишь, что она напрямую модифицирует req->env.

/php-src-php-7.3.10/main/fastcgi.c

Эта конструкция инициализируются в самом начале работы с запросом функцией fcgi_hash_init.

/php-src-php-7.3.10/main/fastcgi.c

/php-src-php-7.3.10/main/fastcgi.c

Здесь видим, что req->env — это структура fcgi_data_seg.

/php-src-php-7.3.10/main/fastcgi.c

В этой структуре хранятся глобальные переменные необходимые для коммуникации процесса PHP-FPM и веб-сервера (в моем случае — Nginx). Здесь pos указывает на адрес текущего буфера, end на адрес, где он заканчивается, а data — на то место, куда нужно записывать данные. Когда блок памяти, выделенный под структуру, заканчивается (pos больше end), то создается новая, а адрес текущей записывается в next.

Например, вот так выглядит эта структура для текущего запроса и данные, которые в ней хранятся.

Структура fcgi_data_seg текущего запроса и данные
Структура fcgi_data_seg текущего запроса и данные

Там находится и наш PATH_INFO. Обрати внимание на адреc pos, он расположен выше, поэтому можно установить указатель на него и записать туда null-байт.

Заголовок структуры в памяти PHP-FPM
Заголовок структуры в памяти PHP-FPM

Но сначала необходимо создать новую структуру fcgi_data_seg. Для этого нужно заполнить свободную память в текущем блоке. Если я буду добавлять символы в путь, то будет меняться и slen. Если еще раз глянуть конфиг Nginx, то мы увидим там QUERY_STRING. В моей конфигурации она объявляется раньше, чем PATH_INFO, а значит и в памяти будет располагаться выше.

/etc/nginx/fastcgi_params

Поэтому основной мусор передам через параметры к скрипту. Идея в следующем — нужно, манипулируя частями URL, добиться такой ситуации, когда значение PATH_INFO попадает в новый блок памяти. Тогда между адресом значения PATH_INFO и новым request.env.data.pos будет 34 байта.

Так как мы точно знаем это значение, то можем использовать buffer underflow. С его помощью поставим указатель path_info на request.env.data.pos. У меня получился следующий URL, в твоем случае он может быть другим из-за разницы в адресации памяти. Разница будет в количестве символов А.

Дальше идет еще 1750 букв «A». Не буду приводить их все, чтобы не было похоже на твиттер в день появления смешного мема.

Установим брекпойинт на строку, где происходит запись null-байта (b fpm_main.c:1161) и отправим запрос. Теперь path_info указывает на адрес 0x5555566194a0, а это и есть request.env.data.pos.

Хидер новой структуры fcgi_data_seg до перезаписи pos
Хидер новой структуры fcgi_data_seg до перезаписи pos

Сделаем шаг вперед и посмотрим на состояние структуры теперь.

Заголовок новой структуры fcgi_data_seg после перезаписи pos
Заголовок новой структуры fcgi_data_seg после перезаписи pos

Указатель pos изменился, теперь он имеет значение 0x555556619400. Дальше, как ты уже знаешь, происходит вызов FCGI_PUTENV и добавляется строка ORIG_SCRIPT_NAME и контролируемое нами значение из orig_script_name.

Добавленная строка в переменные окружения
Добавленная строка в переменные окружения
/php-src-php-7.3.10/sapi/fpm/fpm/fpm_main.c

Итак, теперь можно добавлять свои переменные. Нас интересует PHP_VALUE. С ее помощью можно менять настройки PHP.

/php-src-php-7.3.10/sapi/fpm/fpm/fpm_main.c

Только вот не все так просто. PHP-FPM хранит каждую переменную окружения в структуре fcgi_hash_bucket.

/php-src-php-7.3.10/main/fastcgi.c

Все они хранятся в fcgi_hash_buckets, и в отладчике их можно посмотреть командой p *request.env.buckets.

/php-src-php-7.3.10/main/fastcgi.c

Здесь можно обнаружить добавленную нами переменную.

Добавленная через уязвимость переменная окружения в fcgi_hash_buckets
Добавленная через уязвимость переменная окружения в fcgi_hash_buckets

Наверное, ты уже заметил проблему: целостность структуры нарушена. Как минимум не совпадают длина переменной (var_len) и её значения (val_len). PHP-FPM выполняет проверку целостности и такого жесткого вмешательства не потерпит.

Заглянем в FCGI_GETENV.

/php-src-php-7.3.10/main/fastcgi.c

Сначала происходит вызов FCGI_HASH_FUNC. Функция подсчитывает хеш на основе некоторых данных переменной окружения.

/php-src-php-7.3.10/main/fastcgi.h

Я хочу добавить PHP_VALUE. Подсчитаем ее хеш:

Подсчет хеша PHP_VALUE
Подсчет хеша PHP_VALUE

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

Кастомный хидер TEST в fcgi_hash_buckets
Кастомный хидер TEST в fcgi_hash_buckets

Исходя из формулы, первое значение будет равно (‘P’ << 2) = 320 и длина должна быть равна 9 (как у PHP_VALUE).

Естественно, возможных вариантов очень много. Возьмем, например, SLUT. Теперь, когда мы заменим этот добавленный заголовок на переменную PHP_VALUE, все проверки будут пройдены.

/php-src-php-7.3.10/main/fastcgi.c

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

Для теста я возьму опцию session.auto_start=1. Если она будет внедрена, то сервер вернет cookie с сессией. У меня получился такой реквест. Для перебора я просто добавлял символы a в Pimp, и результат контролировал через отладчик с помощью p request.env.buckets.

1750 букв «А» здесь снова опущены. Отправляем и наблюдаем в ответе хидер Set-Cookie, а значит пейлоад отработал успешно.

Успешная эксплуатация PHP-FPM
Успешная эксплуатация PHP-FPM

Эта опция действительна, пока работает воркер PHP-FPM, который был проэксплуатирован.

Теперь осталось найти цепочку гаджетов из настроек PHP, которые будут давать выполнение кода. Вот что предлагают авторы эксплоита:

attack.go

  • short_open_tag=1 — включает короткую форму записи PHP-тегов;
  • include_path=/tmp — указывает директорию /tmp, в которой PHP будет искать файлы;
  • auto_prepend_file=a — файл с именем а будет автоматически обрабатываться перед вызовом любого скрипта;
  • log_errors=1 — включает логирование ошибок;
  • error_reporting=2 — включает уровень логирования ошибок (E_WARNING);
  • error_log=1 — ошибки будут также логироваться в файл a;

После того как все директивы будут установлены, PHP создаст во временной директории /tmp файл a. В него попадет ошибка, которая в конечном счете будет иметь вид:

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

Удаленное выполнение команд в PHP-FPM 7.3.10
Удаленное выполнение команд в PHP-FPM 7.3.10

Оригинальный эксплоит, написанный Эмилем, ты можешь скачать на гитхабе.

Заключение

Сегодня я разобрал одну из самых интересных уязвимостей за последнее время. Здесь прекрасно все: от обнаружения до написания полноценного эксплоита с критическим импактом. Это отличный пример того, как превратить аномалию в полноценно работающий вектор атаки. Для этого нужно понимать механизм работы исследуемого приложения и усердно ресерчить.

Найденная уязвимость в очередной раз доказывает, что даже в таких крупных продуктах, как PHP, еще полно необнаруженных уязвимостей.

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

К слову, дефолтный конфиг Ubuntu 18.04 от падения в пучину RCE отделяет всего одна строка — try_files $fastcgi_script_name =404;.

Дефолтный конфиг fastcgi.conf для nginx в Ubuntu 18.04
Дефолтный конфиг fastcgi.conf для nginx в Ubuntu 18.04

Если ее убрать, то получаем уязвимую среду. А это не такой уж и редкий случай. Представь, что Nginx и PHP-FPM находятся на разных машинах. Такая схема довольно часто используется в продакшене.

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

Что касается патча, то разработчики довольно быстро отреагировали на инцидент и залатали брешь. Но так как дистрибутивы PHP не так часто обновляются на серверах, то в ближайшее время будет еще множество потенциально уязвимых продуктов. Так что не зевай, следи за обновлениями дистрибутивов и своевременно накатывай патчи! Особенно те, которые повышают безопасность.

Большой респект aLLy!

Еще по теме: Как искать уязвимости сайтов

Дима (Kozhuh)

Эксперт в кибербезопасности. Работал в ведущих компаниях занимающихся аналитикой компьютерных угроз. Анонсы новых статей в Телеграме.

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