metasploit-framework/tools/password/md5_lookup.rb

467 lines
13 KiB
Ruby
Executable File

#!/usr/bin/env ruby
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
#
# This script will look up a collection of MD5 hashes (from a file) against the following databases
# via md5cracker.org:
# authsecu, i337.net, md5.my-addr.com, md5.net, md5crack, md5cracker.org, md5decryption.com,
# md5online.net, md5pass, netmd5crack, tmto.
# This msf tool script was originally ported from:
# https://github.com/hasherezade/metasploit_modules/blob/master/md5_lookup.rb
#
# To-do:
# Maybe as a msf plugin one day and grab hashes directly from the workspace.
#
# Authors:
# * hasherezade (http://hasherezade.net, @hasherezade)
# * sinn3r (ported the module as a standalone msf tool)
#
#
# Load our MSF API
#
msfbase = __FILE__
while File.symlink?(msfbase)
msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
end
$:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', 'lib')))
require 'msfenv'
require 'rex'
require 'optparse'
#
# Basic prints we can't live without
#
# Prints with [*] that represents the message is a status
#
# @param msg [String] The message to print
# @return [void]
def print_status(msg='')
$stdout.puts "[*] #{msg}"
end
# Prints with [-] that represents the message is an error
#
# @param msg [String] The message to print
# @return [void]
def print_error(msg='')
$stdout.puts "[-] #{msg}"
end
module Md5LookupUtility
# This class manages the disclaimer
class Disclaimer
# @!attribute config_file
# @return [String] The config file path
attr_accessor :config_file
# @!attribute group_name
# @return [String] The name of the tool
attr_accessor :group_name
def initialize
self.config_file = Msf::Config.config_file
self.group_name = 'MD5Lookup'
end
# Prompts a disclaimer. The user will not be able to get out unless they acknowledge.
#
# @return [TrueClass] true if acknowledged.
def ack
print_status("WARNING: This tool will look up your MD5 hashes by submitting them")
print_status("in the clear (HTTP) to third party websites. This can expose")
print_status("sensitive data to unknown and untrusted entities.")
while true
$stdout.print "[*] Enter 'Y' to acknowledge: "
if $stdin.gets =~ /^y|yes$/i
return true
end
end
end
# Saves the waiver so the warning won't show again after ack
#
# @return [void]
def save_waiver
save_setting('waiver', true)
end
# Returns true if we don't have to show the warning again
#
# @return [Boolean]
def has_waiver?
load_setting('waiver') == 'true' ? true : false
end
private
# Saves a setting to Metasploit's config file
#
# @param key_name [String] The name of the setting
# @param value [String] The value of the setting
# @return [void]
def save_setting(key_name, value)
ini = Rex::Parser::Ini.new(self.config_file)
ini.add_group(self.group_name) if ini[self.group_name].nil?
ini[self.group_name][key_name] = value
ini.to_file(self.config_file)
end
# Returns the value of a specific setting
#
# @param key_name [String] The name of the setting
# @return [String]
def load_setting(key_name)
ini = Rex::Parser::Ini.new(self.config_file)
group = ini[self.group_name]
return '' if group.nil?
group[key_name].to_s
end
end
# This class is basically an auxiliary module without relying on msfconsole
class Md5Lookup < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
# @!attribute rhost
# @return [String] Should be md5cracker.org
attr_accessor :rhost
# @!attribute rport
# @return [Integer] The port number to md5cracker.org
attr_accessor :rport
# @!attribute target_uri
# @return [String] The URI (API)
attr_accessor :target_uri
# @!attribute ssl
# @return [FalseClass] False because doesn't look like md5cracker.org supports HTTPS
attr_accessor :ssl
def initialize(opts={})
# The user should not be able to modify these settings, otherwise
# the we can't guarantee results.
self.rhost = 'md5cracker.org'
self.rport = 80
self.target_uri = '/api/api.cracker.php'
self.ssl = false
super(
'DefaultOptions' =>
{
'SSL' => self.ssl,
'RHOST' => self.rhost,
'RPORT' => self.rport
}
)
end
# Returns the found cracked MD5 hash
#
# @param md5_hash [String] The MD5 hash to lookup
# @param db [String] The specific database to check against
# @return [String] Found cracked MD5 hash
def lookup(md5_hash, db)
res = send_request_cgi({
'uri' => self.target_uri,
'method' => 'GET',
'vars_get' => {'database' => db, 'hash' => md5_hash}
})
get_json_result(res)
end
private
# Parses the cracked result from a JSON input
# @param res [Rex::Proto::Http::Response] The Rex HTTP response
# @return [String] Found cracked MD5 hash
def get_json_result(res)
result = ''
# Hmm, no proper response :-(
return result unless res && res.code == 200
begin
json = JSON.parse(res.body)
result = json['result'] if json['status']
rescue JSON::ParserError
# No json?
end
result
end
end
# This class parses the user-supplied options (inputs)
class OptsConsole
# The databases supported by md5cracker.org
# The hash keys (symbols) are used as choices for the user, the hash values are the original
# database values that md5cracker.org will recognize
DATABASES =
{
:all => nil, # This is shifted before being passed to Md5Lookup
:authsecu => 'authsecu',
:i337 => 'i337.net',
:md5_my_addr => 'md5.my-addr.com',
:md5_net => 'md5.net',
:md5crack => 'md5crack',
:md5cracker => 'md5cracker.org',
:md5decryption => 'md5decryption.com',
:md5online => 'md5online.net',
:md5pass => 'md5pass',
:netmd5crack => 'netmd5crack',
:tmto => 'tmto'
}
# The default file path to save the results to
DEFAULT_OUTFILE = 'md5_results.txt'
# Returns the normalized user inputs
#
# @param args [Array] This should be Ruby's ARGV
# @raise [OptionParser::MissingArgument] Missing arguments
# @return [Hash] The normalized options
def self.parse(args)
parser, options = get_parsed_options
# Set the optional datation argument (--database)
unless options[:databases]
options[:databases] = get_database_names
end
# Set the optional output argument (--out)
unless options[:outfile]
options[:outfile] = DEFAULT_OUTFILE
end
# Now let's parse it
# This may raise OptionParser::InvalidOption
parser.parse!(args)
# Final checks
if options.empty?
raise OptionParser::MissingArgument, 'No options set, try -h for usage'
elsif options[:input].blank?
raise OptionParser::MissingArgument, '-i is a required argument'
end
options
end
private
# Returns the parsed options from ARGV
#
# raise [OptionParser::InvalidOption] Invalid option found
# @return [OptionParser, Hash] The OptionParser object and an hash containing the options
def self.get_parsed_options
options = {}
parser = OptionParser.new do |opt|
opt.banner = "Usage: #{__FILE__} [options]"
opt.separator ''
opt.separator 'Specific options:'
opt.on('-i', '--input <file>',
'The file that contains all the MD5 hashes (one line per hash)') do |v|
if v && !::File.exist?(v)
raise OptionParser::InvalidOption, "Invalid input file: #{v}"
end
options[:input] = v
end
opt.on('-d','--databases <names>',
"(Optional) Select databases: #{get_database_symbols * ", "} (Default=all)") do |v|
options[:databases] = extract_db_names(v)
end
opt.on('-o', '--out <filepath>',
"(Optional) Save the results to a file (Default=#{DEFAULT_OUTFILE})") do |v|
options[:outfile] = v
end
opt.on_tail('-h', '--help', 'Show this message') do
$stdout.puts opt
exit
end
end
return parser, options
end
# Returns the actual database names based on what the user wants
#
# @param list [String] A list of user-supplied database names
# @return [Array<String>] All the matched database names
def self.extract_db_names(list)
new_db_list = []
list_copy = list.split(',')
if list_copy.include?('all')
return get_database_names
end
list_copy.each do |item|
item = item.strip.to_sym
new_db_list << DATABASES[item] if DATABASES[item]
end
new_db_list
end
# Returns a list of all of the supported database symbols
#
# @return [Array<Symbol>] Database symbols
def self.get_database_symbols
DATABASES.keys
end
# Returns a list of all the original database values recognized by md5cracker.org
#
# @return [Array<String>] Original database values
def self.get_database_names
new_db_list = DATABASES.values
new_db_list.shift #Get rid of the 'all' option
return new_db_list
end
end
# This class decides how this process works
class Driver
def initialize
begin
@opts = OptsConsole.parse(ARGV)
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
print_error("#{e.message} (please see -h)")
exit
end
@output_handle = nil
begin
@output_handle = ::File.new(@opts[:outfile], 'wb')
rescue
# Not end of the world, but if this happens we won't be able to save the results.
# The user will just have to copy and paste from the screen.
print_error("Unable to create file handle, results will not be saved to #{@opts[:output]}")
end
end
# Main function
#
# @return [void]
def run
input = @opts[:input]
dbs = @opts[:databases]
disclaimer = Md5LookupUtility::Disclaimer.new
unless disclaimer.has_waiver?
disclaimer.ack
disclaimer.save_waiver
end
get_hash_results(input, dbs) do |result|
original_hash = result[:hash]
cracked_hash = result[:cracked_hash]
credit_db = result[:credit]
print_status("Found: #{original_hash} = #{cracked_hash} (from #{credit_db})")
save_result(result) if @output_handle
end
end
# Cleans up the output file handler if exists
#
# @return [void]
def cleanup
@output_handle.close if @output_handle
end
private
# Saves the MD5 result to file
#
# @param result [Hash] The result that contains the MD5 information
# @option result :hash [String] The original MD5 hash
# @option result :cracked_hash [String] The cracked MD5 hash
# @return [void]
def save_result(result)
@output_handle.puts "#{result[:hash]} = #{result[:cracked_hash]}"
end
# Returns the hash results by actually invoking Md5Lookup
#
# @param input [String] The path of the input file (MD5 hashes)
# @yield [result] Gives a hash as the found result
# @return [void]
def get_hash_results(input, dbs)
search_engine = Md5LookupUtility::Md5Lookup.new
extract_hashes(input) do |hash|
dbs.each do |db|
cracked_hash = search_engine.lookup(hash, db)
unless cracked_hash.empty?
result = { :hash => hash, :cracked_hash => cracked_hash, :credit => db }
yield result
end
# Awright, we already found one cracked, we don't need to keep looking,
# Let's move on to the next hash!
break unless cracked_hash.empty?
end
end
end
# Extracts all the MD5 hashes one by one
#
# @param input_file [String] The path of the input file (MD5 hashes)
# @yield [hash] The original MD5 hash
# @return [void]
def extract_hashes(input_file)
::File.open(input_file, 'rb') do |f|
f.each_line do |hash|
next unless is_md5_format?(hash)
yield hash.strip # Make sure no newlines
end
end
end
# Checks if the hash format is MD5 or not
#
# @param md5_hash [String] The MD5 hash (hex)
# @return [TrueClass/FalseClass] True if the format is valid, otherwise false
def is_md5_format?(md5_hash)
(md5_hash =~ /^[a-f0-9]{32}$/i) ? true : false
end
end
end
#
# main
#
if __FILE__ == $PROGRAM_NAME
driver = Md5LookupUtility::Driver.new
begin
driver.run
rescue Interrupt
$stdout.puts
$stdout.puts "Good bye"
ensure
driver.cleanup # Properly close resources
end
end