375 lines
15 KiB
Ruby
375 lines
15 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core/auxiliary/password_cracker'
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
include Msf::Auxiliary::PasswordCracker
|
|
include Msf::Exploit::Deprecated
|
|
moved_from 'auxiliary/analyze/jtr_mssql_fast'
|
|
moved_from 'auxiliary/analyze/jtr_mysql_fast'
|
|
moved_from 'auxiliary/analyze/jtr_oracle_fast'
|
|
moved_from 'auxiliary/analyze/jtr_postgres_fast'
|
|
|
|
def initialize
|
|
super(
|
|
'Name' => 'Password Cracker: Databases',
|
|
'Description' => %Q{
|
|
This module uses John the Ripper or Hashcat to identify weak passwords that have been
|
|
acquired from the mssql_hashdump, mysql_hashdump, postgres_hashdump, or oracle_hashdump modules.
|
|
Passwords that have been successfully cracked are then saved as proper credentials.
|
|
Due to the complexity of some of the hash types, they can be very slow. Setting the
|
|
ITERATION_TIMEOUT is highly recommended.
|
|
},
|
|
'Author' =>
|
|
[
|
|
'theLightCosine',
|
|
'hdm',
|
|
'h00die' # hashcat integration
|
|
] ,
|
|
'License' => MSF_LICENSE, # JtR itself is GPLv2, but this wrapper is MSF (BSD)
|
|
'Actions' =>
|
|
[
|
|
['john', {'Description' => 'Use John the Ripper'}],
|
|
['hashcat', {'Description' => 'Use Hashcat'}],
|
|
],
|
|
'DefaultAction' => 'john',
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptBool.new('MSSQL',[false, 'Include MSSQL hashes', true]),
|
|
OptBool.new('MYSQL',[false, 'Include MySQL hashes', true]),
|
|
OptBool.new('ORACLE',[false, 'Include Oracle hashes', true]),
|
|
OptBool.new('POSTGRES',[false, 'Include Postgres hashes', true]),
|
|
OptBool.new('INCREMENTAL',[false, 'Run in incremental mode', true]),
|
|
OptBool.new('WORDLIST',[false, 'Run in wordlist mode', true])
|
|
]
|
|
)
|
|
|
|
end
|
|
|
|
def show_command(cracker_instance)
|
|
return unless datastore['ShowCommand']
|
|
if action.name == 'john'
|
|
cmd = cracker_instance.john_crack_command
|
|
elsif action.name == 'hashcat'
|
|
cmd = cracker_instance.hashcat_crack_command
|
|
end
|
|
print_status(" Cracking Command: #{cmd.join(' ')}")
|
|
end
|
|
|
|
def print_results(tbl, cracked_hashes)
|
|
cracked_hashes.each do |row|
|
|
unless tbl.rows.include? row
|
|
tbl << row
|
|
end
|
|
end
|
|
tbl.to_s
|
|
end
|
|
|
|
def run
|
|
def process_crack(results, hashes, cred, hash_type, method)
|
|
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'], hash_type, cred['username'], cred['password'], method]
|
|
end
|
|
|
|
create_cracked_credential( username: cred['username'], password: cred['password'], core_id: cred['core_id'])
|
|
results
|
|
end
|
|
|
|
def check_results(passwords, results, hash_type, hashes, method, cracker)
|
|
passwords.each do |password_line|
|
|
password_line.chomp!
|
|
next if password_line.blank?
|
|
fields = password_line.split(":")
|
|
# If we don't have an expected minimum number of fields, this is probably not a hash line
|
|
if action.name == 'john'
|
|
cred = {}
|
|
# we branch here since postgres (dynamic_1034) doesn't include the core_id
|
|
if ['dynamic_1034'].include? hash_type
|
|
next unless fields.count >=2
|
|
cred['username'] = fields.shift
|
|
cred['password'] = fields.join(':') # Anything left must be the password. This accounts for passwords with semi-colons in it
|
|
cred['core_id'] = nil
|
|
# we now need to read the pot file to pull the original hash to match it back up successfully
|
|
pot = File.open(cracker.john_pot_file, 'rb')
|
|
pots = pot.read
|
|
pot.close
|
|
# here we combine un:hash and hash:password to make un:hash:password
|
|
combined = []
|
|
pots.each_line do |p|
|
|
next unless p.starts_with?('$dynamic_1034$')
|
|
hashes.each do |h|
|
|
next unless p.starts_with? "#{h['hash'].split(':')[1]}$HEX$"
|
|
cred['core_id'] = h['id']
|
|
break
|
|
end
|
|
end
|
|
else
|
|
#mysql*, mssql*, oracle*
|
|
next unless fields.count >=3
|
|
cred['username'] = fields.shift
|
|
cred['core_id'] = fields.pop
|
|
cred['password'] = fields.join(':') # Anything left must be the password. This accounts for passwords with semi-colons in it
|
|
end
|
|
results = process_crack(results, hashes, cred, hash_type, method)
|
|
elsif action.name == 'hashcat'
|
|
next unless fields.count >= 2
|
|
case hash_type
|
|
when 'dynamic_1034'
|
|
# for postgres we get 3 fields, hash:un:pass.
|
|
hash = fields.shift
|
|
username = fields.shift
|
|
password = fields.join(':')
|
|
when 'oracle11', 'raw-sha1,oracle'
|
|
hash = "#{fields.shift}#{fields.shift}" # we pull the first two fields, hash, and salt
|
|
password = fields.join(':')
|
|
else
|
|
hash = fields.shift
|
|
password = fields.join(':') # Anything left must be the password. This accounts for passwords with : in them
|
|
end
|
|
|
|
next if hash.include?("Hashfile '") && hash.include?("' on line ") # skip error lines
|
|
|
|
hashes.each do |h|
|
|
case hash_type
|
|
when 'mssql05','mssql12', 'mysql'
|
|
# mssql\d\d comes back as 0x then the rest uppercase, so we need to upcase everything to match correctly
|
|
next unless h['hash'].upcase == hash.upcase
|
|
when 'mssql'
|
|
# for whatever reason hashcat zeroes out part of the hash in --show.
|
|
# show: 0x0100a607ba7c0000000000000000000000000000000000000000b6d6261460d3f53b279cc6913ce747006a2e3254:FOO
|
|
# orig: 0x0100A607BA7C54A24D17B565C59F1743776A10250F581D482DA8B6D6261460D3F53B279CC6913CE747006A2E3254
|
|
hash_zero_format = "#{h['hash'][0..13]}#{'0'*40}#{h['hash'][54..hash.length]}".upcase
|
|
next unless hash_zero_format == hash.upcase
|
|
when 'mysql-sha1'
|
|
# add the * back to the beginning to match up and fix casing
|
|
next unless h['hash'] == "*#{hash}".upcase
|
|
when 'oracle11', 'raw-sha1,oracle'
|
|
next unless h['hash'].starts_with? "S:#{hash};".upcase
|
|
when 'oracle12c'
|
|
next unless h['hash'].include? ";T:#{hash}"
|
|
else
|
|
next unless h['hash'] == hash
|
|
end
|
|
cred = {'core_id' => h['id'],
|
|
'username' => h['un'],
|
|
'password' => password}
|
|
results = process_crack(results, hashes, cred, hash_type, method)
|
|
end
|
|
end
|
|
end
|
|
results
|
|
end
|
|
|
|
tbl = Rex::Text::Table.new(
|
|
'Header' => 'Cracked Hashes',
|
|
'Indent' => 1,
|
|
'Columns' => ['DB ID', 'Hash Type', 'Username', 'Cracked Password', 'Method']
|
|
)
|
|
|
|
# array of hashes in jtr_format in the db, converted to an OR combined regex
|
|
hashes_regex = []
|
|
|
|
if datastore['MSSQL']
|
|
hashes_regex << 'mssql'
|
|
hashes_regex << 'mssql05'
|
|
hashes_regex << 'mssql12'
|
|
end
|
|
if datastore['MYSQL']
|
|
hashes_regex << 'mysql'
|
|
hashes_regex << 'mysql-sha1'
|
|
end
|
|
if datastore['ORACLE']
|
|
# dynamic_1506 is oracle 11/12's H field, MD5.
|
|
|
|
# hashcat requires a format we dont have all the data for
|
|
# in the current dumper, so this is disabled in module and lib
|
|
if action.name == 'john'
|
|
hashes_regex << 'oracle'
|
|
hashes_regex << 'dynamic_1506'
|
|
end
|
|
hashes_regex << 'raw-sha1,oracle'
|
|
hashes_regex << 'oracle11'
|
|
hashes_regex << 'oracle12c'
|
|
end
|
|
if datastore['POSTGRES']
|
|
hashes_regex << 'dynamic_1034'
|
|
end
|
|
|
|
# check we actually have an action to perform
|
|
fail_with(Failure::BadConfig, 'Please enable at least one database type to crack') if hashes_regex.empty?
|
|
|
|
# array of arrays for cracked passwords.
|
|
# Inner array format: db_id, hash_type, username, password, method_of_crack
|
|
results = []
|
|
|
|
cracker = new_password_cracker
|
|
cracker.cracker = action.name
|
|
|
|
cracker_version = cracker.cracker_version
|
|
if action.name == 'john' and not cracker_version.include?'jumbo'
|
|
fail_with(Failure::BadConfig, 'John the Ripper JUMBO patch version required. See https://github.com/magnumripper/JohnTheRipper')
|
|
end
|
|
print_good("#{action.name} Version Detected: #{cracker_version}")
|
|
|
|
# create the hash file first, so if there aren't any hashes we can quit early
|
|
# hashes is a reference list used by hashcat only
|
|
cracker.hash_path, hashes = hash_file(hashes_regex)
|
|
|
|
# generate our wordlist and close the file handle.
|
|
wordlist = wordlist_file
|
|
unless wordlist
|
|
print_error('This module cannot run without a database connected. Use db_connect to connect to a database.')
|
|
return
|
|
end
|
|
|
|
wordlist.close
|
|
print_status "Wordlist file written out to #{wordlist.path}"
|
|
|
|
cleanup_files = [cracker.hash_path, wordlist.path]
|
|
|
|
hashes_regex.each do |format|
|
|
# dupe our original cracker so we can safely change options between each run
|
|
cracker_instance = cracker.dup
|
|
cracker_instance.format = format
|
|
if action.name == 'john'
|
|
cracker_instance.fork = datastore['FORK']
|
|
end
|
|
|
|
# first check if anything has already been cracked so we don't report it incorrectly
|
|
print_status "Checking #{format} hashes already cracked..."
|
|
results = check_results(cracker_instance.each_cracked_password, results, format, hashes, 'Already Cracked/POT', cracker_instance)
|
|
vprint_good(print_results(tbl, results))
|
|
|
|
if action.name == 'john'
|
|
print_status "Cracking #{format} hashes in single mode..."
|
|
cracker_instance.mode_single(wordlist.path)
|
|
show_command cracker_instance
|
|
cracker_instance.crack do |line|
|
|
vprint_status line.chomp
|
|
end
|
|
results = check_results(cracker_instance.each_cracked_password, results, format, hashes, 'Single', cracker_instance)
|
|
vprint_good(print_results(tbl, results))
|
|
|
|
print_status "Cracking #{format} hashes in normal mode"
|
|
cracker_instance.mode_normal
|
|
show_command cracker_instance
|
|
cracker_instance.crack do |line|
|
|
vprint_status line.chomp
|
|
end
|
|
results = check_results(cracker_instance.each_cracked_password, results, format, hashes, 'Normal', cracker_instance)
|
|
vprint_good(print_results(tbl, results))
|
|
end
|
|
|
|
print_status "Cracking #{format} hashes in wordlist mode..."
|
|
if action.name == 'john'
|
|
# Turn on KoreLogic rules if the user asked for it
|
|
if datastore['KORELOGIC']
|
|
cracker_instance.rules = 'KoreLogicRules'
|
|
print_status "Applying KoreLogic ruleset..."
|
|
end
|
|
end
|
|
show_command cracker_instance
|
|
cracker_instance.crack do |line|
|
|
vprint_status line.chomp
|
|
end
|
|
|
|
results = check_results(cracker_instance.each_cracked_password, results, format, hashes, 'Wordlist', cracker_instance)
|
|
vprint_good(print_results(tbl, results))
|
|
|
|
if datastore['INCREMENTAL']
|
|
print_status "Cracking #{format} hashes in incremental mode..."
|
|
cracker_instance.mode_incremental
|
|
show_command cracker_instance
|
|
cracker_instance.crack do |line|
|
|
vprint_status line.chomp
|
|
end
|
|
results = check_results(cracker_instance.each_cracked_password, results, format, hashes, 'Incremental', cracker_instance)
|
|
vprint_good(print_results(tbl, results))
|
|
end
|
|
|
|
if datastore['WORDLIST']
|
|
print_status "Cracking #{format} hashes in wordlist mode..."
|
|
cracker_instance.mode_wordlist(wordlist.path)
|
|
# Turn on KoreLogic rules if the user asked for it
|
|
if action.name == 'john' && datastore['KORELOGIC']
|
|
cracker_instance.rules = 'KoreLogicRules'
|
|
print_status "Applying KoreLogic ruleset..."
|
|
end
|
|
show_command cracker_instance
|
|
cracker_instance.crack do |line|
|
|
vprint_status line.chomp
|
|
end
|
|
|
|
results = check_results(cracker_instance.each_cracked_password, results, format, hashes, 'Wordlist', cracker_instance)
|
|
vprint_good(print_results(tbl, results))
|
|
end
|
|
|
|
#give a final print of results
|
|
print_good(print_results(tbl, results))
|
|
end
|
|
if datastore['DeleteTempFiles']
|
|
cleanup_files.each do |f|
|
|
File.delete(f)
|
|
end
|
|
end
|
|
end
|
|
|
|
def hash_file(hashes_regex)
|
|
hashes = []
|
|
wrote_hash = false
|
|
hashlist = Rex::Quickfile.new("hashes_tmp")
|
|
# Convert names from JtR to db
|
|
hashes_regex = hashes_regex.join('|')
|
|
hashes_regex = hashes_regex.gsub('oracle', 'des|oracle')\
|
|
.gsub('dynamic_1506', 'raw-sha1|oracle11|oracle12c|dynamic_1506')\
|
|
.gsub('oracle11', 'raw-sha1|oracle11')\
|
|
.gsub('dynamic_1034', 'postgres|raw-md5')
|
|
regex = Regexp.new hashes_regex
|
|
framework.db.creds(workspace: myworkspace, type: 'Metasploit::Credential::NonreplayableHash').each do |core|
|
|
next unless core.private.jtr_format =~ regex
|
|
# only add hashes which havne't been cracked
|
|
next unless already_cracked_pass(core.private.data).nil?
|
|
if action.name == 'john'
|
|
hashlist.puts hash_to_jtr(core)
|
|
elsif action.name == 'hashcat'
|
|
# hashcat hash files dont include the ID to reference back to so we build an array to reference
|
|
hashes << {'hash' => core.private.data, 'un' => core.public.username, 'id' => core.id}
|
|
hashlist.puts hash_to_hashcat(core)
|
|
end
|
|
wrote_hash = true
|
|
end
|
|
if datastore['POSTGRES']
|
|
framework.db.creds(workspace: myworkspace, type: 'Metasploit::Credential::PostgresMD5').each do |core|
|
|
next unless core.private.jtr_format =~ regex
|
|
# only add hashes which havne't been cracked
|
|
next unless already_cracked_pass(core.private.data).nil?
|
|
if action.name == 'john'
|
|
# hashcat hash files dont include the ID to reference back to so we build an array to reference
|
|
# however, for postgres, john doesn't take an id either
|
|
hashes << {'hash' => hash_to_jtr(core), 'un' => core.public.username, 'id' => core.id}
|
|
hashlist.puts hash_to_jtr(core)
|
|
elsif action.name == 'hashcat'
|
|
# hashcat hash files dont include the ID to reference back to so we build an array to reference
|
|
hashes << {'hash' => core.private.data, 'un' => core.public.username, 'id' => core.id}
|
|
hashlist.puts hash_to_hashcat(core)
|
|
end
|
|
wrote_hash = true
|
|
end
|
|
end
|
|
hashlist.close
|
|
unless wrote_hash # check if we wrote anything and bail early if we didn't
|
|
hashlist.delete
|
|
fail_with Failure::NotFound, 'No applicable hashes in database to crack'
|
|
end
|
|
print_status "Hashes Written out to #{hashlist.path}"
|
|
return hashlist.path, hashes
|
|
end
|
|
end
|