Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the DB_SKIP_EXISTING option to the AuthBrute mixin #15630

Merged
merged 9 commits into from Sep 24, 2021
26 changes: 24 additions & 2 deletions lib/metasploit/framework/credential_collection.rb
Expand Up @@ -35,6 +35,10 @@ class PrivateCredentialCollection
# @return [String]
attr_accessor :realm

# @!attribute filter
# A block that can be used to filter credential objects
attr_accessor :filter

# @option opts [Boolean] :blank_passwords See {#blank_passwords}
# @option opts [String] :pass_file See {#pass_file}
# @option opts [String] :password See {#password}
Expand All @@ -49,6 +53,7 @@ def initialize(opts = {})
end
self.prepended_creds ||= []
self.additional_privates ||= []
self.filter = nil
end

# Adds a string as an additional private credential
Expand All @@ -70,12 +75,20 @@ def prepend_cred(cred)
self
end

def each_filtered
each_unfiltered do |credential|
next unless self.filter.nil? || self.filter.call(credential)

yield credential
end
end

# Combines all the provided credential sources into a stream of {Credential}
# objects, yielding them one at a time
#
# @yieldparam credential [Metasploit::Framework::Credential]
# @return [void]
def each
def each_unfiltered
if pass_file.present?
pass_fd = File.open(pass_file, 'r:binary')
end
Expand Down Expand Up @@ -109,13 +122,22 @@ def empty?
prepended_creds.empty? && !has_privates?
end

# Returns true when a filter is defined
#
# @return [Boolean]
def filtered?
!self.filter.nil?
end

# Returns true when there are any private values set
#
# @return [Boolean]
def has_privates?
password.present? || pass_file.present? || !additional_privates.empty? || blank_passwords
end

alias each each_filtered

protected

# Analyze a private value to determine its type by checking it against a known list of regular expressions
Expand Down Expand Up @@ -189,7 +211,7 @@ def add_public(public_str='')
#
# @yieldparam credential [Metasploit::Framework::Credential]
# @return [void]
def each
def each_unfiltered
if pass_file.present?
pass_fd = File.open(pass_file, 'r:binary')
end
Expand Down
67 changes: 66 additions & 1 deletion lib/msf/core/auxiliary/auth_brute.rb
Expand Up @@ -25,6 +25,7 @@ def initialize(info = {})
OptBool.new('DB_ALL_CREDS', [false,"Try each user/password couple stored in the current database",false]),
OptBool.new('DB_ALL_USERS', [false,"Add all users in the current database to the list",false]),
OptBool.new('DB_ALL_PASS', [false,"Add all passwords in the current database to the list",false]),
OptEnum.new('DB_SKIP_EXISTING', [false,"Skip existing credentials stored in the current database", 'none', %w[ none user user&realm ]]),
OptBool.new('STOP_ON_SUCCESS', [ true, "Stop guessing when a credential works for a host", false]),
], Auxiliary::AuthBrute)

Expand All @@ -45,6 +46,70 @@ def initialize(info = {})
], Auxiliary::AuthBrute)
end

# Build a new CredentialCollection instance configured based on the datastore options. Any options passed in will take
# precedence over the datastore. Usernames and passwords will be prepended to the credential collection if their
# respective datastore options are configured appropriately. Finally the resulting CredentialCollection will be
# configured to perform any necessary filtering per the DB_SKIP_EXISTING option.
#
# @param [Hash] opts the options with which to build the CredentialCollection instance
# @return [Metasploit::Framework::CredentialCollection] the built CredentialCollection
def build_credential_collection(opts)
cred_collection = Metasploit::Framework::CredentialCollection.new({
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
user_as_pass: datastore['USER_AS_PASS'],
}.merge(opts))

if framework.db.active
cred_collection = prepend_db_usernames(cred_collection)
cred_collection = prepend_db_passwords(cred_collection)
else
ignored = %w{ DB_ALL_CREDS DB_ALL_PASS DB_ALL_USERS }.select { |option| datastore[option] }
ignored << 'DB_SKIP_EXISTING' unless datastore['DB_SKIP_EXISTING'].blank? || datastore['DB_SKIP_EXISTING'] == 'none'
unless ignored.empty?
print_warning("No active DB -- The following option#{ ignored.length == 1 ? '' : 's'} will be ignored: #{ ignored.join(', ') }")
end
end

# only define the filter if any filtering needs to take place
unless datastore['DB_SKIP_EXISTING'].blank? || datastore['DB_SKIP_EXISTING'] == 'none'
cred_collection.filter = -> (cred) do
return true unless datastore['DB_SKIP_EXISTING']
return true unless framework.db.active
opts = { workspace: myworkspace.name }

