lib/metasploit/credential/importer/pwdump.rb
# Implements importation behavior for pwdump files exported by Metasploit as well as files from the John the Ripper
# hash cracking suite: http://www.openwall.com/john/
#
# Please note that in the case of data exported from Metasploit, the dataset will contain information on the `Mdm::Host`
# and `Mdm::Service` objects that are related to the credential. This means that Metasploit exports will be limited to
# containing {Metasploit::Credential::Login} objects, which is the legacy behavior of this export prior to the creation
# of this library.
class Metasploit::Credential::Importer::Pwdump
include Metasploit::Credential::Importer::Base
include Metasploit::Credential::Creation
#
# Constants
#
# Matches a line starting with a '#'
COMMENT_LINE_START_REGEX = /^[\s]*#/
# The string that John the Ripper uses to designate a lack of password in a credentials entry
JTR_NO_PASSWORD_STRING = "NO PASSWORD"
# Matches lines that contain usernames and non-SMB hashes
NONREPLAYABLE_REGEX = /^[\s]*([\x21-\x7f]+):([\x21-\x7f]+):::/n
# Matches lines that contain usernames and plaintext passwords
PLAINTEXT_REGEX = /^[\s]*([\x21-\x7f]+)[\s]+([\x21-\x7f]+)?/n
# Matches lines taht contain MD5 hashes for PostgreSQL
POSTGRES_REGEX = /^[\s]*([\x21-\x7f]+):md5([0-9a-f]{32})$/
# Matches a line that we use to get information for creating `Mdm::Host` and `Mdm::Service` objects
# TODO: change to use named groups from 1.9+
SERVICE_COMMENT_REGEX = /^#[\s]*([0-9.]+):([0-9]+)(\x2f(tcp|udp))?[\s]*(\x28([^\x29]*)\x29)?/n
# Matches the way that John the Ripper exports SMB hashes with no password piece
SMB_WITH_JTR_BLANK_PASSWORD_REGEX = /^[\s]*([^\s:]+):([0-9]+):NO PASSWORD\*+:NO PASSWORD\*+[^\s]*$/
# Matches LM/NTLM hash format
SMB_WITH_HASH_REGEX = /^[\s]*([^\s:]+):[0-9]+:([A-Fa-f0-9]+:[A-Fa-f0-9]+):[^\s]*$/
# Matches a line with free-form text - less restrictive than {SMB_WITH_HASH_REGEX}
SMB_WITH_PLAINTEXT_REGEX = /^[\s]*([^\s:]+):(.+):[A-Fa-f0-9]*:[A-Fa-f0-9]*:::$/
# Matches warning lines in legacy pwdump files
WARNING_REGEX = /^[\s]*Warning:/
#
# Validations
#
validates :filename, presence: true
#
# Instance Methods
#
# Checks a string for matching {Metasploit::Credential::Exporter::Pwdump::BLANK_CRED_STRING} and returns blank string
# if it matches that constant.
# @param check_string [String] the string to check
# @param dehex [Boolean] convert hex to char if true
# @return [String]
def blank_or_string(check_string, dehex=false)
if check_string.blank? || check_string == Metasploit::Credential::Exporter::Pwdump::BLANK_CRED_STRING || check_string == JTR_NO_PASSWORD_STRING
""
else
if dehex
Metasploit::Credential::Text.dehex check_string
else
check_string
end
end
end
# Perform the import of the credential data, creating `Mdm::Host` and `Mdm::Service` objects as needed,
# parsing out data by matching against regex constants that match the various kinds of valid lines found
# in the file. Ignore lines which match none of the REGEX constants.
# @return [void]
def import!
service_info = nil
Metasploit::Credential::Core.transaction do
input.each_line do |line|
case line
when WARNING_REGEX
next
when COMMENT_LINE_START_REGEX
service_info = service_info_from_comment_string(line)
when SMB_WITH_HASH_REGEX
info = parsed_regex_results($1, $2)
username, private = info[:username], info[:private]
creds_class = Metasploit::Credential::NTLMHash
when SMB_WITH_JTR_BLANK_PASSWORD_REGEX
info = parsed_regex_results($1, $2)
username, private = info[:username], info[:private]
creds_class = Metasploit::Credential::NTLMHash
when SMB_WITH_PLAINTEXT_REGEX
info = parsed_regex_results($1, $2)
username, private = info[:username], info[:private]
creds_class = Metasploit::Credential::NTLMHash
when NONREPLAYABLE_REGEX
info = parsed_regex_results($1, $2)
username, private = info[:username], info[:private]
creds_class = Metasploit::Credential::NonreplayableHash
when POSTGRES_REGEX
info = parsed_regex_results($1,"md5#{$2}")
username, private = info[:username], info[:private]
creds_class = Metasploit::Credential::PostgresMD5
when PLAINTEXT_REGEX
info = parsed_regex_results($1, $2, true)
username, private = info[:username], info[:private]
creds_class = Metasploit::Credential::Password
else
next
end
# Skip unless we have enough to make a Login
if service_info.present?
if [service_info[:host_address], service_info[:port], username, private].compact.size != 4
next
end
else
next
end
public_obj = create_credential_public(username: username)
private_obj = creds_class.where(data: private).first_or_create
core = create_credential_core(origin: origin, private: private_obj, public: public_obj, workspace_id: workspace.id)
login_opts = {
address: service_info[:host_address],
port: service_info[:port],
protocol: service_info[:protocol],
service_name: service_info[:name],
workspace_id: workspace.id,
core: core,
status: Metasploit::Model::Login::Status::UNTRIED
}
create_credential_login(login_opts)
end
end
end
def initialize(args={})
super args
end
# Break a line into user, hash
# @param username [String]
# @param private [String]
# @param dehex [Boolean] convert hex to char if true
# @return [Hash]
def parsed_regex_results(username, private, dehex=false)
results = {}
results[:username] = blank_or_string(username, dehex)
results[:private] = blank_or_string(private, dehex)
results
end
# Take an msfpwdump comment string and parse it into information necessary for
# creating `Mdm::Host` and `Mdm::Service` objects.
# @param comment_string [String] a string starting with a '#' that conforms to {SERVICE_COMMENT_REGEX}
# @return [Hash]
def service_info_from_comment_string(comment_string)
service_info = {}
if comment_string[SERVICE_COMMENT_REGEX]
service_info[:host_address] = $1
service_info[:port] = $2
service_info[:protocol] = $4.present? ? $4 : "tcp"
service_info[:name] = $6
service_info
else
nil
end
end
end