Merge branch 'feature/MSP-9689/jtr_cracker' into staging/electro-release
commit
b606448976
2
Gemfile
2
Gemfile
|
@ -19,6 +19,8 @@ group :development do
|
|||
gem 'redcarpet'
|
||||
# generating documentation
|
||||
gem 'yard'
|
||||
# for development and testing purposes
|
||||
gem 'pry'
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
|
|
|
@ -54,6 +54,7 @@ GEM
|
|||
arel (3.0.3)
|
||||
bcrypt (3.1.7)
|
||||
builder (3.0.4)
|
||||
coderay (1.1.0)
|
||||
diff-lcs (1.2.5)
|
||||
erubis (2.7.0)
|
||||
factory_girl (4.4.0)
|
||||
|
@ -75,6 +76,7 @@ GEM
|
|||
activesupport
|
||||
metasploit-model (>= 0.24.1.pre.semantic.pre.versioning.pre.2.pre.0, < 0.25)
|
||||
pg
|
||||
method_source (0.8.2)
|
||||
mini_portile (0.6.0)
|
||||
msgpack (0.5.8)
|
||||
multi_json (1.0.4)
|
||||
|
@ -84,6 +86,10 @@ GEM
|
|||
packetfu (1.1.9)
|
||||
pcaprub (0.11.3)
|
||||
pg (0.17.1)
|
||||
pry (0.9.12.6)
|
||||
coderay (~> 1.0)
|
||||
method_source (~> 0.8)
|
||||
slop (~> 3.4)
|
||||
rack (1.4.5)
|
||||
rack-cache (1.2)
|
||||
rack (>= 0.4)
|
||||
|
@ -128,6 +134,7 @@ GEM
|
|||
multi_json (~> 1.0.3)
|
||||
simplecov-html (~> 0.5.3)
|
||||
simplecov-html (0.5.3)
|
||||
slop (3.5.0)
|
||||
sprockets (2.2.2)
|
||||
hike (~> 1.2)
|
||||
multi_json (~> 1.0)
|
||||
|
@ -154,6 +161,7 @@ DEPENDENCIES
|
|||
network_interface (~> 0.0.1)
|
||||
pcaprub
|
||||
pg (>= 0.11)
|
||||
pry
|
||||
rake (>= 10.0.0)
|
||||
redcarpet
|
||||
rspec (>= 2.12)
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
require 'metasploit/framework/file_path_validator'
|
||||
require 'metasploit/framework/executable_path_validator'
|
|
@ -0,0 +1,16 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
# This is a ActiveModel custom validator that assumes the attribute
|
||||
# is supposed to be the path to a regular file. It checks whether the
|
||||
# file exists and whether or not it is an executable file.
|
||||
class ExecutablePathValidator < ActiveModel::EachValidator
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
unless ::File.executable? value
|
||||
record.errors[attribute] << (options[:message] || "is not a valid path to an executable file")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
# This is a ActiveModel custom validator that assumes the attribute
|
||||
# is supposed to be the path to a regular file. It checks whether the
|
||||
# file exists and whether or not it is a regular file.
|
||||
class FilePathValidator < ActiveModel::EachValidator
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
unless ::File.file? value
|
||||
record.errors[attribute] << (options[:message] || "is not a valid path to a regular file")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,261 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module JtR
|
||||
|
||||
class JohnNotFoundError < StandardError
|
||||
end
|
||||
|
||||
class Cracker
|
||||
include ActiveModel::Validations
|
||||
|
||||
# @!attribute config
|
||||
# @return [String] The path to an optional config file for John to use
|
||||
attr_accessor :config
|
||||
|
||||
# @!attribute format
|
||||
# @return [String] The hash format to try
|
||||
attr_accessor :format
|
||||
|
||||
# @!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 john_path
|
||||
# This attribute allows the user to specify a john binary to use.
|
||||
# If not supplied, the Cracker will search the PATH for a suitable john binary
|
||||
# and finally fall back to the pre-compiled versions shipped with Metasploit.
|
||||
#
|
||||
# @return [String] The file path to an alternative John binary to use
|
||||
attr_accessor :john_path
|
||||
|
||||
# @!attribute max_runtime
|
||||
# @return [Fixnum] An optional maximum duration of the cracking attempt in seconds
|
||||
attr_accessor :max_runtime
|
||||
|
||||
# @!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
|
||||
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 :hash_path, :'Metasploit::Framework::File_path' => true, if: 'hash_path.present?'
|
||||
|
||||
validates :john_path, :'Metasploit::Framework::Executable_path' => true, if: 'john_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 :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 follows a decision tree to determine the path
|
||||
# to the John the Ripper 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 john_path && ::File.file?(john_path)
|
||||
bin_path = john_path
|
||||
else
|
||||
# Look in the Environment PATH for the john binary
|
||||
path = Rex::FileUtils.find_full_path("john") ||
|
||||
Rex::FileUtils.find_full_path("john.exe")
|
||||
|
||||
if path && ::File.file?(path)
|
||||
bin_path = path
|
||||
else
|
||||
# If we can't find john anywhere else, look at our precompiled binaries
|
||||
bin_path = select_shipped_binary
|
||||
end
|
||||
end
|
||||
raise JohnNotFoundError, 'No suitable John binary was found on the system' if bin_path.blank?
|
||||
bin_path
|
||||
end
|
||||
|
||||
# This method runs the command from {#crack_command} and yields each line of output.
|
||||
#
|
||||
# @yield [String] a line of output from the john command
|
||||
# @return [void]
|
||||
def crack
|
||||
::IO.popen(crack_command, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
yield line
|
||||
end
|
||||
end
|
||||
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 [JohnNotFoundError] if a suitable John binary was never found
|
||||
# @return [Array] An array set up for {::IO.popen} to use
|
||||
def crack_command
|
||||
cmd_string = binary_path
|
||||
cmd = [ cmd_string, '--session=' + john_session_id, '--nolog', '--dupe-suppression' ]
|
||||
|
||||
if config.present?
|
||||
cmd << ( "--config=" + config )
|
||||
end
|
||||
|
||||
if pot.present?
|
||||
cmd << ( "--pot=" + pot )
|
||||
else
|
||||
cmd << ( "--pot=" + john_pot_file)
|
||||
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
|
||||
|
||||
cmd << hash_path
|
||||
end
|
||||
|
||||
# This runs the show command in john and yields cracked passwords.
|
||||
#
|
||||
# @yield [String] the output lines from the command
|
||||
# @return [void]
|
||||
def each_cracked_password
|
||||
::IO.popen(show_command, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
yield line
|
||||
end
|
||||
end
|
||||
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 John.
|
||||
# It allows us to dinstiguish between cracking sessions.
|
||||
#
|
||||
# @ return [String] the Session ID to use
|
||||
def john_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
|
||||
cmd = [cmd_string, "--show", "--pot=#{pot_file}", "--format=#{format}" ]
|
||||
|
||||
if config
|
||||
cmd << "--config=#{config}"
|
||||
end
|
||||
|
||||
cmd << hash_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This method tries to identify the correct version of the pre-shipped
|
||||
# JtR binaries to use based on the platform.
|
||||
#
|
||||
# @return [NilClass] if the correct bianry could not be determined
|
||||
# @return [String] the path to the selected binary
|
||||
def select_shipped_binary
|
||||
cpuinfo_base = ::File.join(Msf::Config.data_directory, "cpuinfo")
|
||||
runpath = nil
|
||||
if File.directory?(cpuinfo_base)
|
||||
data = nil
|
||||
|
||||
case ::RUBY_PLATFORM
|
||||
when /mingw|cygwin|mswin/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.exe"
|
||||
if File.exists?(fname) and File.executable?(fname)
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /sse2/
|
||||
run_path ||= "run.win32.sse2/john.exe"
|
||||
when /mmx/
|
||||
run_path ||= "run.win32.mmx/john.exe"
|
||||
else
|
||||
run_path ||= "run.win32.any/john.exe"
|
||||
end
|
||||
when /x86_64-linux/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.ia64.bin"
|
||||
if File.exists? fname
|
||||
::FileUtils.chmod(0755, fname) rescue nil
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /mmx/
|
||||
run_path ||= "run.linux.x64.mmx/john"
|
||||
else
|
||||
run_path ||= "run.linux.x86.any/john"
|
||||
end
|
||||
when /i[\d]86-linux/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.ia32.bin"
|
||||
if File.exists? fname
|
||||
::FileUtils.chmod(0755, fname) rescue nil
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /sse2/
|
||||
run_path ||= "run.linux.x86.sse2/john"
|
||||
when /mmx/
|
||||
run_path ||= "run.linux.x86.mmx/john"
|
||||
else
|
||||
run_path ||= "run.linux.x86.any/john"
|
||||
end
|
||||
end
|
||||
end
|
||||
runpath
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module JtR
|
||||
|
||||
# This class is the generic Exception raised by a {Wordlist} when
|
||||
# it fails validation. It rolls up all validation errors into a
|
||||
# single exception so that all errors can be dealt with at once.
|
||||
class InvalidWordlist < StandardError
|
||||
attr_reader :model
|
||||
|
||||
def initialize(model)
|
||||
@model = model
|
||||
|
||||
errors = @model.errors.full_messages.join(', ')
|
||||
super(errors)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,429 @@
|
|||
require 'metasploit/framework/jtr/invalid_wordlist'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module JtR
|
||||
|
||||
class Wordlist
|
||||
include ActiveModel::Validations
|
||||
|
||||
# A mapping of the mutation substitution rules
|
||||
MUTATIONS = {
|
||||
'@' => 'a',
|
||||
'0' => 'o',
|
||||
'3' => 'e',
|
||||
'$' => 's',
|
||||
'7' => 't',
|
||||
'1' => 'l',
|
||||
'5' => 's'
|
||||
}
|
||||
|
||||
# @!attribute appenders
|
||||
# @return [Array] an array of strings to append to each word
|
||||
attr_accessor :appenders
|
||||
|
||||
# @!attribute custom_wordlist
|
||||
# @return [String] the path to a custom wordlist file to include
|
||||
attr_accessor :custom_wordlist
|
||||
|
||||
# @!attribute mutate
|
||||
# @return [TrueClass] if you want each word mutated as it is added
|
||||
# @return [FalseClass] if you do not want each word mutated
|
||||
attr_accessor :mutate
|
||||
|
||||
# @!attribute prependers
|
||||
# @return [Array] an array of strings to prepend to each word
|
||||
attr_accessor :prependers
|
||||
|
||||
# @!attribute use_common_root
|
||||
# @return [TrueClass] if you want to use the common root words wordlist
|
||||
# @return [FalseClass] if you do not want to use the common root words wordlist
|
||||
attr_accessor :use_common_root
|
||||
|
||||
# @!attribute use_creds
|
||||
# @return [TrueClass] if you want to seed the wordlist with existing credential data from the database
|
||||
# @return [FalseClass] if you do not want to seed the wordlist with existing credential data from the database
|
||||
attr_accessor :use_creds
|
||||
|
||||
# @!attribute use_db_info
|
||||
# @return [TrueClass] if you want to seed the wordlist with looted database names and schemas
|
||||
# @return [FalseClass] if you do not want to seed the wordlist with looted database names and schemas
|
||||
attr_accessor :use_db_info
|
||||
|
||||
# @!attribute use_default_wordlist
|
||||
# @return [TrueClass] if you want to use the default wordlist
|
||||
# @return [FalseClass] if you do not want to use the default wordlist
|
||||
attr_accessor :use_default_wordlist
|
||||
|
||||
# @!attribute use_hostnames
|
||||
# @return [TrueClass] if you want to seed the wordlist with existing hostnames from the database
|
||||
# @return [FalseClass] if you do not want to seed the wordlist with existing hostnames from the database
|
||||
attr_accessor :use_hostnames
|
||||
|
||||
# @!attribute workspace
|
||||
# @return [Mdm::Workspace] the workspace this cracker is for.
|
||||
attr_accessor :workspace
|
||||
|
||||
validates :custom_wordlist, :'Metasploit::Framework::File_path' => true, if: 'custom_wordlist.present?'
|
||||
|
||||
validates :mutate,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
|
||||
validates :use_common_root,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
validates :use_creds,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
validates :use_db_info,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
validates :use_default_wordlist,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
validates :use_hostnames,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
validates :workspace,
|
||||
presence: true
|
||||
|
||||
# @param attributes [Hash{Symbol => String,nil}]
|
||||
def initialize(attributes={})
|
||||
attributes.each do |attribute, value|
|
||||
public_send("#{attribute}=", value)
|
||||
end
|
||||
@appenders ||= []
|
||||
@prependers ||= []
|
||||
end
|
||||
|
||||
# This method takes a word, and appends each word from the appenders list
|
||||
# and yields the new words.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_appended_word(word='')
|
||||
yield word
|
||||
appenders.each do |suffix|
|
||||
yield "#{word}#{suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
# This method checks all the attributes set on the object and calls
|
||||
# the appropriate enumerators for each option and yields the results back
|
||||
# up the call-chain.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_base_word
|
||||
# Make sure are attributes are all valid first!
|
||||
valid!
|
||||
|
||||
# Yield the expanded form of each line of the custom wordlist if one was given
|
||||
if custom_wordlist.present?
|
||||
each_custom_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
# Yield each word from the common root words list if it was selected
|
||||
if use_common_root
|
||||
each_root_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
# If the user has selected use_creds we yield each password, username, and realm name
|
||||
# that currently exists in the database.
|
||||
if use_creds
|
||||
each_cred_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
if use_db_info
|
||||
each_database_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
if use_default_wordlist
|
||||
each_default_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
if use_hostnames
|
||||
each_hostname_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# This method searches all saved Credentials in the database
|
||||
# and yields all passwords, usernames, and realm names it finds.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_cred_word
|
||||
# We don't want all Private types here. Only Passwords make sense for inclusion in the wordlist.
|
||||
Metasploit::Credential::Password.all.each do |password|
|
||||
yield password.data
|
||||
end
|
||||
|
||||
Metasploit::Credential::Public.all.each do |public|
|
||||
yield public.username
|
||||
end
|
||||
|
||||
Metasploit::Credential::Realm.all.each do |realm|
|
||||
yield realm.value
|
||||
end
|
||||
end
|
||||
|
||||
# This method reads the file provided as custom_wordlist and yields
|
||||
# the expanded form of each word in the list.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_custom_word
|
||||
::File.open(custom_wordlist, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
expanded_words(line) do |word|
|
||||
yield word
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method searches the notes in the current workspace
|
||||
# for DB instance names, database names, table names, and
|
||||
# column names gathered from live database servers. It yields
|
||||
# each one that it finds.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_database_word
|
||||
# Yield database, table and column names from any looted database schemas
|
||||
workspace.notes.where('ntype like ?', '%.schema%').each do |note|
|
||||
expanded_words(note.data['DBName']) do |word|
|
||||
yield word
|
||||
end
|
||||
|
||||
note.data['Tables'].each do |table|
|
||||
expanded_words(table['TableName']) do |word|
|
||||
yield word
|
||||
end
|
||||
|
||||
table['Columns'].each do |column|
|
||||
expanded_words(column['ColumnName']) do |word|
|
||||
yield word
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Yield any capture MSSQL Instance names
|
||||
workspace.notes.find(:all, :conditions => ['ntype=?', 'mssql.instancename']).each do |note|
|
||||
expanded_words(note.data['InstanceName']) do |word|
|
||||
yield word
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method yields expanded words taken from the default john
|
||||
# wordlist that we ship in the data directory.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_default_word
|
||||
::File.open(default_wordlist_path, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
expanded_words(line) do |word|
|
||||
yield word
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method yields the expanded words out of all the hostnames
|
||||
# found in the current workspace.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_hostname_word
|
||||
workspace.hosts.all.each do |host|
|
||||
unless host.name.nil?
|
||||
expanded_words(host.name) do |word|
|
||||
yield nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method checks to see if the user asked for mutations. If mutations
|
||||
# have been enabled, then it creates all the unique mutations and yields
|
||||
# each result.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_mutated_word(word='')
|
||||
mutants = [ ]
|
||||
|
||||
# Run the mutations only if the option is set
|
||||
if mutate
|
||||
mutants = mutants + mutate_word(word)
|
||||
end
|
||||
|
||||
mutants << word
|
||||
mutants.uniq.each do |mutant|
|
||||
yield mutant
|
||||
end
|
||||
end
|
||||
|
||||
# This method takes a word, and prepends each word from the prependers list
|
||||
# and yields the new words.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_prepended_word(word='')
|
||||
yield word
|
||||
prependers.each do |prefix|
|
||||
yield "#{prefix}#{word}"
|
||||
end
|
||||
end
|
||||
|
||||
# This method reads the common_roots.txt wordlist
|
||||
# expands any words in the list and yields them.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_root_word
|
||||
::File.open(common_root_words_path, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
expanded_words(line) do |word|
|
||||
yield word
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method wraps around all the other enumerators. It processes
|
||||
# all of the options and yields each word generated by the options
|
||||
# selected.
|
||||
#
|
||||
# @yieldparam word [String] the word to write out to the wordlist file
|
||||
# @return [void]
|
||||
def each_word
|
||||
each_base_word do |base_word|
|
||||
each_mutated_word(base_word) do |mutant|
|
||||
each_prepended_word(mutant) do |prepended|
|
||||
yield prepended
|
||||
end
|
||||
|
||||
each_appended_word(mutant) do |appended|
|
||||
yield appended
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method takes a string and splits it on non-word characters
|
||||
# and the underscore. It does this to find likely distinct words
|
||||
# in the string. It then yields each 'word' found this way.
|
||||
#
|
||||
# @param word [String] the string to split apart
|
||||
# @yieldparam expanded [String] the expanded words
|
||||
# @return [void]
|
||||
def expanded_words(word='')
|
||||
word.split(/[\W_]+/).each do |expanded|
|
||||
yield expanded
|
||||
end
|
||||
end
|
||||
|
||||
# This method takes a word and applies various mutation rules to that word
|
||||
# and returns an array of all the mutated forms.
|
||||
#
|
||||
# @param word [String] the word to apply the mutations to
|
||||
# @return [Array<String>] An array containing all the mutated forms of the word
|
||||
def mutate_word(word)
|
||||
results = []
|
||||
# Iterate through combinations to create each possible mutation
|
||||
mutation_keys.each do |iteration|
|
||||
next if iteration.flatten.empty?
|
||||
intermediate = word.dup
|
||||
subsititutions = iteration.collect { |key| MUTATIONS[key] }
|
||||
intermediate.tr!(subsititutions.join, iteration.join)
|
||||
results << intermediate
|
||||
end
|
||||
results.flatten.uniq
|
||||
end
|
||||
|
||||
# A getter for a memoized version fo the mutation keys list
|
||||
#
|
||||
# @return [Array<Array>] a 2D array of all mutation combinations
|
||||
def mutation_keys
|
||||
@mutation_keys ||= generate_mutation_keys
|
||||
end
|
||||
|
||||
# This method takes all the options provided and streams the generated wordlist out
|
||||
# to a {Rex::Quickfile} and returns the {Rex::Quickfile}.
|
||||
#
|
||||
# @return [Rex::Quickfile] The {Rex::Quickfile} object that the wordlist has been written to
|
||||
def to_file
|
||||
valid!
|
||||
wordlist_file = Rex::Quickfile.new("jtrtmp")
|
||||
each_word do |word|
|
||||
wordlist_file.puts word
|
||||
end
|
||||
wordlist_file
|
||||
end
|
||||
|
||||
# Raise an exception if the attributes are not valid.
|
||||
#
|
||||
# @raise [Invalid] if the attributes are not valid on this scanner
|
||||
# @return [void]
|
||||
def valid!
|
||||
unless valid?
|
||||
raise Metasploit::Framework::JtR::InvalidWordlist.new(self)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
private
|
||||
|
||||
# This method returns the path to the common_roots.txt wordlist
|
||||
#
|
||||
# @return [String] the file path to the common_roots.txt file
|
||||
def common_root_words_path
|
||||
::File.join(Msf::Config.data_directory, 'john', 'wordlists', 'common_roots.txt')
|
||||
end
|
||||
|
||||
# This method returns the path to the passwords.lst wordlist
|
||||
#
|
||||
# @return [String] the file path to the passwords.lst file
|
||||
def default_wordlist_path
|
||||
::File.join(Msf::Config.data_directory, 'john', 'wordlists', 'password.lst')
|
||||
end
|
||||
|
||||
def generate_mutation_keys
|
||||
iterations = MUTATIONS.keys.dup
|
||||
|
||||
# Find PowerSet of all possible mutation combinations
|
||||
iterations.inject([[]]) do |accumulator,mutation_key|
|
||||
power_set = []
|
||||
accumulator.each do |i|
|
||||
power_set << i
|
||||
power_set << i+[mutation_key]
|
||||
end
|
||||
power_set
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,7 +2,8 @@
|
|||
require 'open3'
|
||||
require 'fileutils'
|
||||
require 'rex/proto/ntlm/crypt'
|
||||
|
||||
require 'metasploit/framework/jtr/cracker'
|
||||
require 'metasploit/framework/jtr/wordlist'
|
||||
|
||||
|
||||
module Msf
|
||||
|
@ -24,241 +25,20 @@ module Auxiliary::JohnTheRipper
|
|||
|
||||
register_options(
|
||||
[
|
||||
OptPath.new('JOHN_BASE', [false, 'The directory containing John the Ripper (src, run, doc)']),
|
||||
OptPath.new('JOHN_PATH', [false, 'The absolute path to the John the Ripper executable']),
|
||||
OptPath.new('Wordlist', [false, 'The path to an optional Wordlist']),
|
||||
OptBool.new('Munge',[false, 'Munge the Wordlist (Slower)', false])
|
||||
OptPath.new('CONFIG', [false, 'The path to a John config file to use instead of the default']),
|
||||
OptPath.new('CUSTOM_WORDLIST', [false, 'The path to an optional custom wordlist']),
|
||||
OptInt.new('ITERATION_TIMOUT', [false, 'The max-run-time for each iteration of cracking']),
|
||||
OptPath.new('JOHN_PATH', [false, 'The absolute path to the John the Ripper executable']),
|
||||
OptBool.new('MUTATE', [false, 'Apply common mutations to the Wordlist (SLOW)', false]),
|
||||
OptPath.new('POT', [false, 'The path to a John POT file to use instead of the default']),
|
||||
OptBool.new('USE_CREDS', [false, 'Use existing credential data saved in the database', true]),
|
||||
OptBool.new('USE_DB_INFO', [false, 'Use looted database schema info to seed the wordlist', true]),
|
||||
OptBool.new('USE_DEFAULT_WORDLIST', [false, 'Use the default metasploit wordlist', true]),
|
||||
OptBool.new['USE_HOSTNAMES', [false, 'Seed the wordlist with hostnames from the workspace', true]],
|
||||
OptBool.new('USE_ROOT_WORDS', [false, 'Use the Common Root Words Wordlist', true])
|
||||
], Msf::Auxiliary::JohnTheRipper
|
||||
)
|
||||
|
||||
@run_path = nil
|
||||
@john_path = ::File.join(Msf::Config.data_directory, "john")
|
||||
|
||||
autodetect_platform
|
||||
end
|
||||
|
||||
# @return [String] the run path instance variable if the platform is detectable, nil otherwise.
|
||||
def autodetect_platform
|
||||
return @run_path if @run_path
|
||||
cpuinfo_base = ::File.join(Msf::Config.data_directory, "cpuinfo")
|
||||
if File.directory?(cpuinfo_base)
|
||||
data = nil
|
||||
|
||||
case ::RUBY_PLATFORM
|
||||
when /mingw|cygwin|mswin/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.exe"
|
||||
if File.exists?(fname) and File.executable?(fname)
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /sse2/
|
||||
@run_path ||= "run.win32.sse2/john.exe"
|
||||
when /mmx/
|
||||
@run_path ||= "run.win32.mmx/john.exe"
|
||||
else
|
||||
@run_path ||= "run.win32.any/john.exe"
|
||||
end
|
||||
when /x86_64-linux/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.ia64.bin"
|
||||
if File.exists? fname
|
||||
::FileUtils.chmod(0755, fname) rescue nil
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /mmx/
|
||||
@run_path ||= "run.linux.x64.mmx/john"
|
||||
else
|
||||
@run_path ||= "run.linux.x86.any/john"
|
||||
end
|
||||
when /i[\d]86-linux/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.ia32.bin"
|
||||
if File.exists? fname
|
||||
::FileUtils.chmod(0755, fname) rescue nil
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /sse2/
|
||||
@run_path ||= "run.linux.x86.sse2/john"
|
||||
when /mmx/
|
||||
@run_path ||= "run.linux.x86.mmx/john"
|
||||
else
|
||||
@run_path ||= "run.linux.x86.any/john"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return @run_path
|
||||
end
|
||||
|
||||
def john_session_id
|
||||
@session_id ||= ::Rex::Text.rand_text_alphanumeric(8)
|
||||
end
|
||||
|
||||
def john_pot_file
|
||||
::File.join( ::Msf::Config.config_directory, "john.pot" )
|
||||
end
|
||||
|
||||
def john_cracked_passwords
|
||||
ret = {}
|
||||
return ret if not ::File.exist?(john_pot_file)
|
||||
::File.open(john_pot_file, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
hash,clear = line.sub(/\r?\n$/, '').split(",", 2)
|
||||
ret[hash] = clear
|
||||
end
|
||||
end
|
||||
ret
|
||||
end
|
||||
|
||||
def john_show_passwords(hfile, format=nil)
|
||||
res = {:cracked => 0, :uncracked => 0, :users => {} }
|
||||
|
||||
john_command = john_binary_path
|
||||
|
||||
if john_command.nil?
|
||||
print_error("John the Ripper executable not found")
|
||||
return res
|
||||
end
|
||||
|
||||
pot = john_pot_file
|
||||
conf = ::File.join(john_base_path, "confs", "john.conf")
|
||||
|
||||
cmd = [ john_command, "--show", "--conf=#{conf}", "--pot=#{pot}", hfile]
|
||||
|
||||
if format
|
||||
cmd << "--format=" + format
|
||||
end
|
||||
|
||||
if RUBY_VERSION =~ /^1\.8\./
|
||||
cmd = cmd.join(" ")
|
||||
end
|
||||
|
||||
::IO.popen(cmd, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
line.chomp!
|
||||
print_status(line)
|
||||
if line =~ /(\d+) password hash(es)* cracked, (\d+) left/m
|
||||
res[:cracked] = $1.to_i
|
||||
res[:uncracked] = $2.to_i
|
||||
end
|
||||
|
||||
# XXX: If the password had : characters in it, we're screwed
|
||||
|
||||
bits = line.split(':', -1)
|
||||
|
||||
# Skip blank passwords
|
||||
next if not bits[2]
|
||||
|
||||
if (format== 'lm' or format == 'nt')
|
||||
res[ :users ][ bits[0] ] = bits[1]
|
||||
else
|
||||
bits.last.chomp!
|
||||
res[ :users ][ bits[0] ] = bits.drop(1)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
def john_unshadow(passwd_file,shadow_file)
|
||||
|
||||
retval=""
|
||||
|
||||
john_command = john_binary_path
|
||||
|
||||
if john_command.nil?
|
||||
print_error("John the Ripper executable not found")
|
||||
return nil
|
||||
end
|
||||
|
||||
if File.exists?(passwd_file)
|
||||
unless File.readable?(passwd_file)
|
||||
print_error("We do not have permission to read #{passwd_file}")
|
||||
return nil
|
||||
end
|
||||
else
|
||||
print_error("File does not exist: #{passwd_file}")
|
||||
return nil
|
||||
end
|
||||
|
||||
if File.exists?(shadow_file)
|
||||
unless File.readable?(shadow_file)
|
||||
print_error("We do not have permission to read #{shadow_file}")
|
||||
return nil
|
||||
end
|
||||
else
|
||||
print_error("File does not exist: #{shadow_file}")
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
cmd = [ john_command.gsub(/john$/, "unshadow"), passwd_file , shadow_file ]
|
||||
|
||||
if RUBY_VERSION =~ /^1\.8\./
|
||||
cmd = cmd.join(" ")
|
||||
end
|
||||
::IO.popen(cmd, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
retval << line
|
||||
end
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
def john_wordlist_path
|
||||
# We ship it under wordlists/
|
||||
path = ::File.join(john_base_path, "wordlists", "password.lst")
|
||||
# magnumripper/JohnTheRipper repo keeps it under run/
|
||||
unless ::File.file? path
|
||||
path = ::File.join(john_base_path, "run", "password.lst")
|
||||
end
|
||||
|
||||
path
|
||||
end
|
||||
|
||||
def john_binary_path
|
||||
path = nil
|
||||
if datastore['JOHN_PATH'] and ::File.file?(datastore['JOHN_PATH'])
|
||||
path = datastore['JOHN_PATH']
|
||||
::FileUtils.chmod(0755, path) rescue nil
|
||||
return path
|
||||
end
|
||||
|
||||
if not @run_path
|
||||
if ::RUBY_PLATFORM =~ /mingw|cygwin|mswin/
|
||||
::File.join(john_base_path, "john.exe")
|
||||
else
|
||||
path = ::File.join(john_base_path, "john")
|
||||
::FileUtils.chmod(0755, path) rescue nil
|
||||
end
|
||||
else
|
||||
path = ::File.join(john_base_path, @run_path)
|
||||
::FileUtils.chmod(0755, path) rescue nil
|
||||
end
|
||||
|
||||
if path and ::File.exists?(path)
|
||||
return path
|
||||
end
|
||||
|
||||
path = Rex::FileUtils.find_full_path("john") ||
|
||||
Rex::FileUtils.find_full_path("john.exe")
|
||||
end
|
||||
|
||||
def john_base_path
|
||||
if datastore['JOHN_BASE'] and ::File.directory?(datastore['JOHN_BASE'])
|
||||
return datastore['JOHN_BASE']
|
||||
end
|
||||
if datastore['JOHN_PATH'] and ::File.file?(datastore['JOHN_PATH'])
|
||||
return ::File.dirname( datastore['JOHN_PATH'] )
|
||||
end
|
||||
@john_path
|
||||
end
|
||||
|
||||
def john_expand_word(str)
|
||||
res = [str]
|
||||
str.split(/\W+/) {|w| res << w }
|
||||
res.uniq
|
||||
end
|
||||
|
||||
def john_lm_upper_to_ntlm(pwd, hash)
|
||||
|
@ -273,179 +53,42 @@ module Auxiliary::JohnTheRipper
|
|||
end
|
||||
|
||||
|
||||
def john_crack(hfile, opts={})
|
||||
|
||||
res = {:cracked => 0, :uncracked => 0, :users => {} }
|
||||
|
||||
john_command = john_binary_path
|
||||
|
||||
if john_command.nil?
|
||||
print_error("John the Ripper executable not found")
|
||||
return nil
|
||||
end
|
||||
|
||||
# Don't bother making a log file, we'd just have to rm it when we're
|
||||
# done anyway.
|
||||
cmd = [ john_command, "--session=" + john_session_id, "--nolog"]
|
||||
|
||||
if opts[:conf]
|
||||
cmd << ( "--conf=" + opts[:conf] )
|
||||
else
|
||||
cmd << ( "--conf=" + ::File.join(john_base_path, "confs", "john.conf") )
|
||||
end
|
||||
|
||||
if opts[:pot]
|
||||
cmd << ( "--pot=" + opts[:pot] )
|
||||
else
|
||||
cmd << ( "--pot=" + john_pot_file )
|
||||
end
|
||||
|
||||
if opts[:format]
|
||||
cmd << ( "--format=" + opts[:format] )
|
||||
end
|
||||
|
||||
if opts[:wordlist]
|
||||
cmd << ( "--wordlist=" + opts[:wordlist] )
|
||||
end
|
||||
|
||||
if opts[:incremental]
|
||||
cmd << ( "--incremental=" + opts[:incremental] )
|
||||
end
|
||||
|
||||
if opts[:single]
|
||||
cmd << ( "--single=" + opts[:single] )
|
||||
end
|
||||
|
||||
if opts[:rules]
|
||||
cmd << ( "--rules=" + opts[:rules] )
|
||||
end
|
||||
|
||||
cmd << hfile
|
||||
|
||||
if RUBY_VERSION =~ /^1\.8\./
|
||||
cmd = cmd.join(" ")
|
||||
end
|
||||
|
||||
::IO.popen(cmd, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
print_status("Output: #{line.strip}")
|
||||
end
|
||||
end
|
||||
|
||||
res
|
||||
# This method creates a new {Metasploit::Framework::JtR::Cracker} and populates
|
||||
# some of the attributes based on the module datastore options.
|
||||
#
|
||||
# @return [nilClass] if there is no active framework db connection
|
||||
# @return [Metasploit::Framework::JtR::Cracker] if it successfully creates a JtR Cracker object
|
||||
def new_john_cracker
|
||||
return nil unless framework.db.active?
|
||||
Metasploit::Framework::JtR::Cracker.new(
|
||||
config: datastore['CONFIG'],
|
||||
john_path: datastore['JOHN_PATH'],
|
||||
max_runtime: datastore['ITERATION_TIMEOUT'],
|
||||
pot: datastore['POT'],
|
||||
wordlist: datastore['CUSTOM_WORDLIST']
|
||||
)
|
||||
end
|
||||
|
||||
def build_seed
|
||||
|
||||
seed = []
|
||||
#Seed the wordlist with Database , Table, and Instance Names
|
||||
|
||||
count = 0
|
||||
schemas = myworkspace.notes.where('ntype like ?', '%.schema%')
|
||||
unless schemas.nil? or schemas.empty?
|
||||
schemas.each do |anote|
|
||||
seed << anote.data['DBName']
|
||||
count += 1
|
||||
anote.data['Tables'].each do |table|
|
||||
seed << table['TableName']
|
||||
count += 1
|
||||
table['Columns'].each do |column|
|
||||
seed << column['ColumnName']
|
||||
count += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
print_status "Seeding wordlist with DB schema info... #{count} words added"
|
||||
count = 0
|
||||
|
||||
instances = myworkspace.notes.find(:all, :conditions => ['ntype=?', 'mssql.instancename'])
|
||||
unless instances.nil? or instances.empty?
|
||||
instances.each do |anote|
|
||||
seed << anote.data['InstanceName']
|
||||
count += 1
|
||||
end
|
||||
end
|
||||
print_status "Seeding with MSSQL Instance Names....#{count} words added"
|
||||
count = 0
|
||||
|
||||
# Seed the wordlist with usernames, passwords, and hostnames
|
||||
|
||||
myworkspace.hosts.find(:all).each do |o|
|
||||
if o.name
|
||||
seed << john_expand_word( o.name )
|
||||
count += 1
|
||||
end
|
||||
end
|
||||
print_status "Seeding with hostnames....#{count} words added"
|
||||
count = 0
|
||||
|
||||
|
||||
myworkspace.creds.each do |o|
|
||||
if o.user
|
||||
seed << john_expand_word( o.user )
|
||||
count +=1
|
||||
end
|
||||
if (o.pass and o.ptype !~ /hash/)
|
||||
seed << john_expand_word( o.pass )
|
||||
count += 1
|
||||
end
|
||||
end
|
||||
print_status "Seeding with found credentials....#{count} words added"
|
||||
count = 0
|
||||
|
||||
# Grab any known passwords out of the john.pot file
|
||||
john_cracked_passwords.values do |v|
|
||||
seed << v
|
||||
count += 1
|
||||
end
|
||||
print_status "Seeding with cracked passwords from John....#{count} words added"
|
||||
count = 0
|
||||
|
||||
#Grab the default John Wordlist
|
||||
john = File.open(john_wordlist_path, "rb")
|
||||
john.each_line do |line|
|
||||
seed << line.chomp
|
||||
count += 1
|
||||
end
|
||||
print_status "Seeding with default John wordlist...#{count} words added"
|
||||
count = 0
|
||||
|
||||
if datastore['Wordlist']
|
||||
wordlist= File.open(datastore['Wordlist'], "rb")
|
||||
wordlist.each_line do |line|
|
||||
seed << line.chomp
|
||||
count ==1
|
||||
end
|
||||
print_status "Seeding from user supplied wordlist...#{count} words added"
|
||||
end
|
||||
|
||||
|
||||
|
||||
unless seed.empty?
|
||||
seed.flatten!
|
||||
seed.uniq!
|
||||
if datastore['Munge']
|
||||
mungedseed=[]
|
||||
seed.each do |word|
|
||||
munged = word.gsub(/[sS]/, "$").gsub(/[aA]/,"@").gsub(/[oO]/,"0")
|
||||
mungedseed << munged
|
||||
munged.gsub!(/[eE]/, "3")
|
||||
munged.gsub!(/[tT]/, "7")
|
||||
mungedseed << munged
|
||||
end
|
||||
print_status "Adding #{mungedseed.count} words from munging..."
|
||||
seed << mungedseed
|
||||
seed.flatten!
|
||||
seed.uniq!
|
||||
end
|
||||
end
|
||||
print_status "De-duping the wordlist...."
|
||||
|
||||
print_status("Wordlist Seeded with #{seed.length} words")
|
||||
|
||||
return seed
|
||||
|
||||
# This method instantiates a {Metasploit::Framework::JtR::Wordlist}, writes the data
|
||||
# out to a file and returns the {rex::quickfile} object.
|
||||
#
|
||||
# @return [nilClass] if there is no active framework db connection
|
||||
# @return [Rex::Quickfile] if it successfully wrote the wordlist to a file
|
||||
def wordlist_file
|
||||
return nil unless framework.db.active?
|
||||
wordlist = Metasploit::Framework::JtR::Wordlist.new(
|
||||
custom_wordlist: datastore['CUSTOM_WORDLIST'],
|
||||
mutate: datastore['MUTATE'],
|
||||
pot: datastore['POT'],
|
||||
use_creds: datastore['USE_CREDS'],
|
||||
use_db_info: datastore['USE_DB_INFO'],
|
||||
use_default_wordlist: datastore['USE_DEFAULT_WORDLIST'],
|
||||
use_hostnames: datastore['USE_HOSTNAMES'],
|
||||
use_common_root: datastore['USE_ROOT_WORDS'],
|
||||
workspace: myworkspace
|
||||
)
|
||||
wordlist.to_file
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,5 @@ require 'msf/core/auxiliary/login'
|
|||
require 'msf/core/auxiliary/rservices'
|
||||
require 'msf/core/auxiliary/cisco'
|
||||
require 'msf/core/auxiliary/nmap'
|
||||
require 'msf/core/auxiliary/jtr'
|
||||
require 'msf/core/auxiliary/iax2'
|
||||
require 'msf/core/auxiliary/pii'
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
password
|
||||
root
|
||||
toor
|
|
@ -0,0 +1,3 @@
|
|||
changeme
|
||||
summer123
|
||||
admin
|
|
@ -0,0 +1,249 @@
|
|||
require 'spec_helper'
|
||||
require 'metasploit/framework/jtr/cracker'
|
||||
|
||||
describe Metasploit::Framework::JtR::Cracker do
|
||||
|
||||
subject(:cracker) { described_class.new }
|
||||
let(:john_path) { '/path/to/john' }
|
||||
let(:other_john_path) { '/path/to/other/john' }
|
||||
let(:session_id) { 'Session1' }
|
||||
let(:config) { '/path/to/config.conf' }
|
||||
let(:pot) { '/path/to/john.pot' }
|
||||
let(:other_pot) { '/path/to/other/pot' }
|
||||
let(:wordlist) { '/path/to/wordlist' }
|
||||
let(:hash_path) { '/path/to/hashes' }
|
||||
let(:nt_format) { 'nt' }
|
||||
let(:incremental) { 'Digits5' }
|
||||
let(:rules) { 'Rule34'}
|
||||
let(:max_runtime) { 5000 }
|
||||
|
||||
describe '#binary_path' do
|
||||
|
||||
|
||||
context 'when the user supplied a john_path' do
|
||||
before(:each) do
|
||||
cracker.john_path = john_path
|
||||
end
|
||||
|
||||
it 'returns the manual path if it exists and is a regular file' do
|
||||
expect(::File).to receive(:file?).with(john_path).once.and_return true
|
||||
expect(cracker.binary_path).to eq john_path
|
||||
end
|
||||
|
||||
it 'rejects the manual path if it does not exist or is not a regular file' do
|
||||
expect(::File).to receive(:file?).with(john_path).once.and_return false
|
||||
expect(Rex::FileUtils).to receive(:find_full_path).with('john').and_return other_john_path
|
||||
expect(::File).to receive(:file?).with(other_john_path).once.and_return true
|
||||
expect(cracker.binary_path).to_not eq john_path
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user did not supply a path' do
|
||||
it 'returns the john binary from the PATH if it exists' do
|
||||
expect(Rex::FileUtils).to receive(:find_full_path).and_return john_path
|
||||
expect(::File).to receive(:file?).with(john_path).once.and_return true
|
||||
expect(cracker.binary_path).to eq john_path
|
||||
end
|
||||
|
||||
it 'returns the shipped john binary if it does not exist in the PATH' do
|
||||
expect(Rex::FileUtils).to receive(:find_full_path).twice.and_return nil
|
||||
expect(::File).to receive(:file?).with(nil).once.and_return false
|
||||
expect(cracker).to receive(:select_shipped_binary).and_return other_john_path
|
||||
expect(cracker.binary_path).to eq other_john_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#crack_command' do
|
||||
before(:each) do
|
||||
expect(cracker).to receive(:binary_path).and_return john_path
|
||||
expect(cracker).to receive(:john_session_id).and_return session_id
|
||||
end
|
||||
|
||||
it 'starts with the john binary path' do
|
||||
expect(cracker.crack_command[0]).to eq john_path
|
||||
end
|
||||
|
||||
it 'sets a session id' do
|
||||
expect(cracker.crack_command).to include "--session=#{session_id}"
|
||||
end
|
||||
|
||||
it 'sets the nolog flag' do
|
||||
expect(cracker.crack_command).to include '--nolog'
|
||||
end
|
||||
|
||||
it 'adds a config directive if the user supplied one' do
|
||||
cracker.config = config
|
||||
expect(cracker.crack_command).to include "--config=#{config}"
|
||||
end
|
||||
|
||||
it 'does not use a config directive if not supplied one' do
|
||||
expect(cracker.crack_command).to_not include "--config=#{config}"
|
||||
end
|
||||
|
||||
it 'uses the user supplied john.pot if there is one' do
|
||||
cracker.pot = pot
|
||||
expect(cracker.crack_command).to include "--pot=#{pot}"
|
||||
end
|
||||
|
||||
it 'uses default john.pot if the user did not supply one' do
|
||||
expect(cracker).to receive(:john_pot_file).and_return other_pot
|
||||
expect(cracker.crack_command).to include "--pot=#{other_pot}"
|
||||
end
|
||||
|
||||
it 'uses the user supplied format directive' do
|
||||
cracker.format = nt_format
|
||||
expect(cracker.crack_command).to include "--format=#{nt_format}"
|
||||
end
|
||||
|
||||
it 'uses the user supplied wordlist directive' do
|
||||
cracker.wordlist = wordlist
|
||||
expect(cracker.crack_command).to include "--wordlist=#{wordlist}"
|
||||
end
|
||||
|
||||
it 'uses the user supplied incremental directive' do
|
||||
cracker.incremental = incremental
|
||||
expect(cracker.crack_command).to include "--incremental=#{incremental}"
|
||||
end
|
||||
|
||||
it 'uses the user supplied rules directive' do
|
||||
cracker.rules = rules
|
||||
expect(cracker.crack_command).to include "--rules=#{rules}"
|
||||
end
|
||||
|
||||
it 'uses the user supplied max-run-time' do
|
||||
cracker.max_runtime = max_runtime
|
||||
expect(cracker.crack_command).to include "--max-run-time=#{max_runtime.to_s}"
|
||||
end
|
||||
|
||||
it 'puts the path to the has file at the end' do
|
||||
cracker.hash_path = hash_path
|
||||
expect(cracker.crack_command.last).to eq hash_path
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe '#show_command' do
|
||||
before(:each) do
|
||||
expect(cracker).to receive(:binary_path).and_return john_path
|
||||
end
|
||||
|
||||
it 'starts with the john binary path' do
|
||||
expect(cracker.show_command[0]).to eq john_path
|
||||
end
|
||||
|
||||
it 'has the --show flag' do
|
||||
expect(cracker.show_command).to include '--show'
|
||||
end
|
||||
|
||||
it 'uses the user supplied john.pot if there is one' do
|
||||
cracker.pot = pot
|
||||
expect(cracker.show_command).to include "--pot=#{pot}"
|
||||
end
|
||||
|
||||
it 'uses default john.pot if the user did not supply one' do
|
||||
expect(cracker).to receive(:john_pot_file).and_return other_pot
|
||||
expect(cracker.show_command).to include "--pot=#{other_pot}"
|
||||
end
|
||||
|
||||
it 'uses the user supplied format directive' do
|
||||
cracker.format = nt_format
|
||||
expect(cracker.show_command).to include "--format=#{nt_format}"
|
||||
end
|
||||
|
||||
it 'puts the path to the has file at the end' do
|
||||
cracker.hash_path = hash_path
|
||||
expect(cracker.show_command.last).to eq hash_path
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
context 'failures' do
|
||||
context 'file_path validators' do
|
||||
before(:each) do
|
||||
expect(File).to receive(:file?).and_return false
|
||||
end
|
||||
|
||||
it 'produces the correct error message for config' do
|
||||
cracker.config = config
|
||||
expect(cracker).to_not be_valid
|
||||
expect(cracker.errors[:config]).to include "is not a valid path to a regular file"
|
||||
end
|
||||
|
||||
it 'produces the correct error message for hash_path' do
|
||||
cracker.hash_path = hash_path
|
||||
expect(cracker).to_not be_valid
|
||||
expect(cracker.errors[:hash_path]).to include "is not a valid path to a regular file"
|
||||
end
|
||||
|
||||
it 'produces the correct error message for pot' do
|
||||
cracker.pot = pot
|
||||
expect(cracker).to_not be_valid
|
||||
expect(cracker.errors[:pot]).to include "is not a valid path to a regular file"
|
||||
end
|
||||
|
||||
it 'produces the correct error message for wordlist' do
|
||||
cracker.wordlist = wordlist
|
||||
expect(cracker).to_not be_valid
|
||||
expect(cracker.errors[:wordlist]).to include "is not a valid path to a regular file"
|
||||
end
|
||||
end
|
||||
|
||||
context 'executable_path validators' do
|
||||
before(:each) do
|
||||
expect(File).to receive(:executable?).and_return false
|
||||
end
|
||||
|
||||
it 'produces the correct error message for john_path' do
|
||||
cracker.john_path = john_path
|
||||
expect(cracker).to_not be_valid
|
||||
expect(cracker.errors[:john_path]).to include "is not a valid path to an executable file"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'successes' do
|
||||
context 'file_path validators' do
|
||||
before(:each) do
|
||||
expect(File).to receive(:file?).and_return true
|
||||
end
|
||||
|
||||
it 'produces no error message for config' do
|
||||
cracker.config = config
|
||||
expect(cracker).to be_valid
|
||||
expect(cracker.errors[:config]).to_not include "is not a valid path to a regular file"
|
||||
end
|
||||
|
||||
it 'produces no error message for hash_path' do
|
||||
cracker.hash_path = hash_path
|
||||
expect(cracker).to be_valid
|
||||
expect(cracker.errors[:hash_path]).to_not include "is not a valid path to a regular file"
|
||||
end
|
||||
|
||||
it 'produces no error message for pot' do
|
||||
cracker.pot = pot
|
||||
expect(cracker).to be_valid
|
||||
expect(cracker.errors[:pot]).to_not include "is not a valid path to a regular file"
|
||||
end
|
||||
|
||||
it 'produces no error message for wordlist' do
|
||||
cracker.wordlist = wordlist
|
||||
expect(cracker).to be_valid
|
||||
expect(cracker.errors[:wordlist]).to_not include "is not a valid path to a regular file"
|
||||
end
|
||||
end
|
||||
|
||||
context 'executable_path validators' do
|
||||
before(:each) do
|
||||
expect(File).to receive(:executable?).and_return true
|
||||
end
|
||||
|
||||
it 'produces no error message for john_path' do
|
||||
cracker.john_path = john_path
|
||||
expect(cracker).to be_valid
|
||||
expect(cracker.errors[:john_path]).to_not include "is not a valid path to an executable file"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
require 'spec_helper'
|
||||
require 'metasploit/framework/jtr/invalid_wordlist'
|
||||
|
||||
describe Metasploit::Framework::JtR::InvalidWordlist do
|
||||
|
||||
subject(:invalid) do
|
||||
described_class.new(model)
|
||||
end
|
||||
|
||||
let(:model) do
|
||||
model_class.new
|
||||
end
|
||||
|
||||
let(:model_class) do
|
||||
Class.new do
|
||||
include ActiveModel::Validations
|
||||
end
|
||||
end
|
||||
|
||||
it { should be_a StandardError }
|
||||
|
||||
it 'should use ActiveModel::Errors#full_messages' do
|
||||
model.errors.should_receive(:full_messages).and_call_original
|
||||
|
||||
described_class.new(model)
|
||||
end
|
||||
|
||||
context '#model' do
|
||||
subject(:error_model) do
|
||||
invalid.model
|
||||
end
|
||||
|
||||
it 'should be the passed in model' do
|
||||
error_model.should == model
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,138 @@
|
|||
require 'spec_helper'
|
||||
require 'metasploit/framework/jtr/wordlist'
|
||||
|
||||
describe Metasploit::Framework::JtR::Wordlist do
|
||||
|
||||
subject(:wordlist) { described_class.new }
|
||||
|
||||
let(:custom_wordlist) { File.expand_path('string_list.txt',FILE_FIXTURES_PATH) }
|
||||
let(:expansion_word) { 'Foo bar_baz-bat.bam\\foo//bar' }
|
||||
let(:common_root_path) { File.expand_path('fake_common_roots.txt',FILE_FIXTURES_PATH) }
|
||||
let(:default_wordlist_path) { File.expand_path('fake_default_wordlist.txt',FILE_FIXTURES_PATH) }
|
||||
let(:password) { FactoryGirl.create(:metasploit_credential_password) }
|
||||
let(:public) { FactoryGirl.create(:metasploit_credential_public) }
|
||||
let(:realm) { FactoryGirl.create(:metasploit_credential_realm) }
|
||||
let(:mutate_me) { 'password' }
|
||||
let(:mutants) { [
|
||||
"pa55word",
|
||||
"password",
|
||||
"pa$$word",
|
||||
"passw0rd",
|
||||
"pa55w0rd",
|
||||
"pa$$w0rd",
|
||||
"p@ssword",
|
||||
"p@55word",
|
||||
"p@$$word",
|
||||
"p@ssw0rd",
|
||||
"p@55w0rd",
|
||||
"p@$$w0rd"
|
||||
] }
|
||||
|
||||
it { should respond_to :appenders }
|
||||
it { should respond_to :custom_wordlist }
|
||||
it { should respond_to :mutate }
|
||||
it { should respond_to :prependers }
|
||||
it { should respond_to :use_common_root }
|
||||
it { should respond_to :use_creds }
|
||||
it { should respond_to :use_db_info }
|
||||
it { should respond_to :use_default_wordlist }
|
||||
it { should respond_to :use_hostnames }
|
||||
|
||||
describe 'validations' do
|
||||
|
||||
it 'raises an error if the custom_wordlist does not exist on the filesystem' do
|
||||
expect(File).to receive(:file?).and_return false
|
||||
wordlist.custom_wordlist = custom_wordlist
|
||||
expect(wordlist).to_not be_valid
|
||||
expect(wordlist.errors[:custom_wordlist]).to include "is not a valid path to a regular file"
|
||||
end
|
||||
|
||||
it 'raises an error if mutate is not set to true or false' do
|
||||
expect(wordlist).to_not be_valid
|
||||
expect(wordlist.errors[:mutate]).to include "must be true or false"
|
||||
end
|
||||
|
||||
it 'raises an error if use_common_root is not set to true or false' do
|
||||
expect(wordlist).to_not be_valid
|
||||
expect(wordlist.errors[:use_common_root]).to include "must be true or false"
|
||||
end
|
||||
|
||||
it 'raises an error if use_creds is not set to true or false' do
|
||||
expect(wordlist).to_not be_valid
|
||||
expect(wordlist.errors[:use_creds]).to include "must be true or false"
|
||||
end
|
||||
|
||||
it 'raises an error if use_db_info is not set to true or false' do
|
||||
expect(wordlist).to_not be_valid
|
||||
expect(wordlist.errors[:use_db_info]).to include "must be true or false"
|
||||
end
|
||||
|
||||
it 'raises an error if use_default_wordlist is not set to true or false' do
|
||||
expect(wordlist).to_not be_valid
|
||||
expect(wordlist.errors[:use_default_wordlist]).to include "must be true or false"
|
||||
end
|
||||
|
||||
it 'raises an error if use_hostnames is not set to true or false' do
|
||||
expect(wordlist).to_not be_valid
|
||||
expect(wordlist.errors[:use_hostnames]).to include "must be true or false"
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid!' do
|
||||
it 'raises an InvalidWordlist exception if not valid?' do
|
||||
expect{ wordlist.valid! }.to raise_error Metasploit::Framework::JtR::InvalidWordlist
|
||||
end
|
||||
end
|
||||
|
||||
describe '#expanded_words' do
|
||||
it 'yields all the possible component words in the string' do
|
||||
expect { |b| wordlist.expanded_words(expansion_word,&b) }.to yield_successive_args('Foo','bar','baz','bat','bam','foo','bar')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#each_custom_word' do
|
||||
it 'yields each word in that wordlist' do
|
||||
wordlist.custom_wordlist = custom_wordlist
|
||||
expect{ |b| wordlist.each_custom_word(&b) }.to yield_successive_args('foo', 'bar','baz')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#each_root_word' do
|
||||
it 'yields each word in the common_roots.txt list' do
|
||||
expect(wordlist).to receive(:common_root_words_path).and_return common_root_path
|
||||
expect { |b| wordlist.each_root_word(&b) }.to yield_successive_args('password', 'root', 'toor')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#each_default_word' do
|
||||
it 'yields each word in the passwords.lst list' do
|
||||
expect(wordlist).to receive(:default_wordlist_path).and_return default_wordlist_path
|
||||
expect { |b| wordlist.each_default_word(&b) }.to yield_successive_args('changeme', 'summer123', 'admin')
|
||||
end
|
||||
end
|
||||
|
||||
define '#each_cred_word' do
|
||||
it 'yields each username,password,and realm in the database' do
|
||||
expect{ |b| wordlist.each_cred_word(&b) }.to yield_successive_args(password.data, public,username, realm,value)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mutate_word' do
|
||||
it 'returns an array with all possible mutations of the word' do
|
||||
expect(wordlist.mutate_word(mutate_me)).to eq mutants
|
||||
end
|
||||
end
|
||||
|
||||
describe '#each_mutated_word' do
|
||||
it 'yields each unique mutated word if mutate set to true' do
|
||||
wordlist.mutate = true
|
||||
expect { |b| wordlist.each_mutated_word(mutate_me,&b)}.to yield_successive_args(*mutants)
|
||||
end
|
||||
|
||||
it 'yields the original word if mutate set to true' do
|
||||
wordlist.mutate = false
|
||||
expect { |b| wordlist.each_mutated_word(mutate_me,&b)}.to yield_with_args(mutate_me)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue