Merge pull request #102 from rapid7/feature/MSP-9707/smb-bruteforce-refactor

Feature/msp 9707/smb bruteforce refactor

MSP-9707 #land
bug/bundler_fix
Trevor Rosen 2014-07-11 11:33:12 -05:00
commit cc93dbbe29
5 changed files with 207 additions and 260 deletions

View File

@ -27,8 +27,6 @@ end
# Create a custom group
group :local do
# Use pry to help view and interact with objects in the framework
gem 'pry', '~> 0.9'
# Use pry-debugger to step through code during development
gem 'pry-debugger', '~> 0.2'
# Add the lab gem so that the 'lab' plugin will work again

View File

@ -17,6 +17,19 @@ module Metasploit
include Metasploit::Framework::LoginScanner::RexSocket
include Metasploit::Framework::LoginScanner::NTLM
# Constants to be used in {Result#access_level}
module AccessLevels
# Administrative access. For SMB, this is defined as being
# able to successfully Tree Connect to the `ADMIN$` share.
# This definition is not without its problems, but suffices to
# conclude that such a user will most likely be able to use
# psexec.
ADMINISTRATOR = "Administrator"
# Guest access means our creds were accepted but the logon
# session is not associated with a real user account.
GUEST = "Guest"
end
CAN_GET_SESSION = true
DEFAULT_REALM = 'WORKSTATION'
LIKELY_PORTS = [ 139, 445 ]
@ -100,6 +113,27 @@ module Metasploit
allow_nil: true
# If login is successul and {Result#access_level} is not set
# then arbitrary credentials are accepted. If it is set to
# Guest, then arbitrary credentials are accepted, but given
# Guest permissions.
#
# @param domain [String] Domain to authenticate against. Use an
# empty string for local accounts.
# @return [Result]
def attempt_bogus_login(domain)
if defined?(@result_for_bogus)
return @result_for_bogus
end
cred = Credential.new(
public: Rex::Text.rand_text_alpha(8),
private: Rex::Text.rand_text_alpha(8),
realm: domain
)
@result_for_bogus = attempt_login(cred)
end
# (see Base#attempt_login)
def attempt_login(credential)
@ -121,7 +155,7 @@ module Metasploit
begin
# TODO: OMG
ok = simple.login(
simple.login(
smb_name,
credential.public,
credential.private,
@ -140,16 +174,29 @@ module Metasploit
}
)
simple.connect("\\\\#{smb_name}\\IPC$")
status = ok ? :success : :failed
rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
# Windows SMB will return an error code during Session
# Setup, but nix Samba requires a Tree Connect. Try admin$
# first, since that will tell us if this user has local
# admin access. Fall back to IPC$ which should be accessible
# to any user with valid creds.
begin
simple.connect("\\\\#{host}\\admin$")
access_level = AccessLevels::ADMINISTRATOR
simple.disconnect("\\\\#{host}\\admin$")
rescue ::Rex::Proto::SMB::Exceptions::ErrorCode
simple.connect("\\\\#{host}\\IPC$")
end
# If we made it this far without raising, we have a valid
# login
status = :success
rescue ::Rex::Proto::SMB::Exceptions::LoginError => e
status = case e.get_error(e.error_code)
when *StatusCodes::CORRECT_CREDENTIAL_STATUS_CODES
:correct
when 'STATUS_LOGON_FAILURE', 'STATUS_ACCESS_DENIED'
:failed
else
puts e.backtrace.join
:failed
end
@ -161,7 +208,11 @@ module Metasploit
status = :connection_error
end
Result.new(credential: credential, status: status, proof: proof)
if status == :success && simple.client.auth_user.nil?
access_level ||= AccessLevels::GUEST
end
Result.new(credential: credential, status: status, proof: proof, access_level: access_level)
end
def connect
@ -206,6 +257,7 @@ module Metasploit
self.smb_name = self.host if self.smb_name.nil?
end
end
end
end

View File

