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:
parent
2d9c2321d1
commit
418e371e35
142
lib/metasploit/framework/login_scanner/smb2.rb
Normal file
142
lib/metasploit/framework/login_scanner/smb2.rb
Normal 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
|
||||
|
217
modules/auxiliary/scanner/smb/smb2_login.rb
Normal file
217
modules/auxiliary/scanner/smb/smb2_login.rb
Normal 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
|
Loading…
Reference in New Issue
Block a user