Land #7804, Switch the creds command to use named options

bug/bundler_fix
Brent Cook 2017-01-22 10:49:19 -06:00
commit ac2ceca5e3
No known key found for this signature in database
GPG Key ID: 1FFAA0B24B708F96
6 changed files with 967 additions and 649 deletions

View File

@ -84,7 +84,6 @@ Feature: Help command
Command Description Command Description
------- ----------- ------- -----------
creds List all credentials in the database
db_connect Connect to an existing database db_connect Connect to an existing database
db_disconnect Disconnect from the current database instance db_disconnect Disconnect from the current database instance
db_export Export a file containing the contents of the database db_export Export a file containing the contents of the database
@ -98,5 +97,15 @@ Feature: Help command
services List all services in the database services List all services in the database
vulns List all vulnerabilities in the database vulns List all vulnerabilities in the database
workspace Switch between database workspaces workspace Switch between database workspaces
Credentials Backend Commands
============================
Command Description
------- -----------
creds List all credentials in the database
""" """

View File

@ -0,0 +1,505 @@
# -*- coding: binary -*-
require 'rexml/document'
require 'rex/parser/nmap_xml'
require 'msf/core/db_export'
module Msf
module Ui
module Console
module CommandDispatcher
class Creds
require 'tempfile'
include Msf::Ui::Console::CommandDispatcher
include Metasploit::Credential::Creation
#
# 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)
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 methothds 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
#
# Parse +arg+ into a {Rex::Socket::RangeWalker} and append the result into +host_ranges+
#
# @note This modifies +host_ranges+ in place
#
# @param arg [String] The thing to turn into a RangeWalker
# @param host_ranges [Array] The array of ranges to append
# @param required [Boolean] Whether an empty +arg+ should be an error
# @return [Boolean] true if parsing was successful or false otherwise
def arg_host_range(arg, host_ranges, required=false)
if (!arg and required)
print_error("Missing required host argument")
return false
end
begin
rw = Rex::Socket::RangeWalker.new(arg)
rescue
print_error("Invalid host parameter, #{arg}.")
return false
end
if rw.valid?
host_ranges << rw
else
print_error("Invalid host parameter, #{arg}.")
return false
end
return true
end
#
# 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"
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.',
'ssh-key': 'Private, private_type SSH key, must be a file path.',
hash: 'Private, private_type Nonreplayable hash',
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 user with an SSH key"
print_line " creds add user:sshadmin ssh-key:/path/to/id_rsa"
print_line " # Add a SSH key"
print_line " creds add ssh-key:/path/to/id_rsa"
print_line " # Add a user and a NonReplayableHash"
print_line " creds add user:other hash:d19c32489b870735b5f587d76b934283"
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 format"
print_line " -d Delete one or more credentials"
print_line
print_line "Filter options for listing"
print_line " -P,--password <regex> List passwords that match this regex"
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 <regex> List users that match this regex"
print_line " -t,--type <type> List creds that match the following types: #{allowed_cred_types.join(',')}"
print_line " -O,--origins List creds that match these origins"
print_line " -R,--rhosts Set RHOSTS from the results of the search"
print_line
print_line "Examples, listing:"
print_line " creds # Default, returns all credentials"
print_line " creds 1.2.3.4/24 # nmap host specification"
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','host','port')
rescue ArgumentError => e
print_error(e.message)
end
# Verify we only have one type of private
if params.slice('password','ntlm','ssh-key','hash').length > 1
private_keys = params.slice('password','ntlm','ssh-key','hash').keys
print_error("You can only specify a single Private type. Private types given: #{private_keys.join(', ')}")
return
end
data = {
workspace_id: framework.db.workspace,
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? 'hash'
data[:private_type] = :nonreplayable_hash
data[:private_data] = params['hash']
end
begin
create_credential(data)
rescue ActiveRecord::RecordInvalid => e
print_error("Failed to add #{data['private_type']}: #{e}")
end
end
def creds_search(*args)
host_ranges = []
origin_ranges = []
port_ranges = []
svcs = []
rhosts = []
set_rhosts = false
#cred_table_columns = [ 'host', 'port', 'user', 'pass', 'type', 'proof', 'active?' ]
cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type' ]
user = nil
delete_count = 0
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)
when "-p","--port"
unless (arg_port_range(args.shift, port_ranges, true))
return
end
when "-t","--type"
ptype = args.shift
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]*/)
when "-P","--password"
pass = args.shift
if (!pass)
print_error("Argument required for -P")
return
end
when "-u","--user"
user = args.shift
if (!user)
print_error("Argument required for -u")
return
end
when "-d"
mode = :delete
when '-R', '--rhosts'
set_rhosts = true
when '-O', '--origins'
hosts = args.shift
if !hosts
print_error("Argument required for -O")
return
end
arg_host_range(hosts, origin_ranges)
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
when 'password'
Metasploit::Credential::Password
when 'hash'
Metasploit::Credential::PasswordHash
when 'ntlm'
Metasploit::Credential::NTLMHash
else
print_error("Unrecognized credential type #{ptype} -- must be one of #{allowed_cred_types.join(',')}")
return
end
end
# normalize
ports = port_ranges.flatten.uniq
svcs.flatten!
tbl_opts = {
'Header' => "Credentials",
'Columns' => cred_table_columns
}
tbl = Rex::Text::Table.new(tbl_opts)
::ActiveRecord::Base.connection_pool.with_connection {
query = Metasploit::Credential::Core.where( workspace_id: framework.db.workspace )
query = query.includes(:private, :public, :logins).references(:private, :public, :logins)
query = query.includes(logins: [ :service, { service: :host } ])
if type.present?
query = query.where(metasploit_credential_privates: { type: type })
end
if svcs.present?
query = query.where(Mdm::Service[:name].in(svcs))
end
if ports.present?
query = query.where(Mdm::Service[:port].in(ports))
end
if user.present?
# If we have a user regex, only include those that match
query = query.where('"metasploit_credential_publics"."username" ~* ?', user)
end
if pass.present?
# If we have a password regex, only include those that match
query = query.where('"metasploit_credential_privates"."data" ~* ?', pass)
end
if host_ranges.any? || ports.any? || svcs.any?
# Only find Cores that have non-zero Logins if the user specified a
# filter based on host, port, or service name
query = query.where(Metasploit::Credential::Login[:id].not_eq(nil))
end
query.find_each do |core|
# Exclude non-blank username creds if that's what we're after
if user == "" && core.public && !(core.public.username.blank?)
next
end
# Exclude non-blank password creds if that's what we're after
if pass == "" && core.private && !(core.private.data.blank?)
next
end
origin = ''
if core.origin.kind_of?(Metasploit::Credential::Origin::Service)
origin = core.origin.service.host.address
elsif core.origin.kind_of?(Metasploit::Credential::Origin::Session)
origin = core.origin.session.host.address
end
if !origin.empty? && origin_ranges.present? && !origin_ranges.any? {|range| range.include?(origin) }
next
end
if core.logins.empty? && origin_ranges.empty?
tbl << [
"", # host
"", # cred
"", # service
core.public,
core.private,
core.realm,
core.private ? core.private.class.model_name.human : "",
]
else
core.logins.each do |login|
# 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?(login.service.host.address) }
next
end
row = [ login.service.host.address ]
row << origin
rhosts << login.service.host.address
if login.service.name.present?
row << "#{login.service.port}/#{login.service.proto} (#{login.service.name})"
else
row << "#{login.service.port}/#{login.service.proto}"
end
row += [
core.public,
core.private,
core.realm,
core.private ? core.private.class.model_name.human : "",
]
tbl << row
end
end
if mode == :delete
core.destroy
delete_count += 1
end
end
if output_file.nil?
print_line(tbl.to_s)
else
# create the output file
::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) }
print_status("Wrote creds to #{output_file}")
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
end
end end end end

