diff --git a/lib/metasploit/framework/login_scanner/smb2.rb b/lib/metasploit/framework/login_scanner/smb2.rb new file mode 100644 index 0000000000..b21b1443ed --- /dev/null +++ b/lib/metasploit/framework/login_scanner/smb2.rb @@ -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 + diff --git a/modules/auxiliary/scanner/smb/smb2_login.rb b/modules/auxiliary/scanner/smb/smb2_login.rb new file mode 100644 index 0000000000..679fbe4609 --- /dev/null +++ b/modules/auxiliary/scanner/smb/smb2_login.rb @@ -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 ', # Original + 'Ben Campbell', # Refactoring + 'Brandon McCann "zeknox" ', # admin check + 'Tom Sellers ' # 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