opts[:type] =
case cred.private_type
when :ntlm_hash
'Metasploit::Credential::NTLMHash'
when :password
'Metasploit::Credential::Password'
when :ssh_key
'Metasploit::Credential::SSHKey'
else
return true # not a private type that we can filter on
end

case datastore['DB_SKIP_EXISTING']
when 'user'
opts[:user] = cred.public
when 'user&realm'
opts[:user] = cred.public
opts[:realm] = cred.realm
else
return true
end

# cred[@public, @private, @private_type[:password], @realm]
framework.db.creds(opts).length == 0
end
end

cred_collection
end

def setup
@@max_per_service = nil
end
Expand Down Expand Up @@ -611,7 +676,7 @@ def vprint_good(msg='')
print_brute :level => :vgood, :msg => msg
end

# Provides a consistant way to display messages about AuthBrute-mixed modules.
# Provides a consistent way to display messages about AuthBrute-mixed modules.
# Acceptable opts are fairly self-explanatory, but :level can be tricky.
#
# It can be one of status, good, error, or line (and corresponds to the usual
Expand Down
8 changes: 6 additions & 2 deletions lib/msf/core/db_manager/cred.rb
Expand Up @@ -31,8 +31,12 @@ def creds(opts)
query = query.where(Mdm::Service[:port].in(opts[:ports]))
end

if opts[:realm].present?
query = query.where('"metasploit_credential_realms"."value" = ?', opts[:realm])
if opts.key?(:realm)
zeroSteiner marked this conversation as resolved.
Show resolved Hide resolved
if opts[:realm].nil?
query = query.where( realm: nil )
else
query = query.where('"metasploit_credential_realms"."value" = ?', opts[:realm])
end
end

if opts[:user].present?
Expand Down
7 changes: 3 additions & 4 deletions modules/auxiliary/scanner/acpp/login.rb
Expand Up @@ -39,6 +39,7 @@ def initialize
# there is no username, so remove all of these options
'DB_ALL_USERS',
'DB_ALL_CREDS',
'DB_SKIP_EXISTING',
'PASSWORD_SPRAY',
'USERNAME',
'USERPASS_FILE',
Expand All @@ -52,13 +53,11 @@ def initialize
def run_host(ip)
vprint_status("#{ip}:#{rport} - Starting ACPP login sweep")

cred_collection = Metasploit::Framework::CredentialCollection.new(
cred_collection = Metasploit::Framework::PrivateCredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
username: '<BLANK>'
password: datastore['PASSWORD']
)

cred_collection = prepend_db_passwords(cred_collection)