View File

@ -14,7 +14,6 @@ class Db
require 'tempfile' require 'tempfile'
include Msf::Ui::Console::CommandDispatcher include Msf::Ui::Console::CommandDispatcher
include Metasploit::Credential::Creation
# #
# The dispatcher's name. # The dispatcher's name.
@ -40,7 +39,6 @@ class Db
"vulns" => "List all vulnerabilities in the database", "vulns" => "List all vulnerabilities in the database",
"notes" => "List all notes in the database", "notes" => "List all notes in the database",
"loot" => "List all loot in the database", "loot" => "List all loot in the database",
"creds" => "List all credentials in the database",
"db_import" => "Import a scan result file (filetype will be auto-detected)", "db_import" => "Import a scan result file (filetype will be auto-detected)",
"db_export" => "Export a file containing the contents of the database", "db_export" => "Export a file containing the contents of the database",
"db_nmap" => "Executes nmap and records the output automatically", "db_nmap" => "Executes nmap and records the output automatically",
@ -64,10 +62,6 @@ class Db
] ]
end end
def allowed_cred_types
%w(password ntlm hash)
end
# #
# Returns true if the db is connected, prints an error and returns # Returns true if the db is connected, prints an error and returns
# false if not. # false if not.
@ -861,387 +855,6 @@ class Db
} }
end end
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-ntlm <user> <ntlm hash> [domain]"
print_line " creds add-password <user> <password> [realm] [realm-type]"
print_line " creds add-ssh-key <user> </path/to/id_rsa> [realm-type]"
print_line "Where [realm type] can be one of:"
Metasploit::Model::Realm::Key::SHORT_NAMES.each do |short, description|
print_line " #{short} - #{description}"
end
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 format"
print_line " -d Delete one or more credentials"
print_line
print_line "Filter options for listing"
print_line " -P,--password <regex> List passwords that match this regex"
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 <regex> List users that match this regex"
print_line " -t,--type <type> List creds that match the following types: #{allowed_cred_types.join(',')}"
print_line " -O,--origins List creds that match these origins"
print_line " -R,--rhosts Set RHOSTS from the results of the search"
print_line
print_line "Examples, listing:"
print_line " creds # Default, returns all credentials"
print_line " creds 1.2.3.4/24 # nmap host specification"
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
print_line "Examples, adding:"
print_line " # Add a user with an NTLMHash"
print_line " creds add-ntlm alice 5cfe4c82d9ab8c66590f5b47cd6690f1:978a2e2e1dec9804c6b936f254727f9a"
print_line " # Add a user with a blank password and a domain"
print_line " creds add-password bob '' contosso"
print_line " # Add a user with an SSH key"
print_line " creds add-ssh-key root /root/.ssh/id_rsa"
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(private_type, username, password=nil, realm=nil, realm_type=nil)
cred_data = {
username: username,
private_data: password,
private_type: private_type,
workspace_id: framework.db.workspace,
origin_type: :import,
filename: "msfconsole"
}
if realm.present?
if realm_type.present?
realm_key = Metasploit::Model::Realm::Key::SHORT_NAMES[realm_type]
if realm_key.nil?
valid = Metasploit::Model::Realm::Key::SHORT_NAMES.keys.map{|n|"'#{n}'"}.join(", ")
print_error("Invalid realm type: #{realm_type}. Valid values: #{valid}")
return
end
end
realm_key ||= Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
cred_data.merge!(
realm_value: realm,
realm_key: realm_key
)
end
begin
create_credential(cred_data)
rescue ActiveRecord::RecordInvalid => e
print_error("Failed to add #{private_type}: #{e}")
end
end
def creds_add_non_replayable_hash(*args)
creds_add(:non_replayable_hash, *args)
end
def creds_add_ntlm_hash(*args)
creds_add(:ntlm_hash, *args)
end
def creds_add_password(*args)
creds_add(:password, *args)
end
def creds_add_ssh_key(username, *args)
key_file, realm = args
begin
key_data = File.read(key_file)
rescue ::Errno::EACCES, ::Errno::ENOENT => e
print_error("Failed to add ssh key: #{e}")
else
creds_add(:ssh_key, username, key_data, realm)
end
end
def creds_search(*args)
host_ranges = []
origin_ranges = []
port_ranges = []
svcs = []
rhosts = []
set_rhosts = false
#cred_table_columns = [ 'host', 'port', 'user', 'pass', 'type', 'proof', 'active?' ]
cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type' ]
user = nil
delete_count = 0
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)
when "-p","--port"
unless (arg_port_range(args.shift, port_ranges, true))
return
end
when "-t","--type"
ptype = args.shift
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]*/)
when "-P","--password"
pass = args.shift
if (!pass)
print_error("Argument required for -P")
return
end
when "-u","--user"
user = args.shift
if (!user)
print_error("Argument required for -u")
return
end
when "-d"
mode = :delete
when '-R', '--rhosts'
set_rhosts = true
when '-O', '--origins'
hosts = args.shift
if !hosts
print_error("Argument required for -O")
return
end
arg_host_range(hosts, origin_ranges)
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
when 'password'
Metasploit::Credential::Password
when 'hash'
Metasploit::Credential::PasswordHash
when 'ntlm'
Metasploit::Credential::NTLMHash
else
print_error("Unrecognized credential type #{ptype} -- must be one of #{allowed_cred_types.join(',')}")
return
end
end
# normalize
ports = port_ranges.flatten.uniq
svcs.flatten!
tbl_opts = {
'Header' => "Credentials",
'Columns' => cred_table_columns
}
tbl = Rex::Text::Table.new(tbl_opts)
::ActiveRecord::Base.connection_pool.with_connection {
query = Metasploit::Credential::Core.where( workspace_id: framework.db.workspace )
query = query.includes(:private, :public, :logins).references(:private, :public, :logins)
query = query.includes(logins: [ :service, { service: :host } ])
if type.present?
query = query.where(metasploit_credential_privates: { type: type })
end
if svcs.present?
query = query.where(Mdm::Service[:name].in(svcs))
end
if ports.present?
query = query.where(Mdm::Service[:port].in(ports))
end
if user.present?
# If we have a user regex, only include those that match
query = query.where('"metasploit_credential_publics"."username" ~* ?', user)
end
if pass.present?
# If we have a password regex, only include those that match
query = query.where('"metasploit_credential_privates"."data" ~* ?', pass)
end
if host_ranges.any? || ports.any? || svcs.any?
# Only find Cores that have non-zero Logins if the user specified a
# filter based on host, port, or service name
query = query.where(Metasploit::Credential::Login[:id].not_eq(nil))
end
query.find_each do |core|
# Exclude non-blank username creds if that's what we're after
if user == "" && core.public && !(core.public.username.blank?)
next
end
# Exclude non-blank password creds if that's what we're after
if pass == "" && core.private && !(core.private.data.blank?)
next
end
origin = ''
if core.origin.kind_of?(Metasploit::Credential::Origin::Service)
origin = core.origin.service.host.address
elsif core.origin.kind_of?(Metasploit::Credential::Origin::Session)
origin = core.origin.session.host.address
end
if !origin.empty? && origin_ranges.present? && !origin_ranges.any? {|range| range.include?(origin) }
next
end
if core.logins.empty? && origin_ranges.empty?
tbl << [
"", # host
"", # cred
"", # service
core.public,
core.private,
core.realm,
core.private ? core.private.class.model_name.human : "",
]
else
core.logins.each do |login|
# 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?(login.service.host.address) }
next
end
row = [ login.service.host.address ]
row << origin
rhosts << login.service.host.address
if login.service.name.present?
row << "#{login.service.port}/#{login.service.proto} (#{login.service.name})"
else
row << "#{login.service.port}/#{login.service.proto}"
end
row += [
core.public,
core.private,
core.realm,
core.private ? core.private.class.model_name.human : "",
]
tbl << row
end
end
if mode == :delete
core.destroy
delete_count += 1
end
end
if output_file.nil?
print_line(tbl.to_s)
else
# create the output file
::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) }
print_status("Wrote creds to #{output_file}")
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
#
# 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"
cmd_creds_help
return
end
subcommand = args.shift
case subcommand
when "add-ntlm"
creds_add_ntlm_hash(*args)
when "add-password"
creds_add_password(*args)
when "add-hash"
creds_add_non_replayable_hash(*args)
when "add-ssh-key"
creds_add_ssh_key(*args)
else
# then it's not actually a subcommand
args.unshift(subcommand) if subcommand
creds_search(*args)
end
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
def cmd_notes_help def cmd_notes_help
print_line "Usage: notes [-h] [-t <type1,type2>] [-n <data string>] [-a] [addr range]" print_line "Usage: notes [-h] [-t <type1,type2>] [-n <data string>] [-a] [addr range]"
print_line print_line

