pl4tyz/CVE-2026-33701-Unsafe-Deserialization-in-OpenTelemetry-Java-Agent-RMI-Instrumentation
GitHub: pl4tyz/CVE-2026-33701-Unsafe-Deserialization-in-OpenTelemetry-Java-Agent-RMI-Instrumentation
针对 OpenTelemetry Java Agent RMI 插桩中不安全反序列化漏洞(CVE-2026-33701)的详细技术分析与利用条件说明。
Stars: 0 | Forks: 0
# CVE-2026-33701 — OpenTelemetry Java Agent RMI Instrumentation 中的不安全反序列化漏洞
**严重程度:** 严重 (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 的自定义端点发送精心构造的序列化 Payload,并可能以 JVM 进程的权限实现远程代码执行 (RCE)。问题在于,RCE 要求应用程序 classpath 中存在兼容的 Gadget Chain。稍后会详细讨论这一点。
## Agent 如何插桩 RMI
当 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。如果是,它会向其发送上下文传播 Payload。如果不是,它会跳过上下文传播,直接执行常规调用。
问题在于,这个端点是在 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 map = (Map) object;
return new ContextPayload(map);
}
} catch (ClassCastException | ClassNotFoundException ex) {
logger.log(FINE, "Error reading object", ex);
}
return null;
}
```
`@SuppressWarnings("BanSerializableRead")` 注释以及 `// fine` 的注释其实很能说明问题。团队中的某人在某个时候将其标记为一个关注点,随后添加了抑制注解,并用该注释来证明其合理性。显然,这并不“fine”。
没有任何序列化过滤器的 `oi.readObject()` 是典型的不安全反序列化模式。Java 序列化在反序列化期间会乐意地实例化 classpath 上的任何类,如果攻击者能够控制序列化流,他们就可以选择实例化哪些类以及按什么顺序实例化。这就是 Gadget Chain 攻击的基础。
代码在反序列化之后确实检查了 `instanceof Map`,但此时损害已经造成。Gadget Chain 在 `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 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 entry : context.entrySet()) {
out.writeUTF(entry.getKey());
out.writeUTF(entry.getValue());
}
}
```
这种方法只从流中读取原始类型(int 和 UTF 字符串)。不涉及对象实例化。Gadget Chain 无法通过 `readInt()` 或 `readUTF()` 执行。该修复是正确且完整的。
还有第二个值得注意的变化。用于识别 Agent 自定义端点的 ObjID 进行了版本化:
```
// Before (2.26.0)
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
// After (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 Chain 都依赖于反射来访问内部 JDK 类或调用通常不可访问的方法。由于强封装,JDK 17 默认破坏了这些反射路径,这意味着大多数已知的 Gadget Chain 在 JDK 17 及更高版本上根本无法工作。
然而,在 JDK 8、11 和 16 上,反射访问要宽松得多,Gadget Chain 可以按预期工作。这些 JDK 版本仍然占生产 Java 部署的很大一部分,尤其是在较旧的企业环境中。
**2. RMI 或 JMX 端口可被攻击者通过网络访问**
应用程序需要配置类似 `-Dcom.sun.management.jmxremote.port=9010` 的参数,或者需要导出自己的 RMI 服务。OTel Agent 的端点寄生在已使用的任何 RMI 传输上。如果没有开放的 RMI 端口,则无法访问该端点。
**3. 应用程序 classpath 中存在兼容 Gadget Chain 的库**
Agent jar 本身不包含任何兼容 Gadget Chain 的类。我们通过检查从 2.26.0 Agent jar 提取的类列表确认了这一点。Agent 包含 OTel 插桩代码、重定位的 API 类、semconv 定义和引导基础设施。其中没有捆绑任何常见的被利用 Gadget 来源,如 Commons Collections、Commons BeanUtils 或 Spring Framework 类。
这意味着 Gadget Chain 必须来自应用程序。开发者必须包含一个含有可利用序列化 Gadget 的库,这意味着可利用性取决于应用程序的依赖项。
## 为什么这在实践中仍然重要
上述三个条件听起来可能有限制性,但在实际的企业 Java 环境中,它们实际上很常见。考虑一个典型的生产场景:一个运行在 JDK 11 或 JDK 17(带有 `--add-opens` 标志,这在容器化部署中很常见,并且可以部分重新启用 Gadget Chain)上的后端服务,使用 OTel Agent 进行可观测性插桩,启用了 JMX 以进行操作监控,并且依赖树足够丰富,包含兼容 Gadget 的类。
关键的见解是,开发者信任可观测性 Agent 作为被动基础设施。他们期望 Agent 是观察者,而不是开放新的网络可访问端点。这里产生的攻击面从应用程序开发者的角度来看是不可见的。他们没有编写任何 RMI 代码。他们没有配置任何 RMI 端点。Agent 作为插桩的后果静默地完成了这一切。
## Gadget Chain 依赖
关于 Gadget Chain 要求的进一步研究,ysoserial 项目(公开可用,在学术和专业安全研究中被广泛引用)记录了几个与此类漏洞相关的 Java 反序列化 Gadget Chain。像 `CommonsCollections`、`Spring1` 和 `Spring2` 这样的 Chain 是很好的文档示例,展示了如何将现有的库类链接起来,通过不安全反序列化实现代码执行。在特定环境中适用哪些具体的 Chain,完全取决于该应用程序在其 classpath 上有哪些库。
## 修复方案
升级到 `opentelemetry-javaagent` 版本 2.26.1 或更高版本。这是唯一完整的修复方案。
如果无法立即升级,可以通过向 JVM 启动标志添加以下系统属性来完全禁用 RMI 插桩:
```
-Dotel.instrumentation.rmi.enabled=false
```
这将禁用注册易受攻击端点的插桩,从而完全消除攻击面。设置此标志期间,跨 RMI 调用的上下文传播将无法工作,但作为临时的缓解措施,这是可以接受的。
独立于此 CVE,如果 JMX 在没有身份验证的情况下暴露于网络可访问端口,则无论是否存在 OTel Agent,都应将其视为严重配置错误。
## 参考
- GitHub Security Advisory: GHSA-xw7x-h9fj-p2c7
- Patch commit: open-telemetry/opentelemetry-java-instrumentation@9cf4fba
- NVD entry: CVE-2026-33701
- ysoserial (public gadget chain reference): https://github.com/frohoff/ysoserial
标签:0day, API集成, CISA项目, CVE-2026-33701, CVSS 9.3, Gadget Chain, GET参数, Javaagent, Java Agent, Java 安全, JS文件枚举, OpenTelemetry, RCE, RMI, RMI Instrumentation, VS Code 插件, 上下文传播, 严重漏洞, 中间件安全, 反序列化漏洞, 可观测性, 对象注入, 服务器监控, 漏洞分析, 用户代理, 编程工具, 路径探测, 远程代码执行