В этой статье мы поговорим об особенностях уязвимостей десериализации данных в 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. Подробнее об этом ты можешь прочитать в посте Омара beched Ганиева на форуме RDot. Там же ты найдешь все сопутствующие ссылки. Довольно интересно, рекомендую.

Итак, сама идея не нова, а лишь недавно была раскручена и использована для проведения атак на реально существующие приложения. Пришло время пощупать все своими руками!

 

Стенд

Нет ничего проще, чем стенд с 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

Затем скачиваем нужную версию WordPress. Фикса на данный момент до сих пор нет, так что можно скачивать любую.

$ 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';"
Установка WordPress на стенде
Установка WordPress на стенде

Осталось только установить расширение Woocommerce, создать пользователя с правами автора, и стенд готов к экспериментам.

 

Немного о PHAR

PHAR — это PHP Archive, специально сформированный архив, который может быть обработан и исполнен интерпретатором PHP. За это отвечает одноименный модуль PHAR, который входит в стандартную поставку PHP начиная с версии 5.3. Архивы PHAR были введены как удобный способ группировки и доставки файлов PHP. Можно упаковать целое приложение и все еще иметь возможность запустить его прямо из этого файла. При этом его не нужно даже распаковывать на диск. Так, например, поставляется менеджер модулей PEAR или всем известный composer.

Composer поставляется как единый файл PHAR
Composer поставляется как единый файл PHAR

Для создания файлов PHAR можно использовать сам интерпретатор PHP.

create-phar.php
1: <?php
2: @unlink("test.phar");
3: $phar = new Phar("test.phar");
4: $phar["helloworld.php"] = '<?php echo("Hello World!");';

Чтобы иметь возможность создавать архивы, нужно запустить репозиторий с отключенной настройкой phar.readonly.

$ php -dphar.readonly=0 create-phar.php
Создание тестового файла PHAR
Создание тестового файла PHAR

По дефолту сжатие не используется.

Если мы попытаемся выполнить полученный файл, то интерпретатор вернет ошибку.

Попытка выполнить созданный тестовый файл PHAR номер раз
Попытка выполнить созданный тестовый файл PHAR номер раз

При простом обращении к файлу модуль пытается прочитать index.php. Так как этого файла в нашем архиве нет, возникает ошибка. Если мы немного дополним тестовый скрипт, то получим архив, который будет отрабатывать при прямом обращении к нему.

create-phar.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 одинаково легко вызываются как непосредственно из командной строки, так и через веб-сервер. Модуль реализует эту функцию с помощью потоков. Чтобы вызывать какие-то конкретные скрипты из контейнера, существует враппер phar://.

exec-internal.php
1: <?php
2: include('phar://test.phar/helloworld.php');

Результатом выполнения будет строка Hello World!.

Что касается формата файла, то его описание можно найти в официальной документации, в том числе и на русском. Любой уважающий себя файл PHAR включает в себя заглушку, манифест, содержимое и подпись.

Пара слов о заглушке (stub). Она, как правило, содержит загрузчик, который выполняется при прямом запуске архива или когда его подключают через include без указания конкретного файла внутри.

По дефолту там находится обычный код на PHP, который после нескольких манипуляций инклудит index.php. Но никто не мешает нам указать собственный лоадер. Это можно сделать, используя метод Phar::setStub(). В качестве его параметра указываем код.

Последней структурой в заглушке всегда должна идти __HALT_COMPILER();. То есть минимально возможный код выглядит так:

<?php __HALT_COMPILER();

Запомним, так как это пригодится нам чуть дальше.

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

Структура манифеста PHAR-архива
Структура манифеста PHAR-архива

Обрати внимание на раздел метаданных, они хранятся в формате serialize и могут быть как глобальными, так и привязанными к конкретному файлу. Установить глобальные метаданные можно при помощи метода Phar::setMetadata. В качестве аргумента можно передать любую переменную PHP.

test-metadata.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());

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

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

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

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

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

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


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

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

    Подписаться

  • Подписаться
    Уведомить о
    4 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии