CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/tools/password/md5_lookup.rb
Views: 11768
1
#!/usr/bin/env ruby
2
3
##
4
# This module requires Metasploit: https://metasploit.com/download
5
# Current source: https://github.com/rapid7/metasploit-framework
6
##
7
8
#
9
# This script will look up a collection of MD5 hashes (from a file) against the following databases
10
# via md5cracker.org:
11
# authsecu, i337.net, md5.my-addr.com, md5.net, md5crack, md5cracker.org, md5decryption.com,
12
# md5online.net, md5pass, netmd5crack, tmto.
13
# This msf tool script was originally ported from:
14
# https://github.com/hasherezade/metasploit_modules/blob/master/md5_lookup.rb
15
#
16
# To-do:
17
# Maybe as a msf plugin one day and grab hashes directly from the workspace.
18
#
19
# Authors:
20
# * hasherezade (http://hasherezade.net, @hasherezade)
21
# * sinn3r (ported the module as a standalone msf tool)
22
#
23
24
#
25
# Load our MSF API
26
#
27
28
msfbase = __FILE__
29
while File.symlink?(msfbase)
30
msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
31
end
32
$:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', 'lib')))
33
require 'msfenv'
34
require 'rex'
35
require 'optparse'
36
37
#
38
# Basic prints we can't live without
39
#
40
41
# Prints with [*] that represents the message is a status
42
#
43
# @param msg [String] The message to print
44
# @return [void]
45
def print_status(msg='')
46
$stdout.puts "[*] #{msg}"
47
end
48
49
# Prints with [-] that represents the message is an error
50
#
51
# @param msg [String] The message to print
52
# @return [void]
53
def print_error(msg='')
54
$stdout.puts "[-] #{msg}"
55
end
56
57
module Md5LookupUtility
58
59
# This class manages the disclaimer
60
class Disclaimer
61
62
# @!attribute config_file
63
# @return [String] The config file path
64
attr_accessor :config_file
65
66
# @!attribute group_name
67
# @return [String] The name of the tool
68
attr_accessor :group_name
69
70
def initialize
71
self.config_file = Msf::Config.config_file
72
self.group_name = 'MD5Lookup'
73
end
74
75
# Prompts a disclaimer. The user will not be able to get out unless they acknowledge.
76
#
77
# @return [TrueClass] true if acknowledged.
78
def ack
79
print_status("WARNING: This tool will look up your MD5 hashes by submitting them")
80
print_status("in the clear (HTTP) to third party websites. This can expose")
81
print_status("sensitive data to unknown and untrusted entities.")
82
83
while true
84
$stdout.print "[*] Enter 'Y' to acknowledge: "
85
if $stdin.gets =~ /^y|yes$/i
86
return true
87
end
88
end
89
end
90
91
# Saves the waiver so the warning won't show again after ack
92
#
93
# @return [void]
94
def save_waiver
95
save_setting('waiver', true)
96
end
97
98
# Returns true if we don't have to show the warning again
99
#
100
# @return [Boolean]
101
def has_waiver?
102
load_setting('waiver') == 'true' ? true : false
103
end
104
105
private
106
107
# Saves a setting to Metasploit's config file
108
#
109
# @param key_name [String] The name of the setting
110
# @param value [String] The value of the setting
111
# @return [void]
112
def save_setting(key_name, value)
113
ini = Rex::Parser::Ini.new(self.config_file)
114
ini.add_group(self.group_name) if ini[self.group_name].nil?
115
ini[self.group_name][key_name] = value
116
ini.to_file(self.config_file)
117
end
118
119
# Returns the value of a specific setting
120
#
121
# @param key_name [String] The name of the setting
122
# @return [String]
123
def load_setting(key_name)
124
ini = Rex::Parser::Ini.new(self.config_file)
125
group = ini[self.group_name]
126
return '' if group.nil?
127
group[key_name].to_s
128
end
129
130
end
131
132
# This class is basically an auxiliary module without relying on msfconsole
133
class Md5Lookup < Msf::Auxiliary
134
135
include Msf::Exploit::Remote::HttpClient
136
137
# @!attribute rhost
138
# @return [String] Should be md5cracker.org
139
attr_accessor :rhost
140
141
# @!attribute rport
142
# @return [Integer] The port number to md5cracker.org
143
attr_accessor :rport
144
145
# @!attribute target_uri
146
# @return [String] The URI (API)
147
attr_accessor :target_uri
148
149
# @!attribute ssl
150
# @return [FalseClass] False because doesn't look like md5cracker.org supports HTTPS
151
attr_accessor :ssl
152
153
def initialize(opts={})
154
# The user should not be able to modify these settings, otherwise
155
# the we can't guarantee results.
156
self.rhost = 'md5cracker.org'
157
self.rport = 80
158
self.target_uri = '/api/api.cracker.php'
159
self.ssl = false
160
161
super(
162
'DefaultOptions' =>
163
{
164
'SSL' => self.ssl,
165
'RHOST' => self.rhost,
166
'RPORT' => self.rport
167
}
168
)
169
end
170
171
# Returns the found cracked MD5 hash
172
#
173
# @param md5_hash [String] The MD5 hash to lookup
174
# @param db [String] The specific database to check against
175
# @return [String] Found cracked MD5 hash
176
def lookup(md5_hash, db)
177
res = send_request_cgi({
178
'uri' => self.target_uri,
179
'method' => 'GET',
180
'vars_get' => {'database' => db, 'hash' => md5_hash}
181
})
182
get_json_result(res)
183
end
184
185
private
186
187
# Parses the cracked result from a JSON input
188
# @param res [Rex::Proto::Http::Response] The Rex HTTP response
189
# @return [String] Found cracked MD5 hash
190
def get_json_result(res)
191
result = ''
192
193
# Hmm, no proper response :-(
194
return result unless res && res.code == 200
195
196
begin
197
json = JSON.parse(res.body)
198
result = json['result'] if json['status']
199
rescue JSON::ParserError
200
# No json?
201
end
202
203
result
204
end
205
206
end
207
208
# This class parses the user-supplied options (inputs)
209
class OptsConsole
210
211
# The databases supported by md5cracker.org
212
# The hash keys (symbols) are used as choices for the user, the hash values are the original
213
# database values that md5cracker.org will recognize
214
DATABASES =
215
{
216
:all => nil, # This is shifted before being passed to Md5Lookup
217
:authsecu => 'authsecu',
218
:i337 => 'i337.net',
219
:md5_my_addr => 'md5.my-addr.com',
220
:md5_net => 'md5.net',
221
:md5crack => 'md5crack',
222
:md5cracker => 'md5cracker.org',
223
:md5decryption => 'md5decryption.com',
224
:md5online => 'md5online.net',
225
:md5pass => 'md5pass',
226
:netmd5crack => 'netmd5crack',
227
:tmto => 'tmto'
228
}
229
230
# The default file path to save the results to
231
DEFAULT_OUTFILE = 'md5_results.txt'
232
233
# Returns the normalized user inputs
234
#
235
# @param args [Array] This should be Ruby's ARGV
236
# @raise [OptionParser::MissingArgument] Missing arguments
237
# @return [Hash] The normalized options
238
def self.parse(args)
239
parser, options = get_parsed_options
240
241
# Set the optional datation argument (--database)
242
unless options[:databases]
243
options[:databases] = get_database_names
244
end
245
246
# Set the optional output argument (--out)
247
unless options[:outfile]
248
options[:outfile] = DEFAULT_OUTFILE
249
end
250
251
# Now let's parse it
252
# This may raise OptionParser::InvalidOption
253
parser.parse!(args)
254
255
# Final checks
256
if options.empty?
257
raise OptionParser::MissingArgument, 'No options set, try -h for usage'
258
elsif options[:input].blank?
259
raise OptionParser::MissingArgument, '-i is a required argument'
260
end
261
262
options
263
end
264
265
private
266
267
# Returns the parsed options from ARGV
268
#
269
# raise [OptionParser::InvalidOption] Invalid option found
270
# @return [OptionParser, Hash] The OptionParser object and an hash containing the options
271
def self.get_parsed_options
272
options = {}
273
parser = OptionParser.new do |opt|
274
opt.banner = "Usage: #{__FILE__} [options]"
275
opt.separator ''
276
opt.separator 'Specific options:'
277
278
opt.on('-i', '--input <file>',
279
'The file that contains all the MD5 hashes (one line per hash)') do |v|
280
if v && !::File.exist?(v)
281
raise OptionParser::InvalidOption, "Invalid input file: #{v}"
282
end
283
284
options[:input] = v
285
end
286
287
opt.on('-d','--databases <names>',
288
"(Optional) Select databases: #{get_database_symbols * ", "} (Default=all)") do |v|
289
options[:databases] = extract_db_names(v)
290
end
291
292
opt.on('-o', '--out <filepath>',
293
"(Optional) Save the results to a file (Default=#{DEFAULT_OUTFILE})") do |v|
294
options[:outfile] = v
295
end
296
297
opt.on_tail('-h', '--help', 'Show this message') do
298
$stdout.puts opt
299
exit
300
end
301
end
302
return parser, options
303
end
304
305
# Returns the actual database names based on what the user wants
306
#
307
# @param list [String] A list of user-supplied database names
308
# @return [Array<String>] All the matched database names
309
def self.extract_db_names(list)
310
new_db_list = []
311
312
list_copy = list.split(',')
313
314
if list_copy.include?('all')
315
return get_database_names
316
end
317
318
list_copy.each do |item|
319
item = item.strip.to_sym
320
new_db_list << DATABASES[item] if DATABASES[item]
321
end
322
323
new_db_list
324
end
325
326
# Returns a list of all of the supported database symbols
327
#
328
# @return [Array<Symbol>] Database symbols
329
def self.get_database_symbols
330
DATABASES.keys
331
end
332
333
# Returns a list of all the original database values recognized by md5cracker.org
334
#
335
# @return [Array<String>] Original database values
336
def self.get_database_names
337
new_db_list = DATABASES.values
338
new_db_list.shift #Get rid of the 'all' option
339
return new_db_list
340
end
341
end
342
343
# This class decides how this process works
344
class Driver
345
346
def initialize
347
begin
348
@opts = OptsConsole.parse(ARGV)
349
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
350
print_error("#{e.message} (please see -h)")
351
exit
352
end
353
354
@output_handle = nil
355
begin
356
@output_handle = ::File.new(@opts[:outfile], 'wb')
357
rescue
358
# Not end of the world, but if this happens we won't be able to save the results.
359
# The user will just have to copy and paste from the screen.
360
print_error("Unable to create file handle, results will not be saved to #{@opts[:output]}")
361
end
362
end
363
364
# Main function
365
#
366
# @return [void]
367
def run
368
input = @opts[:input]
369
dbs = @opts[:databases]
370
371
disclaimer = Md5LookupUtility::Disclaimer.new
372
373
unless disclaimer.has_waiver?
374
disclaimer.ack
375
disclaimer.save_waiver
376
end
377
378
get_hash_results(input, dbs) do |result|
379
original_hash = result[:hash]
380
cracked_hash = result[:cracked_hash]
381
credit_db = result[:credit]
382
print_status("Found: #{original_hash} = #{cracked_hash} (from #{credit_db})")
383
save_result(result) if @output_handle
384
end
385
end
386
387
# Cleans up the output file handler if exists
388
#
389
# @return [void]
390
def cleanup
391
@output_handle.close if @output_handle
392
end
393
394
private
395
396
# Saves the MD5 result to file
397
#
398
# @param result [Hash] The result that contains the MD5 information
399
# @option result :hash [String] The original MD5 hash
400
# @option result :cracked_hash [String] The cracked MD5 hash
401
# @return [void]
402
def save_result(result)
403
@output_handle.puts "#{result[:hash]} = #{result[:cracked_hash]}"
404
end
405
406
# Returns the hash results by actually invoking Md5Lookup
407
#
408
# @param input [String] The path of the input file (MD5 hashes)
409
# @yield [result] Gives a hash as the found result
410
# @return [void]
411
def get_hash_results(input, dbs)
412
search_engine = Md5LookupUtility::Md5Lookup.new
413
extract_hashes(input) do |hash|
414
dbs.each do |db|
415
cracked_hash = search_engine.lookup(hash, db)
416
unless cracked_hash.empty?
417
result = { :hash => hash, :cracked_hash => cracked_hash, :credit => db }
418
yield result
419
end
420
421
# Awright, we already found one cracked, we don't need to keep looking,
422
# Let's move on to the next hash!
423
break unless cracked_hash.empty?
424
end
425
end
426
end
427
428
# Extracts all the MD5 hashes one by one
429
#
430
# @param input_file [String] The path of the input file (MD5 hashes)
431
# @yield [hash] The original MD5 hash
432
# @return [void]
433
def extract_hashes(input_file)
434
::File.open(input_file, 'rb') do |f|
435
f.each_line do |hash|
436
next unless is_md5_format?(hash)
437
yield hash.strip # Make sure no newlines
438
end
439
end
440
end
441
442
# Checks if the hash format is MD5 or not
443
#
444
# @param md5_hash [String] The MD5 hash (hex)
445
# @return [TrueClass/FalseClass] True if the format is valid, otherwise false
446
def is_md5_format?(md5_hash)
447
(md5_hash =~ /^[a-f0-9]{32}$/i) ? true : false
448
end
449
end
450
451
end
452
453
#
454
# main
455
#
456
if __FILE__ == $PROGRAM_NAME
457
driver = Md5LookupUtility::Driver.new
458
begin
459
driver.run
460
rescue Interrupt
461
$stdout.puts
462
$stdout.puts "Good bye"
463
ensure
464
driver.cleanup # Properly close resources
465
end
466
end
467
468