lib/msf/ui/console/command_dispatcher/creds.rb
# -*- coding: binary -*-
require 'rexml/document'
require 'metasploit/framework/password_crackers/hashcat/formatter'
require 'metasploit/framework/password_crackers/jtr/formatter'
module Msf
module Ui
module Console
module CommandDispatcher
class Creds
require 'tempfile'
include Msf::Ui::Console::CommandDispatcher
include Metasploit::Credential::Creation
include Msf::Ui::Console::CommandDispatcher::Common
#
# The dispatcher's name.
#
def name
"Credentials Backend"
end
#
# Returns the hash of commands supported by this dispatcher.
#
def commands
{
"creds" => "List all credentials in the database"
}
end
def allowed_cred_types
%w(password ntlm hash KrbEncKey) + Metasploit::Credential::NonreplayableHash::VALID_JTR_FORMATS
end
#
# Returns true if the db is connected, prints an error and returns
# false if not.
#
# All commands that require an active database should call this before
# doing anything.
# TODO: abstract the db methods to a mixin that can be used by both dispatchers
#
def active?
if not framework.db.active
print_error("Database not connected")
return false
end
true
end
#
# Miscellaneous option helpers
#
#
# Can return return active or all, on a certain host or range, on a
# certain port or range, and/or on a service name.
#
def cmd_creds(*args)
return unless active?
# Short-circuit help
if args.delete("-h") || args.delete("--help")
cmd_creds_help
return
end
subcommand = args.shift
case subcommand
when 'help'
cmd_creds_help
when 'add'
creds_add(*args)
else
# then it's not actually a subcommand
args.unshift(subcommand) if subcommand
creds_search(*args)
end
end
#
# TODO: this needs to be cleaned up to use the new syntax
#
def cmd_creds_help
print_line
print_line "With no sub-command, list credentials. If an address range is"
print_line "given, show only credentials with logins on hosts within that"
print_line "range."
print_line
print_line "Usage - Listing credentials:"
print_line " creds [filter options] [address range]"
print_line
print_line "Usage - Adding credentials:"
print_line " creds add uses the following named parameters."
{
user: 'Public, usually a username',
password: 'Private, private_type Password.',
ntlm: 'Private, private_type NTLM Hash.',
postgres: 'Private, private_type postgres MD5',
pkcs12: 'Private, private_type pkcs12 archive file, must be a file path.',
'ssh-key' => 'Private, private_type SSH key, must be a file path.',
hash: 'Private, private_type Nonreplayable hash',
jtr: 'Private, private_type John the Ripper hash type.',
realm: 'Realm, ',
'realm-type'=>"Realm, realm_type (#{Metasploit::Model::Realm::Key::SHORT_NAMES.keys.join(' ')}), defaults to domain."
}.each_pair do |keyword, description|
print_line " #{keyword.to_s.ljust 10}: #{description}"
end
print_line
print_line "Examples: Adding"
print_line " # Add a user, password and realm"
print_line " creds add user:admin password:notpassword realm:workgroup"
print_line " # Add a user and password"
print_line " creds add user:guest password:'guest password'"
print_line " # Add a password"
print_line " creds add password:'password without username'"
print_line " # Add a user with an NTLMHash"
print_line " creds add user:admin ntlm:E2FC15074BF7751DD408E6B105741864:A1074A69B1BDE45403AB680504BBDD1A"
print_line " # Add a NTLMHash"
print_line " creds add ntlm:E2FC15074BF7751DD408E6B105741864:A1074A69B1BDE45403AB680504BBDD1A"
print_line " # Add a Postgres MD5"
print_line " creds add user:postgres postgres:md5be86a79bf2043622d58d5453c47d4860"
print_line " # Add a user with a PKCS12 file archive"
print_line " creds add user:alice pkcs12:/path/to/certificate.pfx"
print_line " # Add a user with an SSH key"
print_line " creds add user:sshadmin ssh-key:/path/to/id_rsa"
print_line " # Add a user and a NonReplayableHash"
print_line " creds add user:other hash:d19c32489b870735b5f587d76b934283 jtr:md5"
print_line " # Add a NonReplayableHash"
print_line " creds add hash:d19c32489b870735b5f587d76b934283"
print_line
print_line "General options"
print_line " -h,--help Show this help information"
print_line " -o <file> Send output to a file in csv/jtr (john the ripper) format."
print_line " If file name ends in '.jtr', that format will be used."
print_line " If file name ends in '.hcat', the hashcat format will be used."
print_line " csv by default."
print_line " -d,--delete Delete one or more credentials"
print_line
print_line "Filter options for listing"
print_line " -P,--password <text> List passwords that match this text"
print_line " -p,--port <portspec> List creds with logins on services matching this port spec"
print_line " -s <svc names> List creds matching comma-separated service names"
print_line " -u,--user <text> List users that match this text"
print_line " -t,--type <type> List creds of the specified type: password, ntlm, hash or any valid JtR format"
print_line " -O,--origins <IP> List creds that match these origins"
print_line " -r,--realm <realm> List creds that match this realm"
print_line " -R,--rhosts Set RHOSTS from the results of the search"
print_line " -v,--verbose Don't truncate long password hashes"
print_line
print_line "Examples, John the Ripper hash types:"
print_line " Operating Systems (starts with)"
print_line " Blowfish ($2a$) : bf"
print_line " BSDi (_) : bsdi"
print_line " DES : des,crypt"
print_line " MD5 ($1$) : md5"
print_line " SHA256 ($5$) : sha256,crypt"
print_line " SHA512 ($6$) : sha512,crypt"
print_line " Databases"
print_line " MSSQL : mssql"
print_line " MSSQL 2005 : mssql05"
print_line " MSSQL 2012/2014 : mssql12"
print_line " MySQL < 4.1 : mysql"
print_line " MySQL >= 4.1 : mysql-sha1"
print_line " Oracle : des,oracle"
print_line " Oracle 11 : raw-sha1,oracle11"
print_line " Oracle 11 (H type): dynamic_1506"
print_line " Oracle 12c : oracle12c"
print_line " Postgres : postgres,raw-md5"
print_line
print_line "Examples, listing:"
print_line " creds # Default, returns all credentials"
print_line " creds 1.2.3.4/24 # Return credentials with logins in this range"
print_line " creds -O 1.2.3.4/24 # Return credentials with origins in this range"
print_line " creds -p 22-25,445 # nmap port specification"
print_line " creds -s ssh,smb # All creds associated with a login on SSH or SMB services"
print_line " creds -t ntlm # All NTLM creds"
print_line
print_line "Example, deleting:"
print_line " # Delete all SMB credentials"
print_line " creds -d -s smb"
print_line
end
# @param private_type [Symbol] See `Metasploit::Credential::Creation#create_credential`
# @param username [String]
# @param password [String]
# @param realm [String]
# @param realm_type [String] A key in `Metasploit::Model::Realm::Key::SHORT_NAMES`
def creds_add(*args)
params = args.inject({}) do |hsh, n|
opt = n.split(':') # Splitting the string on colons.
hsh[opt[0]] = opt[1..-1].join(':') # everything before the first : is the key, reasembling everything after the colon. why ntlm hashes
hsh
end
begin
params.assert_valid_keys('user','password','realm','realm-type','ntlm','ssh-key','hash','address','port','protocol', 'service-name', 'jtr', 'pkcs12', 'postgres')
rescue ArgumentError => e
print_error(e.message)
end
# Verify we only have one type of private
if params.slice('password','ntlm','ssh-key','hash', 'pkcs12', 'postgres').length > 1
private_keys = params.slice('password','ntlm','ssh-key','hash', 'pkcs12', 'postgres').keys
print_error("You can only specify a single Private type. Private types given: #{private_keys.join(', ')}")
return
end
login_keys = params.slice('address','port','protocol','service-name')
if login_keys.any? and login_keys.length < 3
missing_login_keys = ['host','port','proto','service-name'] - login_keys.keys
print_error("Creating a login requires a address, a port, and a protocol. Missing params: #{missing_login_keys}")
return
end
data = {
workspace_id: framework.db.workspace.id,
origin_type: :import,
filename: 'msfconsole'
}
data[:username] = params['user'] if params.key? 'user'
if params.key? 'realm'
if params.key? 'realm-type'
if Metasploit::Model::Realm::Key::SHORT_NAMES.key? params['realm-type']
data[:realm_key] = Metasploit::Model::Realm::Key::SHORT_NAMES[params['realm-type']]
else
valid = Metasploit::Model::Realm::Key::SHORT_NAMES.keys.map{|n|"'#{n}'"}.join(", ")
print_error("Invalid realm type: #{params['realm_type']}. Valid Values: #{valid}")
end
else
data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
end
data[:realm_value] = params['realm']
end
if params.key? 'password'
data[:private_type] = :password
data[:private_data] = params['password']
end
if params.key? 'ntlm'
data[:private_type] = :ntlm_hash
data[:private_data] = params['ntlm']
end
if params.key? 'ssh-key'
begin
key_data = File.read(params['ssh-key'])
rescue ::Errno::EACCES, ::Errno::ENOENT => e
print_error("Failed to add ssh key: #{e}")
end
data[:private_type] = :ssh_key
data[:private_data] = key_data
end
if params.key? 'pkcs12'
begin
# pkcs12 is a binary format, but for persisting we Base64 encode it
pkcs12_data = Base64.strict_encode64(File.binread(params['pkcs12']))
rescue ::Errno::EACCES, ::Errno::ENOENT => e
print_error("Failed to add pkcs12 archive: #{e}")
end
data[:private_type] = :pkcs12
data[:private_data] = pkcs12_data
end
if params.key? 'hash'
data[:private_type] = :nonreplayable_hash
data[:private_data] = params['hash']
data[:jtr_format] = params['jtr'] if params.key? 'jtr'
end
if params.key? 'postgres'
data[:private_type] = :postgres_md5
if params['postgres'].downcase.start_with?('md5')
data[:private_data] = params['postgres']
data[:jtr_format] = 'postgres'
else
print_error("Postgres MD5 hashes should start with 'md5'")
end
end
begin
if login_keys.any?
data[:address] = params['address']
data[:port] = params['port']
data[:protocol] = params['protocol']
data[:service_name] = params['service-name']
framework.db.create_credential_and_login(data)
else
framework.db.create_credential(data)
end
rescue ActiveRecord::RecordInvalid => e
print_error("Failed to add #{data['private_type']}: #{e}")
end
end
def service_from_origin(core)
# Depending on the origin of the cred, there may or may not be a way to retrieve the associated service
case core.origin
when Metasploit::Credential::Origin::Service
return core.origin.service
end
end
def build_service_info(service)
if service.name.present?
info = "#{service.port}/#{service.proto} (#{service.name})"
else
info = "#{service.port}/#{service.proto}"
end
info
end
def creds_search(*args)
host_ranges = []
origin_ranges = []
port_ranges = []
svcs = []
rhosts = []
opts = {}
set_rhosts = false
truncate = true
cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format' ]
delete_count = 0
search_term = nil
while (arg = args.shift)
case arg
when '-o'
output_file = args.shift
if (!output_file)
print_error('Invalid output filename')
return
end
output_file = ::File.expand_path(output_file)
truncate = false
when '-p', '--port'
unless (arg_port_range(args.shift, port_ranges, true))
return
end
when '-t', '--type'
ptype = args.shift
opts[:ptype] = ptype
if (!ptype)
print_error('Argument required for -t')
return
end
when '-s', '--service'
service = args.shift
if (!service)
print_error('Argument required for -s')
return
end
svcs = service.split(/[\s]*,[\s]*/)
opts[:svcs] = svcs
when '-P', '--password'
if !(opts[:pass] = args.shift)
print_error('Argument required for -P')
return
end
when '-u', '--user'
if !(opts[:user] = args.shift)
print_error('Argument required for -u')
return
end
when '-d', '--delete'
mode = :delete
when '-R', '--rhosts'
set_rhosts = true
when '-O', '--origins'
hosts = args.shift
opts[:hosts] = hosts
if !hosts
print_error('Argument required for -O')
return
end
arg_host_range(hosts, origin_ranges)
when '-S', '--search-term'
search_term = args.shift
opts[:search_term] = search_term
when '-v', '--verbose'
truncate = false
when '-r', '--realm'
opts[:realm] = args.shift
else
# Anything that wasn't an option is a host to search for
unless (arg_host_range(arg, host_ranges))
return
end
end
end
# If we get here, we're searching. Delete implies search
if ptype
type = case ptype.downcase
when 'password'
Metasploit::Credential::Password
when 'hash'
Metasploit::Credential::PasswordHash
when 'ntlm'
Metasploit::Credential::NTLMHash
when 'KrbEncKey'.downcase
Metasploit::Credential::KrbEncKey
when *Metasploit::Credential::NonreplayableHash::VALID_JTR_FORMATS
opts[:jtr_format] = ptype
Metasploit::Credential::NonreplayableHash
else
print_error("Unrecognized credential type #{ptype} -- must be one of #{allowed_cred_types.join(',')}")
return
end
end
opts[:type] = type if type
# normalize
ports = port_ranges.flatten.uniq
opts[:ports] = ports unless ports.empty?
svcs.flatten!
tbl_opts = {
'Header' => "Credentials",
# For now, don't perform any word wrapping on the cred table as it breaks the workflow of
# copying credentials and pasting them into applications
'WordWrap' => false,
'Columns' => cred_table_columns,
'SearchTerm' => search_term
}
opts[:workspace] = framework.db.workspace
query = framework.db.creds(opts)
matched_cred_ids = []
if output_file&.ends_with?('.hcat')
output_file = ::File.open(output_file, 'wb')
output_formatter = method(:hash_to_hashcat)
elsif output_file&.ends_with?('.jtr')
output_file = ::File.open(output_file, 'wb')
output_formatter = method(:hash_to_jtr)
else
output_file = ::File.open(output_file, 'wb') unless output_file.blank?
tbl = Rex::Text::Table.new(tbl_opts)
end
filtered_query(query, opts, origin_ranges, host_ranges) do |core, service, origin|
matched_cred_ids << core.id
if output_file && output_formatter
formatted = output_formatter.call(core)
output_file.puts(formatted) unless formatted.blank?
end
unless tbl.nil?
public_val = core.public ? core.public.username : ''
if core.private
# Show the human readable description by default, unless the user ran with `--verbose` and wants to see the cred data
private_val = truncate ? core.private.to_s : core.private.data
else
private_val = ''
end
if truncate && private_val.to_s.length > 87
private_val = "#{private_val[0,87]} (TRUNCATED)"
end
realm_val = core.realm ? core.realm.value : ''
human_val = core.private ? core.private.class.model_name.human : ''
if human_val == ''
jtr_val = '' #11433, private can be nil
else
jtr_val = core.private.jtr_format ? core.private.jtr_format : ''
end
if service.nil?
host = ''
service_info = ''
else
host = service.host.address
rhosts << host unless host.blank?
service_info = build_service_info(service)
end
tbl << [
host,
origin,
service_info,
public_val,
private_val,
realm_val,
human_val, #private type
jtr_val
]
end
end
if output_file.nil?
print_line(tbl.to_s)
else
output_file.write(tbl.to_csv) if output_formatter.nil?
output_file.close
print_status("Wrote creds to #{output_file.path}")
end
if mode == :delete
result = framework.db.delete_credentials(ids: matched_cred_ids.uniq)
delete_count = result.size
end
# Finally, handle the case where the user wants the resulting list
# of hosts to go into RHOSTS.
set_rhosts_from_addrs(rhosts.uniq) if set_rhosts
print_status("Deleted #{delete_count} creds") if delete_count > 0
end
def cmd_creds_tabs(str, words)
case words.length
when 1
# subcommands
tabs = [ 'add-ntlm', 'add-password', 'add-hash', 'add-ssh-key', ]
when 2
tabs = if words[1] == 'add-ssh-key'
tab_complete_filenames(str, words)
else
[]
end
#when 5
# tabs = Metasploit::Model::Realm::Key::SHORT_NAMES.keys
else
tabs = []
end
return tabs
end
protected
def filtered_query(query, opts, origin_ranges, host_ranges)
query.each do |core|
# Exclude non-blank username creds if that's what we're after
if opts[:user] == '' && core.public && !(core.public.username.blank?)
next
end
# Exclude non-blank password creds if that's what we're after
if opts[:pass] == '' && core.private && !(core.private.data.blank?)
next
end
origin = ''
if core.origin.kind_of?(Metasploit::Credential::Origin::Service)
service = framework.db.services(id: core.origin.service_id).first
origin = service.host.address
elsif core.origin.kind_of?(Metasploit::Credential::Origin::Session)
session = framework.db.sessions(id: core.origin.session_id).first
origin = session.host.address
end
if origin_ranges.present? && !origin_ranges.any? { |range| range.include?(origin) }
next
end
if core.logins.empty?
service = service_from_origin(core)
next if service.nil? && host_ranges.present? # If we're filtering by login IP and we're here there's no associated login, so skip
yield core, service, origin
else
core.logins.each do |login|
service = framework.db.services(id: login.service_id).first
# If none of this Core's associated Logins is for a host within
# the user-supplied RangeWalker, then we don't have any reason to
# print it out. However, we treat the absence of ranges as meaning
# all hosts.
if host_ranges.present? && !host_ranges.any? { |range| range.include?(service.host.address) }
next
end
yield core, service, origin
end
end
end
end
end
end end end end