2015-01-14 19:53:15 +00:00
|
|
|
#!/usr/bin/env ruby
|
|
|
|
|
|
|
|
##
|
|
|
|
# This module requires Metasploit: http://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 was originally ported from:
|
|
|
|
# https://github.com/hasherezade/metasploit_modules/blob/master/md5_lookup.rb
|
|
|
|
#
|
|
|
|
# Authors:
|
|
|
|
# * hasherezade (original work)
|
|
|
|
# * sinn3r (ported the module as a standalone msf tool)
|
|
|
|
#
|
|
|
|
###
|
|
|
|
|
|
|
|
#
|
|
|
|
# Load our MSF API
|
|
|
|
#
|
2015-01-15 18:03:52 +00:00
|
|
|
|
2015-01-14 19:53:15 +00:00
|
|
|
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 'msf/core'
|
|
|
|
require 'optparse'
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Basic prints we can't live without
|
|
|
|
#
|
|
|
|
|
2015-01-15 18:03:52 +00:00
|
|
|
# Prints with [*] that represents the message is a status
|
2015-01-14 19:53:15 +00:00
|
|
|
def print_status(msg='')
|
|
|
|
$stdout.puts "[*] #{msg}"
|
|
|
|
end
|
|
|
|
|
2015-01-15 18:03:52 +00:00
|
|
|
# Prints with [-] that represents the message is an error
|
2015-01-14 19:53:15 +00:00
|
|
|
def print_error(msg='')
|
|
|
|
$stdout.puts "[-] #{msg}"
|
|
|
|
end
|
|
|
|
|
|
|
|
module Md5LookupUtility
|
|
|
|
|
|
|
|
# This class provides the basic settings required for the utility
|
|
|
|
class Config
|
|
|
|
# @!attribute rhost
|
|
|
|
# @return [String] Should be md5cracker.org
|
|
|
|
attr_accessor :rhost
|
|
|
|
|
|
|
|
# @!attribute rport
|
|
|
|
# @return [Fixnum] The port number
|
|
|
|
attr_accessor :rport
|
|
|
|
|
|
|
|
# @!attribute target_uri
|
|
|
|
# @return [String] The URI
|
|
|
|
attr_accessor :target_uri
|
|
|
|
|
|
|
|
# @!attribute out_file
|
|
|
|
# @return [String] The output file path (to save the cracked MD5 results)
|
|
|
|
attr_accessor :out_file
|
|
|
|
|
2015-01-15 05:34:47 +00:00
|
|
|
def initialize(opts={})
|
2015-01-14 19:53:15 +00:00
|
|
|
self.rhost = 'md5cracker.org'
|
|
|
|
self.rport = 80
|
|
|
|
self.target_uri = '/api/api.cracker.php'
|
|
|
|
self.out_file = opts[:out_file] || 'results.txt'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-01-15 05:34:47 +00:00
|
|
|
|
|
|
|
# This class is basically an auxiliary module without relying on msfconsole
|
2015-01-14 19:53:15 +00:00
|
|
|
class Md5Lookup < Msf::Auxiliary
|
|
|
|
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
|
|
|
|
|
|
def initialize(opts={})
|
2015-01-15 22:05:35 +00:00
|
|
|
@config = Md5LookupUtility::Config.new
|
2015-01-15 21:13:03 +00:00
|
|
|
|
2015-01-15 05:34:47 +00:00
|
|
|
super(
|
|
|
|
'DefaultOptions' =>
|
|
|
|
{
|
|
|
|
'SSL' => false, # Doesn't look like md5cracker.org supports HTTPS
|
2015-01-16 02:51:27 +00:00
|
|
|
'RHOST' => @config.rhost,
|
2015-01-15 22:05:35 +00:00
|
|
|
'RPORT' => @config.rport
|
2015-01-15 05:34:47 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2015-01-16 02:51:27 +00:00
|
|
|
# Returns the found cracked MD5 hash
|
|
|
|
#
|
2015-01-15 05:34:47 +00:00
|
|
|
# @param md5_hash [String] The MD5 hash to lookup
|
2015-01-16 02:51:27 +00:00
|
|
|
# @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' => @config.target_uri,
|
2015-01-15 05:34:47 +00:00
|
|
|
'method' => 'GET',
|
2015-01-16 02:51:27 +00:00
|
|
|
'vars_get' => {'database'=> db, 'hash'=>md5_hash}
|
2015-01-15 05:34:47 +00:00
|
|
|
})
|
2015-01-16 02:51:27 +00:00
|
|
|
get_json_result(res)
|
2015-01-14 19:53:15 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2015-01-16 02:51:27 +00:00
|
|
|
|
|
|
|
# Parses the cracked result from a JSON input
|
2015-01-16 02:59:47 +00:00
|
|
|
# @param res [Rex::Proto::Http::Response] The Rex HTTP response
|
|
|
|
# @return [String] The found MD5 result
|
2015-01-16 02:51:27 +00:00
|
|
|
def get_json_result(res)
|
|
|
|
result = ''
|
|
|
|
|
|
|
|
# Hmm, no proper response :-(
|
|
|
|
return result if !res || res.code != 200
|
|
|
|
|
|
|
|
begin
|
|
|
|
json = JSON.parse(res.body)
|
|
|
|
result = json['result'] if json['status']
|
|
|
|
rescue JSON::ParserError
|
|
|
|
# No json?
|
|
|
|
end
|
|
|
|
|
|
|
|
result
|
2015-01-14 19:53:15 +00:00
|
|
|
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 =
|
|
|
|
{
|
2015-01-15 18:03:52 +00:00
|
|
|
:all => nil, # This is shifted before being passed to Md5Lookup
|
2015-01-14 19:53:15 +00:00
|
|
|
: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'
|
|
|
|
}
|
|
|
|
|
2015-01-16 02:51:27 +00:00
|
|
|
# The default file path to save the results to
|
|
|
|
DEFAULT_OUTFILE = 'md5_results.txt'
|
|
|
|
|
|
|
|
# Returns the normalized user inputs
|
|
|
|
#
|
2015-01-14 19:53:15 +00:00
|
|
|
# @param args [Array] This should be Ruby's ARGV
|
2015-01-15 18:03:52 +00:00
|
|
|
# @raise [OptionParser::MissingArgument] Missing arguments
|
2015-01-14 19:53:15 +00:00
|
|
|
# @return [Array] The normalized options
|
|
|
|
def self.parse(args)
|
2015-01-15 18:03:52 +00:00
|
|
|
parser, options = get_parsed_options
|
|
|
|
|
|
|
|
# Set the optional datation argument (--database)
|
|
|
|
if !options[:databases]
|
|
|
|
options[:databases] = get_database_names
|
|
|
|
end
|
|
|
|
|
2015-01-16 02:51:27 +00:00
|
|
|
# Set the optional output argument (--out)
|
|
|
|
if !options[:outfile]
|
|
|
|
options[:outfile] = DEFAULT_OUTFILE
|
|
|
|
end
|
|
|
|
|
2015-01-15 18:03:52 +00:00
|
|
|
# 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'
|
2015-01-15 18:18:50 +00:00
|
|
|
elsif options[:input].blank?
|
|
|
|
raise OptionParser::MissingArgument, '-i is a required argument'
|
2015-01-15 18:03:52 +00:00
|
|
|
end
|
2015-01-14 19:53:15 +00:00
|
|
|
|
2015-01-15 18:03:52 +00:00
|
|
|
options
|
|
|
|
end
|
2015-01-14 19:53:15 +00:00
|
|
|
|
2015-01-15 18:03:52 +00:00
|
|
|
private
|
|
|
|
|
|
|
|
|
|
|
|
# Returns the parsed options from ARGV
|
2015-01-16 02:51:27 +00:00
|
|
|
#
|
2015-01-15 18:18:50 +00:00
|
|
|
# raise [OptionParser::InvalidOption] Invalid option found
|
2015-01-16 02:51:27 +00:00
|
|
|
# @return [OptionParser, Hash] The OptionParser object and an hash containg the options
|
2015-01-15 18:03:52 +00:00
|
|
|
def self.get_parsed_options
|
|
|
|
options = {}
|
|
|
|
parser = OptionParser.new do |opt|
|
2015-01-14 19:53:15 +00:00
|
|
|
opt.banner = "Usage: #{__FILE__} [options]"
|
|
|
|
opt.separator ''
|
|
|
|
opt.separator 'Specific options:'
|
|
|
|
|
2015-01-15 18:18:50 +00:00
|
|
|
opt.on('-i', '--input <file>',
|
|
|
|
'The file that contains all the MD5 hashes (one line per hash)') do |v|
|
2015-01-14 19:53:15 +00:00
|
|
|
if v && !::File.exists?(v)
|
2015-01-15 18:18:50 +00:00
|
|
|
raise OptionParser::InvalidOption, "Invalid input file: #{v}"
|
2015-01-14 19:53:15 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
options[:input] = v
|
|
|
|
end
|
|
|
|
|
2015-01-15 18:18:50 +00:00
|
|
|
opt.on('-d','--databases <names>',
|
|
|
|
"(Optional) Select databases: #{get_database_symbols * ", "} (Default=all)") do |v|
|
2015-01-14 19:53:15 +00:00
|
|
|
options[:databases] = extract_db_names(v)
|
|
|
|
end
|
|
|
|
|
2015-01-16 02:51:27 +00:00
|
|
|
opt.on('-o', '--out <filepath>', "Save the results to a file (Default=#{DEFAULT_OUTFILE})") do |v|
|
|
|
|
options[:outfile] = v
|
|
|
|
end
|
|
|
|
|
2015-01-14 19:53:15 +00:00
|
|
|
opt.on_tail('-h', '--help', 'Show this message') do
|
|
|
|
$stdout.puts opt
|
|
|
|
exit
|
|
|
|
end
|
|
|
|
end
|
2015-01-15 18:03:52 +00:00
|
|
|
return parser, options
|
2015-01-14 19:53:15 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Returns the actual database names based on what the user wants
|
2015-01-16 02:51:27 +00:00
|
|
|
#
|
2015-01-14 19:53:15 +00:00
|
|
|
# @param list [String] A list of user-supplied database names
|
2015-01-16 02:51:27 +00:00
|
|
|
# @return [Array<String>] All the matched database names
|
2015-01-14 19:53:15 +00:00
|
|
|
def self.extract_db_names(list)
|
|
|
|
new_db_list = []
|
|
|
|
|
|
|
|
if list.split(',').include?('all')
|
|
|
|
return get_database_names
|
|
|
|
end
|
|
|
|
|
|
|
|
list.split(',').each do |item|
|
|
|
|
item = item.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
|
2015-01-16 02:51:27 +00:00
|
|
|
#
|
|
|
|
# @return [Array<Symbol>] Database symbols
|
2015-01-14 19:53:15 +00:00
|
|
|
def self.get_database_symbols
|
|
|
|
DATABASES.keys
|
|
|
|
end
|
|
|
|
|
|
|
|
# Returns a list of all the original database values recognized by md5cracker.org
|
2015-01-16 02:51:27 +00:00
|
|
|
#
|
|
|
|
# @return [Array<String>] Original database values
|
2015-01-14 19:53:15 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2015-01-15 21:13:03 +00:00
|
|
|
# This class decides how this process works
|
2015-01-14 19:53:15 +00:00
|
|
|
class Driver
|
|
|
|
def initialize
|
2015-01-15 18:03:52 +00:00
|
|
|
begin
|
|
|
|
@opts = OptsConsole.parse(ARGV)
|
|
|
|
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
|
|
print_error("#{e.message} (please see -h)")
|
|
|
|
exit
|
|
|
|
end
|
2015-01-16 02:51:27 +00:00
|
|
|
|
|
|
|
@output_handle = nil
|
|
|
|
begin
|
|
|
|
@output_handle = ::File.open(@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
|
2015-01-15 05:34:47 +00:00
|
|
|
end
|
|
|
|
|
2015-01-15 21:13:03 +00:00
|
|
|
|
|
|
|
# Main function
|
2015-01-16 02:51:27 +00:00
|
|
|
#
|
|
|
|
# @return [void]
|
2015-01-15 05:34:47 +00:00
|
|
|
def run
|
2015-01-15 21:13:03 +00:00
|
|
|
input = @opts[:input]
|
|
|
|
dbs = @opts[:databases]
|
2015-01-15 05:34:47 +00:00
|
|
|
|
2015-01-16 02:51:27 +00:00
|
|
|
get_hash_results(input, dbs) do |result|
|
2015-01-15 21:13:03 +00:00
|
|
|
original_hash = result[:hash]
|
|
|
|
cracked_hash = result[:cracked_hash]
|
2015-01-16 02:51:27 +00:00
|
|
|
credit_db = result[:credit]
|
|
|
|
|
|
|
|
print_status("Found: #{original_hash} = #{cracked_hash} (from #{credit_db})")
|
|
|
|
|
|
|
|
save_result(result) if @output_handle
|
2015-01-15 21:13:03 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-01-16 02:51:27 +00:00
|
|
|
def cleanup
|
|
|
|
@output_handle.close if @output_handle
|
|
|
|
end
|
|
|
|
|
2015-01-15 21:13:03 +00:00
|
|
|
private
|
|
|
|
|
2015-01-16 02:51:27 +00:00
|
|
|
# Saves the MD5 result to file
|
|
|
|
# @return [void]
|
|
|
|
def save_result(result)
|
|
|
|
@output_handle.puts "#{result[:hash]} = #{result[:cracked_hash]}"
|
|
|
|
end
|
|
|
|
|
2015-01-15 21:13:03 +00:00
|
|
|
# Returns the hash results by actually invoking Md5Lookup
|
2015-01-16 02:51:27 +00:00
|
|
|
#
|
|
|
|
# @param input [String] The path of the input file (MD5 hashes)
|
|
|
|
# @return [void]
|
|
|
|
def get_hash_results(input, dbs)
|
2015-01-15 21:13:03 +00:00
|
|
|
search_engine = Md5LookupUtility::Md5Lookup.new
|
|
|
|
extract_hashes(input) do |hash|
|
2015-01-16 02:51:27 +00:00
|
|
|
dbs.each do |db|
|
|
|
|
cracked_hash = search_engine.lookup(hash, db)
|
|
|
|
if !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 if !cracked_hash.empty?
|
|
|
|
end
|
2015-01-15 21:13:03 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Extracts all the MD5 hashes one by one
|
2015-01-16 02:51:27 +00:00
|
|
|
#
|
|
|
|
# @param input_file [String] The path of the input file (MD5 hashes)
|
|
|
|
# @return [void]
|
2015-01-15 21:13:03 +00:00
|
|
|
def extract_hashes(input_file)
|
2015-01-15 22:05:35 +00:00
|
|
|
::File.open(input_file, 'rb') do |f|
|
|
|
|
f.each_line do |hash|
|
|
|
|
next if !is_md5_format?(hash)
|
|
|
|
yield hash.strip # Make sure no newlines
|
|
|
|
end
|
2015-01-15 21:13:03 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Checks if the hash format is MD5 or not
|
2015-01-16 02:51:27 +00:00
|
|
|
#
|
2015-01-15 21:13:03 +00:00
|
|
|
# @param md5_hash [String] The MD5 hash (hex)
|
|
|
|
# @return [TrueClass/FlaseClass] True if the format is valid, otherwise false
|
|
|
|
def is_md5_format?(md5_hash)
|
|
|
|
(md5_hash =~ /^[a-f0-9]{32}$/i) ? true : false
|
2015-01-14 19:53:15 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# main
|
|
|
|
#
|
|
|
|
if __FILE__ == $PROGRAM_NAME
|
2015-01-16 02:51:27 +00:00
|
|
|
driver = Md5LookupUtility::Driver.new
|
2015-01-14 19:53:15 +00:00
|
|
|
begin
|
2015-01-15 05:34:47 +00:00
|
|
|
driver.run
|
2015-01-14 19:53:15 +00:00
|
|
|
rescue Interrupt
|
|
|
|
$stdout.puts
|
|
|
|
$stdout.puts "Good bye"
|
2015-01-16 02:51:27 +00:00
|
|
|
ensure
|
|
|
|
driver.cleanup
|
2015-01-14 19:53:15 +00:00
|
|
|
end
|
|
|
|
end
|