View File

@ -126,6 +126,8 @@ class Driver < Msf::Ui::Driver
if (framework.db.usable) if (framework.db.usable)
require 'msf/ui/console/command_dispatcher/db' require 'msf/ui/console/command_dispatcher/db'
enstack_dispatcher(CommandDispatcher::Db) enstack_dispatcher(CommandDispatcher::Db)
require 'msf/ui/console/command_dispatcher/creds'
enstack_dispatcher(CommandDispatcher::Creds)
else else
print_error("***") print_error("***")
if framework.db.error == "disabled" if framework.db.error == "disabled"

View File

@ -0,0 +1,449 @@
# frozen_string_literal: true
require 'spec_helper'
require 'msf/ui'
require 'msf/ui/console/command_dispatcher/creds'
RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
include_context 'Msf::DBManager'
include_context 'Msf::UIDriver'
subject(:creds) do
described_class.new(driver)
end
it { is_expected.to respond_to :active? }
it { is_expected.to respond_to :creds_add }
it { is_expected.to respond_to :creds_search }
describe '#cmd_creds' do
let(:username) { 'thisuser' }
let(:password) { 'thispass' }
let(:realm) { 'thisrealm' }
let(:realm_type) { 'Active Directory Domain' }
describe '-u' do
let(:nomatch_username) { 'thatuser' }
let(:nomatch_password) { 'thatpass' }
let(:blank_username) { '' }
let(:blank_password) { '' }
let(:nonblank_username) { 'nonblank_user' }
let(:nonblank_password) { 'nonblank_pass' }
let!(:origin) { FactoryGirl.create(:metasploit_credential_origin_import) }
before(:example) do
priv = FactoryGirl.create(:metasploit_credential_password, data: password)
pub = FactoryGirl.create(:metasploit_credential_username, username: username)
FactoryGirl.create(:metasploit_credential_core,
origin: origin,
private: priv,
public: pub,
realm: nil,
workspace: framework.db.workspace)
blank_pub = FactoryGirl.create(:metasploit_credential_blank_username)
nonblank_priv = FactoryGirl.create(:metasploit_credential_password, data: nonblank_password)
FactoryGirl.create(:metasploit_credential_core,
origin: origin,
private: nonblank_priv,
public: blank_pub,
realm: nil,
workspace: framework.db.workspace)
nonblank_pub = FactoryGirl.create(:metasploit_credential_username, username: nonblank_username)
blank_priv = FactoryGirl.create(:metasploit_credential_password, data: blank_password)
FactoryGirl.create(:metasploit_credential_core,
origin: origin,
private: blank_priv,
public: nonblank_pub,
realm: nil,
workspace: framework.db.workspace)
end
context 'when the credential is present' do
it 'should show a user that matches the given expression' do
creds.cmd_creds('-u', username)
expect(@output).to eq([
'Credentials',
'===========',
'',
'host origin service public private realm private_type',
'---- ------ ------- ------ ------- ----- ------------',
' thisuser thispass Password'
])
end
it 'should match a regular expression' do
creds.cmd_creds('-u', "^#{username}$")
expect(@output).to eq([
'Credentials',
'===========',
'',
'host origin service public private realm private_type',
'---- ------ ------- ------ ------- ----- ------------',
' thisuser thispass Password'
])
end
it 'should return nothing for a non-matching regular expression' do
creds.cmd_creds('-u', "^#{nomatch_username}$")
expect(@output).to eq([
'Credentials',
'===========',
'',
'host origin service public private realm private_type',
'---- ------ ------- ------ ------- ----- ------------'
])
end
context 'and when the username is blank' do
it 'should show a user that matches the given expression' do
creds.cmd_creds('-u', blank_username)
expect(@output).to eq([
'Credentials',
'===========',
'',
'host origin service public private realm private_type',
'---- ------ ------- ------ ------- ----- ------------',
' nonblank_pass Password'
])
end
end
context 'and when the password is blank' do
it 'should show a user that matches the given expression' do
creds.cmd_creds('-P', blank_password)
expect(@output).to eq([
'Credentials',
'===========',
'',
'host origin service public private realm private_type',
'---- ------ ------- ------ ------- ----- ------------',
' nonblank_user Password'
])
end
end
end
context 'when the credential is absent' do
context 'due to a nonmatching username' do
it 'should return a blank set' do
creds.cmd_creds('-u', nomatch_username)
expect(@output).to eq([
'Credentials',
'===========',
'',
'host origin service public private realm private_type',
'---- ------ ------- ------ ------- ----- ------------'
])
end
end
context 'due to a nonmatching password' do
it 'should return a blank set' do
creds.cmd_creds('-P', nomatch_password)
expect(@output).to eq([
'Credentials',
'===========',
'',
'host origin service public private realm private_type',
'---- ------ ------- ------ ------- ----- ------------'
])
end
end
end
end
describe '-t' do
context 'with an invalid type' do
it 'should print the list of valid types' do
creds.cmd_creds('-t', 'asdf')
expect(@error).to match_array [
'Unrecognized credential type asdf -- must be one of password,ntlm,hash'
]
end
end
context 'with valid types' do
let(:ntlm_hash) { '1443d06412d8c0e6e72c57ef50f76a05:27c433245e4763d074d30a05aae0af2c' }
let!(:pub) do
FactoryGirl.create(:metasploit_credential_username, username: username)
end
let!(:password_core) do
priv = FactoryGirl.create(:metasploit_credential_password, data: password)
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: pub,
realm: nil,
workspace: framework.db.workspace)
end
# # Somehow this is hitting a unique constraint on Cores with the same
# # Public, even though it has a different Private. Skip for now
# let!(:ntlm_core) do
# priv = FactoryGirl.create(:metasploit_credential_ntlm_hash, data: ntlm_hash)
# FactoryGirl.create(:metasploit_credential_core,
# origin: FactoryGirl.create(:metasploit_credential_origin_import),
# private: priv,
# public: pub,
# realm: nil,
# workspace: framework.db.workspace)
# end
# let!(:nonreplayable_core) do
# priv = FactoryGirl.create(:metasploit_credential_nonreplayable_hash, data: 'asdf')
# FactoryGirl.create(:metasploit_credential_core,
# origin: FactoryGirl.create(:metasploit_credential_origin_import),
# private: priv,
# public: pub,
# realm: nil,
# workspace: framework.db.workspace)
# end
after(:example) do
# ntlm_core.destroy
password_core.destroy
# nonreplayable_core.destroy
end
context 'password' do
it 'should show just the password' do
creds.cmd_creds('-t', 'password')
# Table matching really sucks
expect(@output).to eq([
'Credentials',
'===========',
'',
'host origin service public private realm private_type',
'---- ------ ------- ------ ------- ----- ------------',
' thisuser thispass Password'
])
end
end
context 'ntlm' do
it 'should show just the ntlm' do
skip 'Weird uniqueness constraint on Core (workspace_id, public_id)'
creds.cmd_creds('-t', 'ntlm')
# Table matching really sucks
expect(@output).to =~ [
'Credentials',
'===========',
'',
'host service public private realm private_type',
'---- ------- ------ ------- ----- ------------',
" thisuser #{ntlm_hash} NTLM hash"
]
end
end
end
end
describe 'add' do
let(:pub) { FactoryGirl.create(:metasploit_credential_username, username: username) }
let(:priv) { FactoryGirl.create(:metasploit_credential_password, data: password) }
let(:r) { FactoryGirl.create(:metasploit_credential_realm, key: realm_type, value: realm) }
context 'username password and realm' do
it 'creates a core if one does not exist' do
expect {
creds.cmd_creds('add', "user:#{username}", "password:#{password}", "realm:#{realm}")
}.to change { Metasploit::Credential::Core.count }.by 1
end
it 'does not create a core if it already exists' do
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: pub,
realm: r,
workspace: framework.db.workspace)
expect {
creds.cmd_creds('add', "user:#{username}", "password:#{password}", "realm:#{realm}")
}.to_not change { Metasploit::Credential::Core.count }
end
end
context 'username and realm' do
it 'creates a core if one does not exist' do
expect {
creds.cmd_creds('add', "user:#{username}", "realm:#{realm}")
}.to change { Metasploit::Credential::Core.count }.by 1
end
it 'does not create a core if it already exists' do
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: nil,
public: pub,
realm: r,
workspace: framework.db.workspace)
expect {
creds.cmd_creds('add', "user:#{username}", "realm:#{realm}")
}.to_not change { Metasploit::Credential::Core.count }
end
end
context 'username and password' do
it 'creates a core if one does not exist' do
expect {
creds.cmd_creds('add', "user:#{username}", "password:#{password}")
}.to change { Metasploit::Credential::Core.count }.by 1
end
it 'does not create a core if it already exists' do
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: pub,
realm: nil,
workspace: framework.db.workspace)
expect {
creds.cmd_creds('add', "user:#{username}", "password:#{password}")
}.to_not change { Metasploit::Credential::Core.count }
end
end
context 'password and realm' do
it 'creates a core if one does not exist' do
expect {
creds.cmd_creds('add', "password:#{password}", "realm:#{realm}")
}.to change { Metasploit::Credential::Core.count }.by 1
end
it 'does not create a core if it already exists' do
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: nil,
realm: r,
workspace: framework.db.workspace)
expect {
creds.cmd_creds('add', "password:#{password}", "realm:#{realm}")
}.to_not change { Metasploit::Credential::Core.count }
end
end
context 'username' do
it 'creates a core if one does not exist' do
expect {
creds.cmd_creds('add', "user:#{username}")
}.to change { Metasploit::Credential::Core.count }.by 1
end
it 'does not create a core if it already exists' do
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: nil,
public: pub,
realm: nil,
workspace: framework.db.workspace)
expect {
creds.cmd_creds('add', "user:#{username}")
}.to_not change { Metasploit::Credential::Core.count }
end
end
context 'private_types' do
context 'password' do
it 'creates a core if one does not exist' do
expect {
creds.cmd_creds('add', "password:#{password}")
}.to change { Metasploit::Credential::Core.count }.by 1
end
it 'does not create a core if it already exists' do
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: nil,
realm: nil,
workspace: framework.db.workspace)
expect {
creds.cmd_creds('add', "password:#{password}")
}.to_not change { Metasploit::Credential::Core.count }
end
end
context 'ntlm' do
let(:priv) { FactoryGirl.create(:metasploit_credential_ntlm_hash) }
it 'creates a core if one does not exist' do
expect {
creds.cmd_creds('add', "ntlm:#{priv.data}")
}.to change { Metasploit::Credential::Core.count }.by 1
end
it 'does not create a core if it already exists' do
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: nil,
realm: nil,
workspace: framework.db.workspace)
expect {
creds.cmd_creds('add', "ntlm:#{priv.data}")
}.to_not change { Metasploit::Credential::Core.count }
end
end
context 'hash' do
let(:priv) { FactoryGirl.create(:metasploit_credential_nonreplayable_hash) }
it 'creates a core if one does not exist' do
expect {
creds.cmd_creds('add', "hash:#{priv.data}")
}.to change { Metasploit::Credential::Core.count }.by 1
end
it 'does not create a core if it already exists' do
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: nil,
realm: nil,
workspace: framework.db.workspace)
expect {
creds.cmd_creds('add', "hash:#{priv.data}")
}.to_not change { Metasploit::Credential::Core.count }
end
end
context 'ssh-key' do
let(:priv) { FactoryGirl.create(:metasploit_credential_ssh_key) }
before(:each) do
@file = Tempfile.new('id_rsa')
@file.write(priv.data)
@file.close
end
it 'creates a core if one does not exist' do
expect {
creds.cmd_creds('add', "user:#{username}", "ssh-key:#{@file.path}")
}.to change { Metasploit::Credential::Core.count }.by 1
end
it 'does not create a core if it already exists' do
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: pub,
realm: nil,
workspace: framework.db.workspace)
expect {
creds.cmd_creds('add', "user:#{username}", "ssh-key:#{@file.path}")
}.to_not change { Metasploit::Credential::Core.count }
end
end
end
context 'realm-types' do
Metasploit::Model::Realm::Key::SHORT_NAMES.each do |short_name, long_name|
context "#{short_name}" do
let(:r) { FactoryGirl.create(:metasploit_credential_realm, key: long_name) }
it 'creates a core if one does not exist' do
expect {
creds.cmd_creds('add', "realm:#{r.value}", "realm-type:#{short_name}")
}.to change { Metasploit::Credential::Core.count }.by 1
end
it 'does not create a core if it already exists' do
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: nil,
public: nil,
realm: r,
workspace: framework.db.workspace)
expect {
creds.cmd_creds('add', "realm:#{r.value}", "realm-type:#{short_name}")
}.to_not change { Metasploit::Credential::Core.count }
end
end
end
end
end
end
end

