42d1fae2e6
The -S flag for console commands, backed by search functionality in Rex' tables, originally pushed upstream in #1604 (iirc), lacks coverage for a number of commands which benefit a good deal from inline filtering of the potentially large number of results. Push more -S flags and surrounding table functionality upstream to provide coverage for the console commands included in framework. Include a fix for deleting hosts when DB references are a problem. Include a fix for the upstream route command wherein scope must be defined for the routing target by assuming a /32 without explicit definition. Note: With this in place, console behavior when filtering results is roughly analagous to the R7 filtering in web UI, which should help those of us trying to use both maintain corresponding workflows. Testing: Used in-house for years, though changes to the diff from upstream and our fork (expunging some internal code) are untested, so would appreciate eyes and hands on.
526 lines
16 KiB
Ruby
526 lines
16 KiB
Ruby
# -*- 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
|
|
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)
|
|
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','address','port','protocol', 'service-name')
|
|
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
|
|
|
|
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,
|
|
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
|
|
if login_keys.any?
|
|
data[:address] = params['address']
|
|
data[:port] = params['port']
|
|
data[:protocol] = params['protocol']
|
|
data[:service_name] = params['service-name']
|
|
create_credential_and_login(data)
|
|
else
|
|
create_credential(data)
|
|
end
|
|
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
|
|
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)
|
|
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)
|
|
when '-S', '--search-term'
|
|
search_term = 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
|
|
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,
|
|
'SearchTerm' => search_term
|
|
}
|
|
|
|
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
|