@ -736,6 +736,10 @@ class Error < ::RuntimeError
super(*args)
end
def error_name
get_error(error_code)
end
# returns an error string if it exists, otherwise just the error code
def get_error(error)
string = ''
@ -807,7 +811,7 @@ end
class ErrorCode < InvalidPacket
def to_s
'The server responded with error: ' +
self.get_error(self.error_code) +
self.error_name +
" (Command=#{self.command} WordCount=#{self.word_count})"
end
end

View File

@ -4,6 +4,8 @@
##
require 'msf/core'
require 'metasploit/framework/login_scanner/smb'
require 'metasploit/framework/credential_collection'
class Metasploit3 < Msf::Auxiliary
@ -15,8 +17,6 @@ class Metasploit3 < Msf::Auxiliary
include Msf::Auxiliary::Report
include Msf::Auxiliary::AuthBrute
attr_reader :accepts_bogus_domains
def proto
'smb'
end
@ -50,30 +50,15 @@ class Metasploit3 < Msf::Auxiliary
)
deregister_options('RHOST','USERNAME','PASSWORD')
@accepts_guest_logins = {}
@correct_credentials_status_codes = [
"STATUS_INVALID_LOGON_HOURS",
"STATUS_INVALID_WORKSTATION",
"STATUS_ACCOUNT_RESTRICTION",
"STATUS_ACCOUNT_EXPIRED",
"STATUS_ACCOUNT_DISABLED",
"STATUS_ACCOUNT_RESTRICTION",
"STATUS_PASSWORD_EXPIRED",
"STATUS_PASSWORD_MUST_CHANGE",
"STATUS_LOGON_TYPE_NOT_GRANTED"
]
# These are normally advanced options, but for this module they have a
# more active role, so make them regular options.
register_options(
[
OptString.new('SMBPass', [ false, "SMB Password" ]),
OptString.new('SMBUser', [ false, "SMB Username" ]),
OptString.new('SMBDomain', [ false, "SMB Domain", '']),
OptBool.new('CHECK_ADMIN', [ false, "Check for Admin rights", false]),
OptBool.new('PRESERVE_DOMAINS', [ false, "Respect a username that contains a domain name.", true]),
OptBool.new('RECORD_GUEST', [ false, "Record guest-privileged random logins to the database", false])
OptString.new('SMBDomain', [ false, "SMB Domain", '' ]),
OptBool.new('PRESERVE_DOMAINS', [ false, "Respect a username that contains a domain name.", true ]),
OptBool.new('RECORD_GUEST', [ false, "Record guest-privileged random logins to the database", false ])
], self.class)
end
@ -83,250 +68,124 @@ class Metasploit3 < Msf::Auxiliary
domain = datastore['SMBDomain'] || ""
if accepts_bogus_logins?(domain)
print_error("#{smbhost} - This system accepts authentication with any credentials, brute force is ineffective.")
return
end
@scanner = Metasploit::Framework::LoginScanner::SMB.new(
host: ip,
port: rport,
stop_on_success: datastore['STOP_ON_SUCCESS'],
connection_timeout: 5,
)
unless datastore['RECORD_GUEST']
if accepts_guest_logins?(domain)
print_status("#{ip} - This system allows guest sessions with any credentials, these instances will not be recorded.")
bogus_result = @scanner.attempt_bogus_login(domain)
if bogus_result.success?
if bogus_result.access_level == Metasploit::Framework::LoginScanner::SMB::AccessLevels::GUEST
print_status("#{ip} - This system allows guest sessions with any credentials")
else
print_error("#{ip} - This system accepts authentication with any credentials, brute force is ineffective.")
return
end
end
begin
each_user_pass do |user, pass|
result = try_user_pass(domain, user, pass)
cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['SMBPass'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
username: datastore['SMBUser'],
user_as_pass: datastore['USER_AS_PASS'],
realm: domain,
)
@scanner.cred_details = cred_collection
@scanner.scan! do |result|
case result.status
when :correct
print_brute :level => :status, :ip => ip, :msg => "Correct credentials, but unable to login: '#{result.credential}', #{result.proof.error_name}"
report_creds(ip, rport, result)
:next_user
when :success
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}' #{result.access_level}"
report_creds(ip, rport, result)
:next_user
when :connection_error
print_brute :level => :verror, :ip => ip, :msg => "Could not connect"
:abort
when :failed
print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}', #{result.proof.error_name}"
invalidate_login(
address: ip,
port: rport,
protocol: 'tcp',
public: result.credential.public,
private: result.credential.private,
realm_key: Metasploit::Credential::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: result.credential.realm,
status: :failed
)
end
rescue ::Rex::ConnectionError
nil
end
end
def check_login_status(domain, user, pass)
connect()
status_code = ""
begin
simple.login(
datastore['SMBName'],
user,
pass,
domain,
datastore['SMB::VerifySignature'],
datastore['NTLM::UseNTLMv2'],
datastore['NTLM::UseNTLM2_session'],
datastore['NTLM::SendLM'],
datastore['NTLM::UseLMKey'],
datastore['NTLM::SendNTLM'],
datastore['SMB::Native_OS'],
datastore['SMB::Native_LM'],
{:use_spn => datastore['NTLM::SendSPN'], :name => self.rhost}
)
# Windows SMB will return an error code during Session Setup, but nix Samba requires a Tree Connect:
simple.connect("\\\\#{datastore['RHOST']}\\IPC$")
status_code = 'STATUS_SUCCESS'
if datastore['CHECK_ADMIN']
status_code = :not_admin
# Drop the existing connection to IPC$ in order to connect to admin$
simple.disconnect("\\\\#{datastore['RHOST']}\\IPC$")
begin
simple.connect("\\\\#{datastore['RHOST']}\\admin$")
status_code = :admin_access
simple.disconnect("\\\\#{datastore['RHOST']}\\admin$")
rescue
status_code = :not_admin
ensure
begin
simple.connect("\\\\#{datastore['RHOST']}\\IPC$")
rescue ::Rex::Proto::SMB::Exceptions::NoReply
end
end
end
rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
status_code = e.get_error(e.error_code)
rescue ::Rex::Proto::SMB::Exceptions::LoginError => e
status_code = e.error_reason
rescue ::Rex::Proto::SMB::Exceptions::InvalidWordCount => e
status_code = e.get_error(e.error_code)
rescue ::Rex::Proto::SMB::Exceptions::NoReply
ensure
disconnect()
end
return status_code
end
# If login is succesful and auth_user is unset
# the login was as a guest user.
def accepts_guest_logins?(domain)
guest = false
user = Rex::Text.rand_text_alpha(8)
pass = Rex::Text.rand_text_alpha(8)
guest_login = ((check_login_status(domain, user, pass) == 'STATUS_SUCCESS') && simple.client.auth_user.nil?)
if guest_login
@accepts_guest_logins['rhost'] ||=[] unless @accepts_guest_logins.include?(rhost)
report_note(
:host => rhost,
:proto => 'tcp',
:sname => 'smb',
:port => datastore['RPORT'],
:type => 'smb.account.info',
:data => 'accepts guest login from any account',
:update => :unique_data
)
end
return guest_login
end
# If login is successul and auth_user is set
# then bogus creds are accepted.
def accepts_bogus_logins?(domain)
user = Rex::Text.rand_text_alpha(8)
pass = Rex::Text.rand_text_alpha(8)
bogus_login = ((check_login_status(domain, user, pass) == 'STATUS_SUCCESS') && !simple.client.auth_user.nil?)
return bogus_login
end
# This logic is not universal ie a local account will not care about workgroup
# but remote domain authentication will so check each instance
def accepts_bogus_domains?(user, pass, rhost)
domain = Rex::Text.rand_text_alpha(8)
status = check_login_status(domain, user, pass)
bogus_domain = valid_credentials?(status)
if bogus_domain
vprint_status "Domain is ignored"
end
return valid_credentials?(status)
end
def valid_credentials?(status)
case status
when 'STATUS_SUCCESS', :admin_access, :not_admin
return true
when *@correct_credentials_status_codes
return true
else
return false
end
end
def try_user_pass(domain, user, pass)
# Note that unless PRESERVE_DOMAINS is true, we're more
# than happy to pass illegal usernames that contain
# slashes.
if datastore["PRESERVE_DOMAINS"]
d,u = domain_username_split(user)
user = u
domain = d if d
end
user = user.to_s.gsub(/<BLANK>/i,"")
status = check_login_status(domain, user, pass)
# Match original output message
if domain.empty? || domain == "."
domain_part = ""
else
domain_part = " \\\\#{domain}"
end
output_message = "#{rhost}:#{rport}#{domain_part} - ".gsub('%', '%%')
output_message << "%s"
output_message << " (#{smb_peer_os}) #{user} : #{pass} [#{status}]".gsub('%', '%%')
case status
when 'STATUS_SUCCESS', :admin_access, :not_admin
# Auth user indicates if the login was as a guest or not
if(simple.client.auth_user)
print_good(output_message % "SUCCESSFUL LOGIN")
validuser_case_sensitive?(domain, user, pass)
report_creds(domain,user,pass,true)
else
if datastore['RECORD_GUEST']
print_status(output_message % "GUEST LOGIN")
report_creds(domain,user,pass,true)
elsif datastore['VERBOSE']
print_status(output_message % "GUEST LOGIN")
end
end
return :next_user
when *@correct_credentials_status_codes
print_status(output_message % "FAILED LOGIN, VALID CREDENTIALS" )
report_creds(domain,user,pass,false)
validuser_case_sensitive?(domain, user, pass)
return :skip_user
when 'STATUS_LOGON_FAILURE', 'STATUS_ACCESS_DENIED'
vprint_error(output_message % "FAILED LOGIN")
else
vprint_error(output_message % "FAILED LOGIN")
end
end
def validuser_case_sensitive?(domain, user, pass)
if user == user.downcase
user = user.upcase
else
user = user.downcase
end
status = check_login_status(domain, user, pass)
case_insensitive = valid_credentials?(status)
if case_insensitive
vprint_status("Username is case insensitive")
end
return case_insensitive
end
def note_creds(domain,user,pass,reason)
report_note(
:host => rhost,
:proto => 'tcp',
:sname => 'smb',
:port => datastore['RPORT'],
:type => 'smb.account.info',
:data => {:user => user, :pass => pass, :status => reason},
:update => :unique_data
def accepts_bogus_domains?(user, pass)
bogus_domain = @scanner.attempt_login(
Metasploit::Framework::Credential.new(
public: user,
private: pass,
realm: Rex::Text.rand_text_alpha(8)
)
)
return bogus_domain.success?
end
def report_creds(domain,user,pass,active)
login_name = ""
if accepts_bogus_domains?(user,pass,rhost) || domain.blank?
login_name = user
else
login_name = "#{domain}\\#{user}"
def report_creds(ip, port, result)
if !datastore['RECORD_GUEST']
if result.access_level == Metasploit::Framework::LoginScanner::SMB::AccessLevels::GUEST
return
end
end
report_hash = {
:host => rhost,
:port => datastore['RPORT'],
:sname => 'smb',
:user => login_name,
:pass => pass,
:source_type => "user_supplied",
:active => active
service_data = {
address: ip,
port: port,
service_name: 'smb',
protocol: 'tcp',
workspace_id: myworkspace_id
}
if pass =~ /[0-9a-fA-F]{32}:[0-9a-fA-F]{32}/
report_hash.merge!({:type => 'smb_hash'})
else
report_hash.merge!({:type => 'password'})
credential_data = {
module_fullname: self.fullname,
origin_type: :service,
private_data: result.credential.private,
private_type: :password,
username: result.credential.public,
}.merge(service_data)
if domain.present?
if accepts_bogus_domains?(result.credential.public, result.credential.private)
print_brute(:level => :vstatus, :ip => ip, :msg => "Domain is ignored for user #{result.credential.public}")
else
credential_data.merge!(
realm_key: Metasploit::Credential::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: result.credential.realm
)
end
end
report_auth_info(report_hash)
credential_core = create_credential(credential_data)
login_data = {
access_level: result.access_level,
core: credential_core,
last_attempted_at: DateTime.now,
status: Metasploit::Credential::Login::Status::SUCCESSFUL
}.merge(service_data)
create_credential_login(login_data)
end
end

View File

@ -72,6 +72,9 @@ describe Metasploit::Framework::LoginScanner::SMB do
end
context '#attempt_login' do
before(:each) do
login_scanner.stub_chain(:simple, :client, :auth_user, :nil?).and_return false
end
context 'when there is a connection error' do
it 'returns a result with the connection_error status' do
login_scanner.stub_chain(:simple, :login).and_raise ::Rex::ConnectionError
@ -91,10 +94,13 @@ describe Metasploit::Framework::LoginScanner::SMB do
0xC0000224, # => "STATUS_PASSWORD_MUST_CHANGE",
].each do |code|
it "returns a status of :correct" do
exception = Rex::Proto::SMB::Exceptions::ErrorCode.new
exception = Rex::Proto::SMB::Exceptions::LoginError.new
exception.error_code = code
login_scanner.stub_chain(:simple, :login).and_raise exception
login_scanner.stub_chain(:simple, :connect)
login_scanner.stub_chain(:simple, :disconnect)
login_scanner.stub_chain(:simple, :client, :auth_user, :nil?).and_return false
expect(login_scanner.attempt_login(pub_blank).status).to eq :correct
end
@ -105,16 +111,44 @@ describe Metasploit::Framework::LoginScanner::SMB do
context 'when the login fails' do
it 'returns a result object with a status of :failed' do
login_scanner.stub_chain(:simple, :login).and_return false
login_scanner.stub_chain(:simple, :connect)
login_scanner.stub_chain(:simple, :connect).and_raise Rex::Proto::SMB::Exceptions::Error
expect(login_scanner.attempt_login(pub_blank).status).to eq :failed
end
end
context 'when the login succeeds' do
it 'returns a result object with a status of :success' do
login_scanner.stub_chain(:simple, :login).and_return true
login_scanner.stub_chain(:simple, :connect)
expect(login_scanner.attempt_login(pub_blank).status).to eq :success
context 'and the user is local admin' do
before(:each) do
login_scanner.simple = double
login_scanner.simple.stub(:connect).with(/.*admin\$/i)
login_scanner.simple.stub(:connect).with(/.*ipc\$/i)
login_scanner.simple.stub(:disconnect)
end
it 'returns a result object with a status of :success' do
login_scanner.stub_chain(:simple, :login).and_return true
result = login_scanner.attempt_login(pub_blank)
expect(result.status).to eq :success
expect(result.access_level).to eq described_class::AccessLevels::ADMINISTRATOR
end
end
context 'and the user is NOT local admin' do
before(:each) do
login_scanner.simple = double
login_scanner.simple.stub(:connect).with(/.*admin\$/i).and_raise(
# STATUS_ACCESS_DENIED
Rex::Proto::SMB::Exceptions::ErrorCode.new.tap{|e|e.error_code = 0xC0000022}
)
login_scanner.simple.stub(:connect).with(/.*ipc\$/i)
end
it 'returns a result object with a status of :success' do
login_scanner.stub_chain(:simple, :login).and_return true
result = login_scanner.attempt_login(pub_blank)
expect(result.status).to eq :success
expect(result.access_level).to_not eq described_class::AccessLevels::ADMINISTRATOR
end
end
end
end