
# CVE-2026-33701 技术分析 对 CVE-2026-33701 的技术分析,该漏洞是 OpenTelemetry Java Agent RMI 插桩中的一个不安全反序列化漏洞,涵盖利用条件、补丁详情及缓解策略。
严重性: 严重(CVSS v4.0:9.3)
受影响版本: opentelemetry-javaagent < 2.26.1
已修复版本: 2.26.1(2026年3月23日发布)
OpenTelemetry 可能是当前 Java 生态系统中采用最广泛的可观测性框架。特别是 Java agent,被数千个生产服务用于自动对应用程序进行追踪、指标和日志的插桩,而无需开发人员修改任何代码。你只需将其作为 javaagent 参数附加,它就会在后台完成所有工作。
CVE-2026-33701 之所以引人关注,不仅仅在于漏洞本身,更在于其引入的方式。该 agent 在其 RMI 插桩的副作用下,悄悄注册了一个自定义 RMI 端点。开发人员并不知道它的存在。它不在任何应用程序代码中。也不是他们配置的内容。agent 自动将其放置在那里,而在低于 2.26.1 的版本中,该端点在对传入数据进行反序列化时完全没有应用任何过滤器。
如果应用程序已经暴露了 RMI 或 JMX 端口,能够通过网络访问该端口的攻击者可以向 agent 的自定义端点发送精心构造的序列化负载,并可能以 JVM 进程的权限实现远程代码执行。关键在于,RCE 需要应用程序类路径上存在兼容的 gadget 链。稍后会详细说明。
当 OTel Java agent 附加到 JVM 时,它会为上下文传播对 RMI 调用进行插桩。其思路是,当 RMI 客户端调用远程方法时,agent 需要将当前追踪上下文传播到服务端,以便跨服务边界正确连接 span。
为此,agent 使用硬编码的 ObjID 注册了自己的自定义 RMI 对象。在 ContextPropagator.java 中:
public static final ObjID CONTEXT_CALL_ID =
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
这个 ObjID 是 agent 在 RMI 运行时中识别自身端点的方式。当被插桩的 RMI 客户端连接到服务器时,它首先检查服务器是否注册了此 ObjID。如果已注册,则向其发送上下文传播负载。如果没有,则跳过上下文传播,仅执行常规调用。
问题在于,该端点注册在 JVM RMI 运行时内部,这意味着它与应用程序打开的任何 RMI 或 JMX 端口共享相同的传输通道。如果应用程序设置了 -Dcom.sun.management.jmxremote.port,或者导出了自己的 RMI 服务,那么任何能够连接到该端口的人都可以访问 agent 的端点。
反序列化发生在 ContextPayload.java 中。在低于 2.26.1 的版本中,read() 方法如下所示:
@SuppressWarnings("BanSerializableRead") // fine
public static ContextPayload read(ObjectInput oi) throws IOException {
try {
Object object = oi.readObject();
if (object instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) object;
return new ContextPayload(map);
}
} catch (ClassCastException | ClassNotFoundException ex) {
logger.log(FINE, "Error reading object", ex);
}
return null;
}
带有注释 // fine 的 @SuppressWarnings("BanSerializableRead") 注解实际上颇具暗示性。团队中某人在某个时候将此标记为问题,添加了抑制注解,而注释旨在为其辩护。显然,这并不“fine”。
没有序列化过滤器的 oi.readObject() 是典型的不安全反序列化模式。Java 序列化在反序列化过程中会愉快地实例化类路径上的任何类,如果攻击者能够控制序列化流,他们就可以选择实例化哪些类以及以何种顺序实例化。这就是 gadget 链攻击的基础。
代码确实在反序列化后检查了 instanceof Map,但到那时,损害已经造成。gadget 链在 readObject() 调用本身期间执行,早于任何类型检查。从安全角度来看,instanceof 检查完全无关紧要。
2.26.1 中的修复完全替换了反序列化方法。新实现不再序列化 Map 对象并调用 readObject(),而是手动将上下文条目作为原始类型读取:
@Nullable
public static ContextPayload read(ObjectInput oi) throws IOException {
int size = oi.readInt();
if (size > MAX_CONTEXT_ENTRIES) {
logger.log(
FINE,
"RMI context propagation payload size {0} exceeds maximum allowed of {1}, skipping context propagation.",
new Object[] {size, MAX_CONTEXT_ENTRIES});
return null;
}
Map<String, String> map = new HashMap<>();
for (int i = 0; i < size; i++) {
String key = oi.readUTF();
String value = oi.readUTF();
map.put(key, value);
}
return new ContextPayload(map);
}
写入端也已更新以匹配:
public void write(ObjectOutput out) throws IOException {
int size = context.size();
if (size > MAX_CONTEXT_ENTRIES) {
out.writeInt(0);
return;
}
out.writeInt(size);
for (Map.Entry<String, String> entry : context.entrySet()) {
out.writeUTF(entry.getKey());
out.writeUTF(entry.getValue());
}
}
此方法仅从流中读取原始类型(int 和 UTF 字符串)。不会发生对象实例化。没有 gadget 链可以通过 readInt() 或 readUTF() 执行。该修复正确且完整。
还有一个值得注意的第二个更改。用于标识 agent 自定义端点的 ObjID 已进行版本化:
// 之前(2.26.0)
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
// 之后(2.26.1)
new ObjID("io.opentelemetry.javaagent.context-call-v2".hashCode());
这意味着已修补的 agent 将完全不会响应旧的 ObjID。针对旧端点的攻击者从已修补的服务器将得不到任何响应。这是在反序列化修复之上的一个很好的额外加固措施,它有效地破坏了任何针对 v1 端点构建的漏洞利用,即使反序列化在某种程度上仍然可达。
以下三个条件必须同时成立才能被利用:
1. OpenTelemetry Java agent 附加在 JDK 16 或更低版本上
这是最重要的约束条件。JDK 17 对 RMI 内部机制进行了重大更改,并通过 Java 平台模块系统强制实施更严格的模块封装。大多数针对 Java 反序列化的 gadget 链依赖于反射来访问内部 JDK 类或调用通常不可访问的方法。JDK 17 默认通过强封装破坏了这些反射路径,这意味着大多数已知的 gadget 链在 JDK 17 及更高版本上根本无法工作。
然而,在 JDK 8、11 和 16 上,反射访问要宽松得多,gadget 链可以按预期工作。这些 JDK 版本仍然代表生产 Java 部署的很大一部分,尤其是在较旧的企业环境中。
2. RMI 或 JMX 端口对攻击者网络可达
应用程序需要配置类似 -Dcom.sun.management.jmxremote.port=9010 的内容,或者需要导出自己的 RMI 服务。OTel agent 的端点依附于任何已在使用的 RMI 传输通道。如果没有打开 RMI 端口,则端点不可达。
3. 应用程序类路径上存在与 gadget 链兼容的库
agent jar 本身不包含任何与 gadget 链兼容的类。我们通过检查从 2.26.0 agent jar 中提取的类列表确认了这一点。agent 包含 OTel 插桩代码、shaded API 类、semconv 定义和引导基础设施。常见的被利用的 gadget 源(如 Commons Collections、Commons BeanUtils 或 Spring Framework 类)均未捆绑在 agent 内部。
这意味着 gadget 链必须来自应用程序。开发人员必须包含一个包含可利用序列化 gadget 的库,这意味着可利用性取决于应用程序依赖的内容。
上述三个条件听起来可能具有限制性,但在真实的企业 Java 环境中,它们实际上经常同时出现。考虑一个典型的生产场景:运行在 JDK 11 或 JDK 17(带有 --add-opens 标志,这在容器化部署中很常见,并且可以部分重新启用 gadget 链)上的后端服务,使用 OTel agent 进行可观测性插桩,启用 JMX 进行运维监控,并且依赖树足够丰富以包含与 gadget 兼容的类。
关键见解在于,开发人员将可观测性 agent 视为被动基础设施。他们期望 agent 进行观察,而不是打开新的网络可达端点。这里创建的攻击面从应用程序开发人员的角度来看是不可见的。他们没有编写任何 RMI 代码。他们没有配置任何 RMI 端点。agent 作为插桩的后果静默地完成了这一切。
关于 gadget 链要求的进一步研究,ysoserial 项目(公开可用,在学术和专业安全研究中被广泛引用)记录了几种与此类漏洞相关的 Java 反序列化 gadget 链。像 CommonsCollections、Spring1 和 Spring2 这样的链是现有库类如何被链接以通过不安全反序列化实现代码执行的记录良好的示例。在给定环境中哪些特定链适用完全取决于该应用程序类路径上有哪些库。
升级到 opentelemetry-javaagent 版本 2.26.1 或更高版本。这是唯一完整的修复。
如果无法立即升级,可以通过在 JVM 启动参数中添加以下系统属性来完全禁用 RMI 插桩:
-Dotel.instrumentation.rmi.enabled=false
这将禁用注册易受攻击端点的插桩,从而完全消除攻击面。设置此标志后,跨 RMI 调用的上下文传播将无法工作,但作为临时缓解措施是可以接受的。
与此 CVE 无关,如果 JMX 在无认证的情况下暴露在网络可达端口上,无论是否存在 OTel agent,都应将其视为严重错误配置。