rapid7/metasploit-framework

View on GitHub
lib/msf/core/auxiliary/password_cracker.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# -*- coding: binary -*-

require 'open3'
require 'fileutils'
require 'metasploit/framework/password_crackers/cracker'
require 'metasploit/framework/password_crackers/wordlist'
require 'metasploit/framework/password_crackers/jtr/formatter'
require 'metasploit/framework/password_crackers/hashcat/formatter'

module Msf
  ###
  #
  # This module provides methods for working with a Password Cracker
  #
  ###
  module Auxiliary::PasswordCracker
    include Msf::Auxiliary::Report

    #
    # Initializes an instance of an auxiliary module that calls out to John the Ripper (jtr)
    #

    def initialize(info = {})
      super

      register_options(
        [
          OptPath.new('CONFIG', [false, 'The path to a John config file to use instead of the default']),
          OptPath.new('CUSTOM_WORDLIST', [false, 'The path to an optional custom wordlist']),
          OptInt.new('ITERATION_TIMEOUT', [false, 'The max-run-time for each iteration of cracking']),
          OptPath.new('CRACKER_PATH', [false, 'The absolute path to the cracker executable']),
          OptInt.new('FORK', [false, 'Forks for John the Ripper to use', 1]),
          OptBool.new('KORELOGIC', [false, 'Apply the KoreLogic rules to John the Ripper Wordlist Mode(slower)', false]),
          OptBool.new('MUTATE', [false, 'Apply common mutations to the Wordlist (SLOW)', false]),
          OptPath.new('POT', [false, 'The path to a John POT file to use instead of the default']),
          OptBool.new('USE_CREDS', [false, 'Use existing credential data saved in the database', true]),
          OptBool.new('USE_DB_INFO', [false, 'Use looted database schema info to seed the wordlist', true]),
          OptBool.new('USE_DEFAULT_WORDLIST', [false, 'Use the default metasploit wordlist', true]),
          OptBool.new('USE_HOSTNAMES', [false, 'Seed the wordlist with hostnames from the workspace', true]),
          OptBool.new('USE_ROOT_WORDS', [false, 'Use the Common Root Words Wordlist', true])
        ], Msf::Auxiliary::PasswordCracker
      )

      register_advanced_options(
        [
          OptBool.new('DeleteTempFiles', [false, 'Delete temporary wordlist and hash files', true]),
          OptBool.new('OptimizeKernel', [false, 'Utilize Optimized Kernels in Hashcat', true]),
          OptBool.new('ShowCommand', [false, 'Print the cracker command being used', true]),
        ], Msf::Auxiliary::PasswordCracker
      )
    end

    # @param pwd [String] Password recovered from cracking an LM hash
    # @param hash [String] NTLM hash for this password
    # @return [String] `pwd` converted to the correct case to match the
    #   given NTLM hash
    # @return [nil] if no case matches the NT hash. This can happen when
    #   `pwd` came from a john run that only cracked half of the LM hash
    def john_lm_upper_to_ntlm(pwd, hash)
      pwd = pwd.upcase
      hash = hash.upcase
      Rex::Text.permute_case(pwd).each do |str|
        if hash == Rex::Proto::NTLM::Crypt.ntlm_hash(str).unpack('H*')[0].upcase
          return str
        end
      end
      nil
    end

    # This method creates a new {Metasploit::Framework::PasswordCracker::Cracker} and populates
    # some of the attributes based on the module datastore options.
    #
    # @return [nilClass] if there is no active framework db connection
    # @return [Metasploit::Framework::PasswordCracker::Cracker] if it successfully creates a Password Cracker object
    def new_password_cracker(cracking_application)
      fail_with(Msf::Module::Failure::BadConfig, 'Password cracking is not available without an active database connection.') unless framework.db.active
      cracker = Metasploit::Framework::PasswordCracker::Cracker.new(
        config: datastore['CONFIG'],
        cracker_path: datastore['CRACKER_PATH'],
        max_runtime: datastore['ITERATION_TIMEOUT'],
        pot: datastore['POT'],
        optimize: datastore['OptimizeKernel'],
        wordlist: datastore['CUSTOM_WORDLIST']
      )
      cracker.cracker = cracking_application
      begin
        cracker.binary_path
      rescue Metasploit::Framework::PasswordCracker::PasswordCrackerNotFoundError => e
        fail_with(Msf::Module::Failure::BadConfig, e.message)
      end
      # throw this to a local variable since it causes a shell out to pull the version
      cracker_version = cracker.cracker_version
      if cracker.cracker == 'john' && (cracker_version.nil? || !cracker_version.include?('jumbo'))
        fail_with(Msf::Module::Failure::BadConfig, 'John the Ripper JUMBO patch version required.  See https://github.com/magnumripper/JohnTheRipper')
      end
      print_good("#{cracker.cracker} Version Detected: #{cracker_version}")
      cracker
    end

    # This method instantiates a {Metasploit::Framework::JtR::Wordlist}, writes the data
    # out to a file and returns the {Rex::Quickfile} object.
    #
    # @param max_len [Integer] max length of a word in the wordlist, 0 default for ignored value
    # @return [nilClass] if there is no active framework db connection
    # @return [Rex::Quickfile] if it successfully wrote the wordlist to a file
    def wordlist_file(max_len = 0)
      return nil unless framework.db.active

      wordlist = Metasploit::Framework::PasswordCracker::Wordlist.new(
        custom_wordlist: datastore['CUSTOM_WORDLIST'],
        mutate: datastore['MUTATE'],
        use_creds: datastore['USE_CREDS'],
        use_db_info: datastore['USE_DB_INFO'],
        use_default_wordlist: datastore['USE_DEFAULT_WORDLIST'],
        use_hostnames: datastore['USE_HOSTNAMES'],
        use_common_root: datastore['USE_ROOT_WORDS'],
        workspace: myworkspace
      )
      wordlist.to_file(max_len)
    end

    # This method determines if a given password hash already been cracked in the database
    #
    # @param hash [String] password hash to check against the database
    # @return [Boolean] if the password has been cracked in the db
    def password_cracked?(hash)
      framework.db.creds({ pass: hash }).each do |test_cred|
        test_cred.public.cores.each do |core|
          if core.origin_type == 'Metasploit::Credential::Origin::CrackedPassword'
            return true
          end
        end
      end
      false
    end

    # This method creates a job for the password cracker to do. A job is categorized by the hash type
    # and will include the hash type (type), formatted_hashlist (hashes in the cracker's format),
    # creds (db objects for each hash), and cred_ids_left_to_crack (array of db ids that aren't cracked yet)
    #
    # @param jtr_type [String] hash type we're cracking such as md5, sha1
    # @param cracker [String] the password cracker to use such as 'john' or 'hashcat'
    # @return [Hash] of the data needed to crack as described above
    def hash_job(jtr_type, cracker)
      # create the base data
      job = { 'type' => jtr_type, 'formatted_hashlist' => [], 'creds' => [], 'cred_ids_left_to_crack' => [] }
      job['db_formats'] = Metasploit::Framework::PasswordCracker::JtR::Formatter.jtr_to_db(jtr_type)
      if jtr_type == 'dynamic_1034' # postgres
        creds = framework.db.creds(workspace: myworkspace, type: 'Metasploit::Credential::PostgresMD5')
      elsif ['lm', 'nt'].include? jtr_type
        creds = framework.db.creds(workspace: myworkspace, type: 'Metasploit::Credential::NTLMHash')
      else
        creds = framework.db.creds(workspace: myworkspace, type: 'Metasploit::Credential::NonreplayableHash')
      end
      creds.each do |core|
        jtr_format = core.private.jtr_format

        # Unfortunately NTLMHash always set JtR Format to 'nt,lm' so we have to do a special case here
        # to figure out which it is
        if jtr_format == 'nt,lm'
          jtr_format = core.private.data.start_with?('aad3b435b51404eeaad3b435b51404ee') ? 'nt' : 'lm'
        end

        next unless job['db_formats'].include? jtr_format
        # only add hashes which havne't been cracked
        next if password_cracked?(core.private.data)

        job['creds'] << core
        job['cred_ids_left_to_crack'] << core.id
        if cracker == 'john'
          job['formatted_hashlist'] << Metasploit::Framework::PasswordCracker::JtR::Formatter.hash_to_jtr(core)
        elsif cracker == 'hashcat'
          job['formatted_hashlist'] << Metasploit::Framework::PasswordCracker::Hashcat::Formatter.hash_to_hashcat(core)
        end
      end

      if job['creds'].length > 0
        return job
      end

      nil
    end

    # This method takes a results table, and a newly cracked cred, and adds the cred to the table if
    # it isn't there already.  It also creates the cracked credential in the database.
    #
    # @param results [Hash] Hash of the newly cracked cred information, should have hash_type, method, username
    #   core_id, and password fields.
    # @return [Array] Array of results for printing in a table
    def process_cracker_results(results, cred)
      return results if cred['core_id'].nil? # make sure we have good data

      # make sure we dont add the same one again
      if results.select { |r| r.first == cred['core_id'] }.empty?
        results << [cred['core_id'], cred['hash_type'], cred['username'], cred['password'], cred['method']]
      end

      create_cracked_credential(username: cred['username'], password: cred['password'], core_id: cred['core_id'])
      results
    end

    # This method appends a list of cracked hashes to the list used to generate the printed table
    #
    # @param tbl [Array] Array of all results that have been cracked
    # @param cracked_hashes [Array] Array of results to add to the table
    # @return [String] the table in string format for printing
    def append_results(tbl, cracked_hashes)
      cracked_hashes.each do |row|
        next if tbl.rows.include? row

        tbl << row
      end
      tbl.to_s
    end

    # This method returns a cracker results table
    #
    # @return [Rex::Text::Table] table for printing results
    def cracker_results_table
      Rex::Text::Table.new(
        'Header' => 'Cracked Hashes',
        'Indent' => 1,
        'Columns' => ['DB ID', 'Hash Type', 'Username', 'Cracked Password', 'Method']
      )
    end
  end
end