View File

@ -14,8 +14,6 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do
it { is_expected.to respond_to :active? } it { is_expected.to respond_to :active? }
it { is_expected.to respond_to :arg_host_range } it { is_expected.to respond_to :arg_host_range }
it { is_expected.to respond_to :arg_port_range } it { is_expected.to respond_to :arg_port_range }
it { is_expected.to respond_to :cmd_creds_help }
it { is_expected.to respond_to :cmd_creds_tabs }
it { is_expected.to respond_to :cmd_db_autopwn } it { is_expected.to respond_to :cmd_db_autopwn }
it { is_expected.to respond_to :cmd_db_autopwn_help } it { is_expected.to respond_to :cmd_db_autopwn_help }
it { is_expected.to respond_to :cmd_db_connect } it { is_expected.to respond_to :cmd_db_connect }
@ -47,12 +45,6 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do
it { is_expected.to respond_to :cmd_workspace_help } it { is_expected.to respond_to :cmd_workspace_help }
it { is_expected.to respond_to :cmd_workspace_tabs } it { is_expected.to respond_to :cmd_workspace_tabs }
it { is_expected.to respond_to :commands } it { is_expected.to respond_to :commands }
it { is_expected.to respond_to :creds_add }
it { is_expected.to respond_to :creds_add_non_replayable_hash }
it { is_expected.to respond_to :creds_add_ntlm_hash }
it { is_expected.to respond_to :creds_add_password }
it { is_expected.to respond_to :creds_add_ssh_key }
it { is_expected.to respond_to :creds_search }
it { is_expected.to respond_to :db_check_driver } it { is_expected.to respond_to :db_check_driver }
it { is_expected.to respond_to :db_connect_postgresql } it { is_expected.to respond_to :db_connect_postgresql }
it { is_expected.to respond_to :db_find_tools } it { is_expected.to respond_to :db_find_tools }
@ -63,258 +55,6 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do
it { is_expected.to respond_to :name } it { is_expected.to respond_to :name }
it { is_expected.to respond_to :set_rhosts_from_addrs } it { is_expected.to respond_to :set_rhosts_from_addrs }
describe "#cmd_creds" do
let(:username) { "thisuser" }
let(:password) { "thispass" }
describe "-u" do
let(:nomatch_username) { "thatuser" }
let(:nomatch_password) { "thatpass" }
let(:blank_username) { "" }
let(:blank_password) { "" }
let(:nonblank_username) { "nonblank_user" }
let(:nonblank_password) { "nonblank_pass" }
let!(:origin) { FactoryGirl.create(:metasploit_credential_origin_import) }
before(:example) do
priv = FactoryGirl.create(:metasploit_credential_password, data: password)
pub = FactoryGirl.create(:metasploit_credential_username, username: username)
FactoryGirl.create(:metasploit_credential_core,
origin: origin,
private: priv,
public: pub,
realm: nil,
workspace: framework.db.workspace)
blank_pub = FactoryGirl.create(:metasploit_credential_blank_username)
nonblank_priv = FactoryGirl.create(:metasploit_credential_password, data: nonblank_password)
FactoryGirl.create(:metasploit_credential_core,
origin: origin,
private: nonblank_priv,
public: blank_pub,
realm: nil,
workspace: framework.db.workspace)
nonblank_pub = FactoryGirl.create(:metasploit_credential_username, username: nonblank_username)
blank_priv = FactoryGirl.create(:metasploit_credential_password, data: blank_password)
FactoryGirl.create(:metasploit_credential_core,
origin: origin,
private: blank_priv,
public: nonblank_pub,
realm: nil,
workspace: framework.db.workspace)
end
context "when the credential is present" do
it "should show a user that matches the given expression" do
db.cmd_creds("-u", username)
expect(@output).to eq([
"Credentials",
"===========",
"",
"host origin service public private realm private_type",
"---- ------ ------- ------ ------- ----- ------------",
" thisuser thispass Password"
])
end
it 'should match a regular expression' do
subject.cmd_creds("-u", "^#{username}$")
expect(@output).to eq([
"Credentials",
"===========",
"",
"host origin service public private realm private_type",
"---- ------ ------- ------ ------- ----- ------------",
" thisuser thispass Password"
])
end
it 'should return nothing for a non-matching regular expression' do
subject.cmd_creds("-u", "^#{nomatch_username}$")
expect(@output).to eq([
"Credentials",
"===========",
"",
"host origin service public private realm private_type",
"---- ------ ------- ------ ------- ----- ------------"
])
end
context "and when the username is blank" do
it "should show a user that matches the given expression" do
db.cmd_creds("-u", blank_username)
expect(@output).to eq([
"Credentials",
"===========",
"",
"host origin service public private realm private_type",
"---- ------ ------- ------ ------- ----- ------------",
" nonblank_pass Password"
])
end
end
context "and when the password is blank" do
it "should show a user that matches the given expression" do
db.cmd_creds("-P", blank_password)
expect(@output).to eq([
"Credentials",
"===========",
"",
"host origin service public private realm private_type",
"---- ------ ------- ------ ------- ----- ------------",
" nonblank_user Password"
])
end
end
end
context "when the credential is absent" do
context "due to a nonmatching username" do
it "should return a blank set" do
db.cmd_creds("-u", nomatch_username)
expect(@output).to eq([
"Credentials",
"===========",
"",
"host origin service public private realm private_type",
"---- ------ ------- ------ ------- ----- ------------"
])
end
end
context "due to a nonmatching password" do
it "should return a blank set" do
db.cmd_creds("-P", nomatch_password)
expect(@output).to eq([
"Credentials",
"===========",
"",
"host origin service public private realm private_type",
"---- ------ ------- ------ ------- ----- ------------"
])
end
end
end
end
describe "-t" do
context "with an invalid type" do
it "should print the list of valid types" do
db.cmd_creds("-t", "asdf")
expect(@error).to match_array [
"Unrecognized credential type asdf -- must be one of password,ntlm,hash"
]
end
end
context "with valid types" do
let(:ntlm_hash) { "1443d06412d8c0e6e72c57ef50f76a05:27c433245e4763d074d30a05aae0af2c" }
let!(:pub) do
FactoryGirl.create(:metasploit_credential_username, username: username)
end
let!(:password_core) do
priv = FactoryGirl.create(:metasploit_credential_password, data: password)
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: pub,
realm: nil,
workspace: framework.db.workspace)
end
=begin
# Somehow this is hitting a unique constraint on Cores with the same
# Public, even though it has a different Private. Skip for now
let!(:ntlm_core) do
priv = FactoryGirl.create(:metasploit_credential_ntlm_hash, data: ntlm_hash)
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: pub,
realm: nil,
workspace: framework.db.workspace)
end
let!(:nonreplayable_core) do
priv = FactoryGirl.create(:metasploit_credential_nonreplayable_hash, data: 'asdf')
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: pub,
realm: nil,
workspace: framework.db.workspace)
end
=end
after(:example) do
#ntlm_core.destroy
password_core.destroy
#nonreplayable_core.destroy
end
context "password" do
it "should show just the password" do
db.cmd_creds("-t", "password")
# Table matching really sucks
expect(@output).to eq([
"Credentials",
"===========",
"",
"host origin service public private realm private_type",
"---- ------ ------- ------ ------- ----- ------------",
" thisuser thispass Password"
])
end
end
context "ntlm" do
it "should show just the ntlm" do
skip "Weird uniqueness constraint on Core (workspace_id, public_id)"
db.cmd_creds("-t", "ntlm")
# Table matching really sucks
expect(@output).to =~ [
"Credentials",
"===========",
"",
"host service public private realm private_type",
"---- ------- ------ ------- ----- ------------",
" thisuser #{ntlm_hash } NTLM hash"
]
end
end
end
end
describe "add-password" do
context "when no core exists" do
it "should add a Core" do
expect {
subject.cmd_creds("add-password", username, password)
}.to change{ Metasploit::Credential::Core.count }.by 1
end
end
context "when a core already exists" do
before(:example) do
priv = FactoryGirl.create(:metasploit_credential_password, data: password)
pub = FactoryGirl.create(:metasploit_credential_username, username: username)
FactoryGirl.create(:metasploit_credential_core,
origin: FactoryGirl.create(:metasploit_credential_origin_import),
private: priv,
public: pub,
realm: nil,
workspace: framework.db.workspace)
end
it "should not add a Core" do
expect {
subject.cmd_creds("add-password", username, password)
}.to_not change{ Metasploit::Credential::Core.count }
end
end
end
end
describe "#cmd_db_export" do describe "#cmd_db_export" do
describe "-h" do describe "-h" do
it "should show a help message" do it "should show a help message" do