scanner = Metasploit::Framework::LoginScanner::ACPP.new(
Expand Down
11 changes: 2 additions & 9 deletions modules/auxiliary/scanner/afp/afp_login.rb
Expand Up @@ -43,18 +43,11 @@ def initialize(info={})
def run_host(ip)
print_status("Scanning IP: #{ip.to_s}")

cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
cred_collection = build_credential_collection(
username: datastore['USERNAME'],
user_as_pass: datastore['USER_AS_PASS'],
password: datastore['PASSWORD'],
)

cred_collection = prepend_db_passwords(cred_collection)

scanner = Metasploit::Framework::LoginScanner::AFP.new(
host: ip,
port: rport,
Expand Down
13 changes: 3 additions & 10 deletions modules/auxiliary/scanner/db2/db2_auth.rb
Expand Up @@ -41,19 +41,12 @@ def initialize
end

def run_host(ip)
cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
cred_collection = build_credential_collection(
realm: datastore['DATABASE'],
username: datastore['USERNAME'],
user_as_pass: datastore['USER_AS_PASS'],
realm: datastore['DATABASE']
password: datastore['PASSWORD']
)

cred_collection = prepend_db_passwords(cred_collection)

scanner = Metasploit::Framework::LoginScanner::DB2.new(
host: ip,
port: rport,
Expand Down
11 changes: 2 additions & 9 deletions modules/auxiliary/scanner/ftp/ftp_login.rb
Expand Up @@ -54,19 +54,12 @@ def initialize
def run_host(ip)
print_status("#{ip}:#{rport} - Starting FTP login sweep")

cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
cred_collection = build_credential_collection(
username: datastore['USERNAME'],
user_as_pass: datastore['USER_AS_PASS'],
password: datastore['PASSWORD'],
prepended_creds: anonymous_creds
)

cred_collection = prepend_db_passwords(cred_collection)

scanner = Metasploit::Framework::LoginScanner::FTP.new(
host: ip,
port: rport,
Expand Down
11 changes: 3 additions & 8 deletions modules/auxiliary/scanner/http/advantech_webaccess_login.rb
Expand Up @@ -38,14 +38,9 @@ def initialize(info={})

def scanner(ip)
@scanner ||= lambda {
cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
username: datastore['USERNAME'],
user_as_pass: datastore['USER_AS_PASS']
cred_collection = build_credential_collection(
username: datastore['USERNAME'],
password: datastore['PASSWORD']
)

if datastore['TRYDEFAULT']
Expand Down
5 changes: 3 additions & 2 deletions modules/auxiliary/scanner/http/appletv_login.rb
Expand Up @@ -51,8 +51,8 @@ def initialize
)])

deregister_options(
'USERNAME', 'USER_AS_PASS', 'DB_ALL_CREDS', 'DB_ALL_USERS', 'NTLM::SendLM', 'NTLM::SendNTLM',
'NTLM::SendSPN', 'NTLM::UseLMKey', 'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2',
'USERNAME', 'USER_AS_PASS', 'DB_ALL_CREDS', 'DB_ALL_USERS', 'DB_SKIP_EXISTING',
'NTLM::SendLM', 'NTLM::SendNTLM', 'NTLM::SendSPN', 'NTLM::UseLMKey', 'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2',
'REMOVE_USERPASS_FILE', 'REMOVE_USER_FILE', 'DOMAIN', 'HttpUsername', 'PASSWORD_SPRAY'
)
end
Expand All @@ -67,6 +67,7 @@ def run_host(ip)
username: 'AirPlay',
user_as_pass: datastore['USER_AS_PASS'],
)
cred_collection = prepend_db_passwords(cred_collection)
else
print_status("Attempting to login to #{uri} by 'Onscreen Code'")
cred_collection = LockCodeCollection.new
Expand Down
11 changes: 2 additions & 9 deletions modules/auxiliary/scanner/http/axis_login.rb
Expand Up @@ -64,18 +64,11 @@ def run_host(ip)

print_status "#{target_url} - Apache Axis - Attempting authentication"

cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
cred_collection = build_credential_collection(
username: datastore['USERNAME'],
user_as_pass: datastore['USER_AS_PASS'],
password: datastore['PASSWORD']
)

cred_collection = prepend_db_passwords(cred_collection)

scanner = Metasploit::Framework::LoginScanner::Axis2.new(
configure_http_login_scanner(
uri: uri,
Expand Down
11 changes: 3 additions & 8 deletions modules/auxiliary/scanner/http/bavision_cam_login.rb
Expand Up @@ -35,14 +35,9 @@ def initialize(info={})

def scanner(ip)
@scanner ||= lambda {
cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
username: datastore['USERNAME'],
user_as_pass: datastore['USER_AS_PASS']
cred_collection = build_credential_collection(
username: datastore['USERNAME'],
password: datastore['PASSWORD']
)

if datastore['TRYDEFAULT']
Expand Down
9 changes: 2 additions & 7 deletions modules/auxiliary/scanner/http/buffalo_login.rb
Expand Up @@ -32,14 +32,9 @@ def initialize
end

def run_host(ip)
cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
cred_collection = build_credential_collection(
username: datastore['USERNAME'],
user_as_pass: datastore['USER_AS_PASS']
password: datastore['PASSWORD']
)

scanner = Metasploit::Framework::LoginScanner::Buffalo.new(
Expand Down
8 changes: 3 additions & 5 deletions modules/auxiliary/scanner/http/caidao_bruteforce_login.rb
Expand Up @@ -43,13 +43,11 @@ def initialize(info = {})

def scanner(ip)
@scanner ||= lambda {
cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
cred_collection = build_credential_collection(
# The LoginScanner API refuses to run if there's no username, so we give it a fake one.
# But we will not be reporting this to the database.
username: 'caidao'
username: 'caidao',
password: datastore['PASSWORD']
)

return Metasploit::Framework::LoginScanner::Caidao.new(
Expand Down
11 changes: 3 additions & 8 deletions modules/auxiliary/scanner/http/chef_webui_login.rb
Expand Up @@ -130,14 +130,9 @@ def do_report(ip, port, result)
end

def init_loginscanner(ip)
@cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
username: datastore['USERNAME'],
user_as_pass: datastore['USER_AS_PASS']
@cred_collection = build_credential_collection(
username: datastore['USERNAME'],
password: datastore['PASSWORD']
)

# Always try the default first
Expand Down