Orange Tsai недавно запостил про «Одну из уязвимостей PHP, которая влияет на XAMPP, развернутый по умолчанию», и нам было интересно рассказать немного об этом. XAMPP - очень популярный способ администраторов и разработчиков развернуть Apache, PHP и множество других инструментов, и любая ошибка, которая может быть RCE в установке этого набора по умолчанию, звучит очень заманчиво.

К счастью, для защитников, ошибку смогли воспроизвести только на инсталляциях PHP для Windows (где PHP используется в режиме CGI), в некоторых локализациях (речь про локали Windows):
Китайский (как упрощенный, так и традиционный)
Японский
Однако, Orange предупреждает, что другие локали могут быть тоже затронуты, и настоятельно призывает обновиться до последней версии PHP, где эти ошибки были уже устранены (для деталей, смотрите его пост - тут автор приводит пример уязвимой конфигурации).
Хочется отметить, что мы не можем проверить наверняка, насколько распространена данная конфигурация или тип развертывания на самом деле.
Пост Orange'а, хоть и информативный, но не демонстрирует нам, что необходимо для реализации такой привлекательной RCE. К сожалению, большой набор конфигураций делает сложным доказательство, что система уязвима (или нет) при пассивном просмотре, это очевидно, потому что информация о локали Windows обычно не размещается в каких либо «отпечатках». Поэтому мы решили приступить к воспроизведению ошибки, если мы сможем её использовать, это ли не лучшее доказательство?
Абсолютно понятно, что уязвимость затрагивает только CGI режим PHP. В этом режиме веб-сервер разбирает HTTP-запросы и передает их в PHP скрипт, затем выполняет некоторую обработку над этим. Для примера, строка запроса парсится и передается в интерпретатор PHP в командной строке - такой запрос http://host/cgi.php?foo=bar
, может быть выполнен как php.exe cgi.php foo=bar
.
Это, конечно, может быть использовано для внедрения команд, поэтому ввод тщательно обрабатывается и очищается перед вызовом php.exe
(после CVE-2012-1823). Однако, как оказалось есть один способ, который разработчики не учли, он позволяет злоумышленнику выйти за пределы командной строки и передать аргументы, которые интерпретируются самим PHP. Этот способ связан с тем, как именно символы unicode перекодируются в ASCII. Покажем это на примере.
Ниже представлены два вызова php.exe
, один вредоносный, а второй нет. Вы сможете найти разницу между ними?

Нет, и я тоже не могу. Давайте взглянем на них через hex-редактор и попробуем найти подсказку.

Здесь мы видим, что первый вызов использует обычное тире (0x2D), а вот второй вызов использует что-то другое (видимо «мягкий дефис»), с кодом 0xAD (на скриншоте он выделен). Хоть внешне они для нас с вами абсолютно одинаковые, для операционной системы - это разные значения.
Самое важное в этом всём, что Apache будет экранировать дефис - 0x2D, но не второй «дефис», 0xAD. Ведь это же не настоящий дефис, верно? Значит его и не надо экранировать...верно?

-Давай посмотрим кто ты на самом деле!
-Я так и знал!
Получается, что в части обработки юникода, PHP будет применять так называемый маппинг «лучшего соответствия», и предположит, что когда пользователь вводит «мягкий дефис», он хотел на самом деле ввести обычный дефис, и он будет его интерпретировать именно так. В этом и заключается уязвимость, если мы предоставим обработчику CGI «мягкий дефис» (0xAD), обработчик не станет его экранировать и передаст PHP. PHP в свою очередь уже будет интерпретировать его как обычный дефис, благодаря этому, злоумышленники могут указывать дополнительные аргументы командной строки, начинающиеся с дефисов для PHP.
Это очень похоже на старую ошибку PHP в режиме CGI (CVE-2012-1823), поэтому мы можем позаимствовать некоторые методы эксплуатации, которые применялись в старой CVE, но адаптируем их для работы с новой ошибкой.
Реализация
В одном полезном writeup (решение для CTF), говорится, что нам понадобятся следующие аргументы для нашей RCE:
-d allow_url_include=1 -d auto_prepend_file=php://input
Благодаря этим аргументам тело нашего HTTP-запроса будет передано и обработано уже в PHP. Но давайте заменим обычные дефисы на ранее обсуждаемые (0xAD). Будут ли они пропущены?
Пример запроса после преобразования:
POST /test.php?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input HTTP/1.1
Host: {{host}}
User-Agent: curl/8.3.0
Accept: */*
Content-Length: 23
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
<?php
phpinfo();
?>
У нас получилось! Мы действительно получили страницу phpinfo, доказывая, что мы достигли RCE.

Выводы
Неприятная ошибка с очень простой эксплуатацией.
Для Windows с локалями на английском, корейском и других, из-за разнообразия сценариев использования PHP в настоящее время невозможно перечислить и исключить все потенциальные сценарии использования данной уязвимости.
К счастью, патчи уже доступны, и мы рекомендуем обновить PHP. Как всегда, фантастическая работа и респект - Orange Tsai.
Мы не будем дублировать здесь рекомендации, их можно найти в ранее упомянутой статье.
Данная статья является переводом публикации коллег из watchTowr (не рекламирую их, но статьи короткие и полезные, советую также в оригинале).
Дополню только информацией из других статей и новостей про уязвимые версии PHP, а именно:
с версии PHP 8.3 по 8.3.8;
с версии PHP 8.2 по 8.2.20;
с версии PHP 8.1 по 8.1.29.