module Metasploit module Framework module PasswordCracker class PasswordCrackerNotFoundError < StandardError end class Cracker include ActiveModel::Validations # @!attribute attack # @return [String] The attack mode for hashcat to use (not applicable to John) attr_accessor :attack # @!attribute config # @return [String] The path to an optional config file for John to use attr_accessor :config # @!attribute cracker # @return [String] Which cracker to use. 'john' and 'hashcat' are valid attr_accessor :cracker # @!attribute cracker_path # This attribute allows the user to specify a cracker binary to use. # If not supplied, the Cracker will search the PATH for a suitable john or hashcat binary # and finally fall back to the pre-compiled john versions shipped with Metasploit. # # @return [String] The file path to an alternative cracker binary to use attr_accessor :cracker_path # @!attribute format # If the cracker type is john, this format will automatically be translated # to the hashcat equivalent via jtr_format_to_hashcat_format # # @return [String] The hash format to try. attr_accessor :format # @!attribute fork # If the cracker type is john, the amount of forks to specify # # @return [String] The hash format to try. attr_accessor :fork # @!attribute hash_path # @return [String] The path to the file containing the hashes attr_accessor :hash_path # @!attribute incremental # @return [String] The incremental mode to use attr_accessor :incremental # @!attribute increment_length # @return [Array] The incremental min and max to use attr_accessor :increment_length # @!attribute mask # If the cracker type is hashcat, If set, the mask to use. Should consist of the character sets # pre-defined by hashcat, such as ?d ?s ?l etc # # @return [String] The mask to use attr_accessor :mask # @!attribute max_runtime # @return [Integer] An optional maximum duration of the cracking attempt in seconds attr_accessor :max_runtime # @!attribute max_length # @return [Integer] An optional maximum length of password to attempt cracking attr_accessor :max_length # @!attribute optimize # @return [Boolean] If the Optimize flag should be given to Hashcat attr_accessor :optimize # @!attribute pot # @return [String] The file path to an alternative John pot file to use attr_accessor :pot # @!attribute rules # @return [String] The wordlist mangling rules to use inside John/Hashcat attr_accessor :rules # @!attribute wordlist # @return [String] The file path to the wordlist to use attr_accessor :wordlist validates :config, :'Metasploit::Framework::File_path' => true, if: 'config.present?' validates :cracker, inclusion: {in: %w[john hashcat]} validates :cracker_path, :'Metasploit::Framework::Executable_path' => true, if: 'cracker_path.present?' validates :fork, numericality: { only_integer: true, greater_than_or_equal_to: 1 }, if: 'fork.present?' validates :hash_path, :'Metasploit::Framework::File_path' => true, if: 'hash_path.present?' validates :pot, :'Metasploit::Framework::File_path' => true, if: 'pot.present?' validates :max_runtime, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: 'max_runtime.present?' validates :max_length, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: 'max_length.present?' validates :wordlist, :'Metasploit::Framework::File_path' => true, if: 'wordlist.present?' # @param attributes [Hash{Symbol => String,nil}] def initialize(attributes={}) attributes.each do |attribute, value| public_send("#{attribute}=", value) end end # This method takes a {framework.db.cred.private.jtr_format} (string), and # returns the string number associated to the hashcat format # # @param[String] a jtr_format string # @return [String] the format number for Hashcat def jtr_format_to_hashcat_format(format) case format when 'md5crypt' '500' when 'descrypt' '1500' when 'bsdicrypt' '12400' when 'sha256crypt' '7400' when 'sha512crypt' '1800' when 'bcrypt' '3200' when 'lm', 'lanman' '3000' when 'nt', 'ntlm' '1000' when 'mssql' '131' when 'mssql05' '132' when 'mssql12' '1731' # hashcat requires a format we dont have all the data for # in the current dumper, so this is disabled in module and lib #when 'oracle', 'des,oracle' # return '3100' when 'oracle11', 'raw-sha1,oracle' '112' when 'oracle12c', 'pbkdf2,oracle12c' '12300' when 'postgres', 'dynamic_1034', 'raw-md5,postgres' '12' when 'mysql' '200' when 'mysql-sha1' '300' when 'PBKDF2-HMAC-SHA512' # osx 10.8+ '7100' when 'xsha' # osx 10.4-6 '122' when 'xsha512' # osx 10.7 '1722' when 'PBKDF2-HMAC-SHA1' # Atlassian '12001' when 'phpass' # Wordpress/PHPass, Joomla, phpBB3 '400' when 'mediawiki' # mediawiki b type '3711' when 'android-samsung-sha1' '5800' when 'android-sha1' '110' when 'android-md5' '10' else nil end end # This method sets the appropriate parameters to run a cracker in incremental mode def mode_incremental self.increment_length = nil self.wordlist = nil self.mask = nil self.max_runtime = nil if cracker == 'john' self.rules = nil self.incremental = 'Digits' elsif cracker == 'hashcat' self.attack = '3' self.incremental = true end end # This method sets the appropriate parameters to run a cracker in wordlist mode # # @param[String] a file location of the wordlist to use def mode_wordlist(file) self.increment_length = nil self.incremental = nil self.max_runtime = nil self.mask = nil if cracker == 'john' self.wordlist = file self.rules = 'wordlist' elsif cracker == 'hashcat' self.wordlist = file self.attack = '0' end end # This method sets the appropriate parameters to run a cracker in a pin mode (4-8 digits) on hashcat def mode_pin self.rules = nil if cracker == 'hashcat' self.attack = '3' self.mask = '?d'*8 self.incremental = true self.increment_length = [4,8] self.max_runtime = 300 #5min on an i7 got through 4-7 digits. 8digit was 32min more end end # This method sets the john to 'normal' mode def mode_normal if cracker == 'john' self.max_runtime = nil self.mask = nil self.wordlist = nil self.rules = nil self.incremental = nil self.increment_length = nil end end # This method sets the john to single mode # # @param[String] a file location of the wordlist to use def mode_single(file) if cracker == 'john' self.wordlist = file self.rules = 'single' self.incremental = nil self.increment_length = nil self.mask = nil end end # This method follows a decision tree to determine the path # to the cracker binary we should use. # # @return [NilClass] if a binary path could not be found # @return [String] the path to the selected JtR binary def binary_path # Always prefer a manually entered path if cracker_path && ::File.file?(cracker_path) return cracker_path else # Look in the Environment PATH for the john binary if cracker == 'john' path = Rex::FileUtils.find_full_path("john") || Rex::FileUtils.find_full_path("john.exe") elsif cracker == 'hashcat' path = Rex::FileUtils.find_full_path("hashcat") || Rex::FileUtils.find_full_path("hashcat.exe") else raise PasswordCrackerNotFoundError, 'No suitable Cracker was selected, so a binary could not be found on the system' end if path && ::File.file?(path) return path end raise PasswordCrackerNotFoundError, 'No suitable john/hashcat binary was found on the system' end end # This method runs the command from {#crack_command} and yields each line of output. # # @yield [String] a line of output from the cracker command # @return [void] def crack if cracker == 'john' results = john_crack_command elsif cracker == 'hashcat' results = hashcat_crack_command end ::IO.popen(results, "rb") do |fd| fd.each_line do |line| yield line end end end # This method returns the version of John the Ripper or Hashcat being used. # # @raise [PasswordCrackerNotFoundError] if a suitable cracker binary was never found # @return [Sring] the version detected def cracker_version if cracker == 'john' cmd = binary_path elsif cracker == 'hashcat' cmd = binary_path cmd << (" -V") end ::IO.popen(cmd, "rb") do |fd| fd.each_line do |line| if cracker == 'john' # John the Ripper 1.8.0.13-jumbo-1-bleeding-973a245b96 2018-12-17 20:12:51 +0100 OMP [linux-gnu 64-bit x86_64 AVX2 AC] # John the Ripper 1.9.0-jumbo-1 OMP [linux-gnu 64-bit x86_64 AVX2 AC] # John the Ripper password cracker, version 1.8.0.2-bleeding-jumbo_omp [64-bit AVX-autoconf] # John the Ripper password cracker, version 1.8.0 return $1.strip if line =~ /John the Ripper(?: password cracker, version)? ([^\[]+)/ elsif cracker == 'hashcat' # v5.1.0 return $1 if line =~ /(v[\d\.]+)/ end end end nil end # This method builds an array for the command to actually run the cracker. # It builds the command from all of the attributes on the class. # # @raise [PasswordCrackerNotFoundError] if a suitable John binary was never found # @return [Array] An array set up for {::IO.popen} to use def john_crack_command cmd_string = binary_path cmd = [cmd_string, '--session=' + cracker_session_id, '--nolog'] if config.present? cmd << ("--config=" + config) else cmd << ("--config=" + john_config_file) end if pot.present? cmd << ("--pot=" + pot) else cmd << ("--pot=" + john_pot_file) end if fork.present? && fork > 1 cmd << ("--fork=" + fork.to_s) end if format.present? cmd << ("--format=" + format) end if wordlist.present? cmd << ("--wordlist=" + wordlist) end if incremental.present? cmd << ("--incremental=" + incremental) end if rules.present? cmd << ("--rules=" + rules) end if max_runtime.present? cmd << ("--max-run-time=" + max_runtime.to_s) end if max_length.present? cmd << ("--max-len=" + max_length.to_s) end cmd << hash_path end # This method builds an array for the command to actually run the cracker. # It builds the command from all of the attributes on the class. # # @raise [PasswordCrackerNotFoundError] if a suitable Hashcat binary was never found # @return [Array] An array set up for {::IO.popen} to use def hashcat_crack_command cmd_string = binary_path cmd = [cmd_string, '--session=' + cracker_session_id, '--logfile-disable'] if pot.present? cmd << ("--potfile-path=" + pot) else cmd << ("--potfile-path=" + john_pot_file) end if format.present? cmd << ("--hash-type=" + jtr_format_to_hashcat_format(format)) end if optimize.present? # https://hashcat.net/wiki/doku.php?id=frequently_asked_questions#what_is_the_maximum_supported_password_length_for_optimized_kernels # Optimized Kernels has a large impact on speed. Here are some stats from Hashcat 5.1.0: # Kali Linux on Dell Precision M3800 ## hashcat -b -w 2 -m 0 # * Device #1: Quadro K1100M, 500/2002 MB allocatable, 2MCU # Speed.#1.........: 185.9 MH/s (11.15ms) @ Accel:64 Loops:16 Thr:1024 Vec:1 ## hashcat -b -w 2 -O -m 0 # * Device #1: Quadro K1100M, 500/2002 MB allocatable, 2MCU # Speed.#1.........: 463.6 MH/s (8.92ms) @ Accel:64 Loops:32 Thr:1024 Vec:1 # Windows 10 # PS C:\hashcat-5.1.0> .\hashcat64.exe -b -O -w 2 -m 0 # * Device #1: GeForce RTX 2070 SUPER, 2048/8192 MB allocatable, 40MCU # Speed.#1.........: 13914.0 MH/s (5.77ms) @ Accel:128 Loops:64 Thr:256 Vec:1 # PS C:\hashcat-5.1.0> .\hashcat64.exe -b -O -w 2 -m 0 # * Device #1: GeForce RTX 2070 SUPER, 2048/8192 MB allocatable, 40MCU # Speed.#1.........: 31545.6 MH/s (10.36ms) @ Accel:256 Loops:128 Thr:256 Vec:1 # This change should result in 225%-250% speed boost at the sacrifice of some password length, which most likely # wouldn't be tested inside of MSF since most users are using the MSF modules for word list and easy cracks. # Anything of length where this would cut off is most likely being done independently (outside MSF) cmd << ("-O") end if incremental.present? cmd << ("--increment") if increment_length.present? cmd << ("--increment-min=" + increment_length[0].to_s) cmd << ("--increment-max=" + increment_length[1].to_s) else # anything more than max 4 on even des took 8+min on an i7. # maybe in the future this can be adjusted or made a variable # but current time, we'll leave it as this seems like reasonable # time expectation for a module to run cmd << ("--increment-max=4") end end if rules.present? cmd << ("--rules-file=" + rules) end if attack.present? cmd << ("--attack-mode=" + attack) end if max_runtime.present? cmd << ("--runtime=" + max_runtime.to_s) end cmd << hash_path if mask.present? cmd << mask.to_s end # must be last if wordlist.present? cmd << (wordlist) end cmd end # This runs the show command in john and yields cracked passwords. # # @return [Array] the output from teh command split on newlines def each_cracked_password ::IO.popen(show_command, "rb").readlines end # This method returns the path to a default john.conf file. # # @return [String] the path to the default john.conf file def john_config_file ::File.join(::Msf::Config.data_directory, "jtr", "john.conf") end # This method returns the path to a default john.pot file. # # @return [String] the path to the default john.pot file def john_pot_file ::File.join(::Msf::Config.config_directory, "john.pot") end # This method is a getter for a random Session ID for the cracker. # It allows us to dinstiguish between cracking sessions. # # @ return [String] the Session ID to use def cracker_session_id @session_id ||= ::Rex::Text.rand_text_alphanumeric(8) end # This method builds the command to show the cracked passwords. # # @raise [JohnNotFoundError] if a suitable John binary was never found # @return [Array] An array set up for {::IO.popen} to use def show_command cmd_string = binary_path pot_file = pot || john_pot_file if cracker=='hashcat' cmd = [cmd_string, "--show", "--potfile-path=#{pot_file}", "--hash-type=#{jtr_format_to_hashcat_format(format)}"] elsif cracker=='john' cmd = [cmd_string, "--show", "--pot=#{pot_file}", "--format=#{format}"] if config cmd << "--config=#{config}" else cmd << ("--config=" + john_config_file) end end cmd << hash_path end private end end end end