diff --git a/modules/auxiliary/scanner/smb/smb_login.rb b/modules/auxiliary/scanner/smb/smb_login.rb index b9e8e4d817..4f5a0a9e49 100644 --- a/modules/auxiliary/scanner/smb/smb_login.rb +++ b/modules/auxiliary/scanner/smb/smb_login.rb @@ -5,7 +5,6 @@ # http://metasploit.com/ ## - require 'msf/core' class Metasploit3 < Msf::Auxiliary @@ -32,17 +31,29 @@ class Metasploit3 < Msf::Auxiliary and connected to a database this module will record successful logins and hosts so you can track your access. }, - 'Author' => 'tebo ', + 'Author' => [ + 'tebo ', # Original + 'Ben Campbell ' # Refactoring + ], 'References' => [ - [ 'CVE', '1999-0506'] # Weak password + [ 'CVE', '1999-0506'], # Weak password + ], 'License' => MSF_LICENSE ) deregister_options('RHOST','USERNAME','PASSWORD') - @accepts_bogus_domains = [] @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. @@ -50,39 +61,32 @@ class Metasploit3 < Msf::Auxiliary [ OptString.new('SMBPass', [ false, "SMB Password" ]), OptString.new('SMBUser', [ false, "SMB Username" ]), - OptString.new('SMBDomain', [ false, "SMB Domain", 'WORKGROUP']), + 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 def run_host(ip) print_brute(:level => :vstatus, :ip => ip, :msg => "Starting SMB login bruteforce") - if accepts_bogus_logins? + domain = datastore['SMBDomain'] || "" + + if accepts_bogus_logins?(domain) print_error("#{smbhost} - This system accepts authentication with any credentials, brute force is ineffective.") return end - begin - if accepts_guest_logins? - print_error("#{ip} - This system allows guest sessions with any credentials, these instances will not be reported.") + 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.") end - end unless datastore['RECORD_GUEST'] + end begin each_user_pass do |user, pass| - result = try_user_pass(user, pass) - if result == :next_user - unless user == user.downcase - result = try_user_pass(user.downcase, pass) - if result == :next_user - print_status("Username is case insensitive") - user = user.downcase - end - end - report_creds(user,pass) if @accepts_guest_logins.select{ |g_host, g_creds| g_host == ip and g_creds == [user,pass] }.empty? - end + result = try_user_pass(domain, user, pass) end rescue ::Rex::ConnectionError nil @@ -90,23 +94,47 @@ class Metasploit3 < Msf::Auxiliary end - def accepts_guest_logins? - guest = false - orig_user,orig_pass = datastore['SMBUser'],datastore['SMBPass'] - datastore["SMBUser"] = Rex::Text.rand_text_alpha(8) - datastore["SMBPass"] = Rex::Text.rand_text_alpha(8) - - # Connection problems are dealt with at a higher level + def check_login_status(domain, user, pass) connect() - + status_code = "" begin - smb_login() - rescue ::Rex::Proto::SMB::Exceptions::LoginError - rescue ::Rex::Proto::SMB::Exceptions::ErrorCode + 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' + 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 + ensure + disconnect() end - begin - guest = true + 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, @@ -117,180 +145,131 @@ class Metasploit3 < Msf::Auxiliary :data => 'accepts guest login from any account', :update => :unique_data ) - end unless(simple.client.auth_user) - - disconnect() - datastore['SMBUser'],datastore['SMBPass'] = orig_user,orig_pass - return guest + end + return guest_login end - - def accepts_bogus_logins? - orig_user,orig_pass = datastore['SMBUser'],datastore['SMBPass'] - datastore["SMBUser"] = Rex::Text.rand_text_alpha(8) - datastore["SMBPass"] = Rex::Text.rand_text_alpha(8) - - # Connection problems are dealt with at a higher level - connect() - - begin - smb_login() - rescue ::Rex::Proto::SMB::Exceptions::LoginError - rescue ::Rex::Proto::SMB::Exceptions::ErrorCode - end - - disconnect() - datastore['SMBUser'],datastore['SMBPass'] = orig_user,orig_pass - - return simple.client.auth_user ? true : false + # 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 - def accepts_bogus_domains?(addr) - if @accepts_bogus_domains.include? addr - return true - end - orig_domain = datastore['SMBDomain'] - datastore['SMBDomain'] = Rex::Text.rand_text_alpha(8) + # 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) - connect() - begin - smb_login() - rescue ::Rex::Proto::SMB::Exceptions::LoginError - rescue ::Rex::Proto::SMB::Exceptions::ErrorCode + bogus_domain = valid_credentials?(status) + if bogus_domain + vprint_status "Domain is ignored" end - disconnect() - datastore['SMBDomain'] = orig_domain - if simple.client.auth_user - @accepts_bogus_domains << addr - return true - else - return false - end + return valid_credentials?(status) end - def try_user_pass(user, pass) - # The SMB mixins require the datastores "SMBUser" and - # "SMBPass" to be populated. - datastore["SMBPass"] = pass - orig_domain = datastore["SMBDomain"] + def valid_credentials?(status) + return (status == "STATUS_SUCCESS" || @correct_credentials_status_codes.include?(status)) + 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) - datastore["SMBUser"] = u.to_s.gsub(//i,"") - datastore["SMBDomain"] = d if d - else - datastore["SMBUser"] = user.to_s.gsub(//i,"") + user = u + domain = d if d end - # Connection problems are dealt with at a higher level - connect() + user = user.to_s.gsub(//i,"") + status = check_login_status(domain, user, pass) - begin - smb_login() - rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e - if e.get_error(e.error_code) == "STATUS_ACCESS_DENIED" - print_error("#{smbhost} - FAILED LOGIN (#{smb_peer_os}) #{splitname(user)} : #{pass} (#{e.get_error(e.error_code)})") - disconnect() - datastore["SMBDomain"] = orig_domain - return :skip_user + # Match original output message + if domain.empty? || domain == "." + domain_part = "" + else + domain_part = " \\\\#{domain}" + end + output_message = "#{rhost}:#{rport}#{domain_part} - %s (#{smb_peer_os}) #{user} : #{pass} [#{status}]" + + case status + when 'STATUS_SUCCESS' + # 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 - raise e + 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 - - rescue ::Rex::Proto::SMB::Exceptions::LoginError => e - - case e.error_reason - when 'STATUS_LOGON_FAILURE', 'STATUS_ACCESS_DENIED' - # Nothing interesting - vprint_error("#{smbhost} - FAILED LOGIN (#{smb_peer_os}) #{splitname(user)} : #{pass} (#{e.error_reason})") - disconnect() - datastore["SMBDomain"] = orig_domain - return - - when 'STATUS_ACCOUNT_DISABLED' - report_note( - :host => rhost, - :proto => 'tcp', - :sname => 'smb', - :port => datastore['RPORT'], - :type => 'smb.account.info', - :data => {:user => user, :status => "disabled"}, - :update => :unique_data - ) - - when 'STATUS_PASSWORD_EXPIRED' - report_note( - :host => rhost, - :proto => 'tcp', - :sname => 'smb', - :port => datastore['RPORT'], - :type => 'smb.account.info', - :data => {:user => user, :status => "expired password"}, - :update => :unique_data - ) - - when 'STATUS_ACCOUNT_LOCKED_OUT' - report_note( - :host => rhost, - :proto => 'tcp', - :sname => 'smb', - :port => datastore['RPORT'], - :type => 'smb.account.info', - :data => {:user => user, :status => "locked out"}, - :update => :unique_data - ) - end - print_error("#{smbhost} - FAILED LOGIN (#{smb_peer_os}) #{splitname(user)} : #{pass} (#{e.error_reason})") - - disconnect() - datastore["SMBDomain"] = orig_domain - return :skip_user # These reasons are sufficient to stop trying. - end - - if(simple.client.auth_user) - print_status("Auth-User: #{simple.client.auth_user.inspect}") - print_good("#{smbhost} - SUCCESSFUL LOGIN (#{smb_peer_os}) '#{splitname(user)}' : '#{pass}'") + 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) + when 'STATUS_LOGON_FAILURE', 'STATUS_ACCESS_DENIED' + vprint_error(output_message % "FAILED LOGIN") else - print_status("#{rhost} - GUEST LOGIN (#{smb_peer_os}) #{splitname(user)} : #{pass}") - @accepts_guest_logins[rhost] = [user, pass] unless datastore['RECORD_GUEST'] + vprint_error(output_message % "FAILED LOGIN") end - - disconnect() - # If we get here then we've found the password for this user, move on - # to the next one. - datastore["SMBDomain"] = orig_domain - return :next_user end - def report_creds(user,pass) + 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 + ) + end + + def report_creds(domain,user,pass,active) + login_name = "" + + if accepts_bogus_domains?(user,pass,rhost) + login_name = user + else + login_name = "#{domain}\\#{user}" + end report_hash = { :host => rhost, :port => datastore['RPORT'], :sname => 'smb', + :user => login_name, :pass => pass, :source_type => "user_supplied", - :active => true + :active => active } - if accepts_bogus_domains? rhost - if datastore["PRESERVE_DOMAINS"] - d,u = domain_username_split(user) - report_hash[:user] = u - else - report_hash[:user] = "#{datastore["SMBUser"]}" - end - else - if datastore["PRESERVE_DOMAINS"] - d,u = domain_username_split(user) - report_hash[:user] = "#{datastore["SMBDomain"]}/#{u}" - else - report_hash[:user] = "#{datastore["SMBDomain"]}/#{datastore["SMBUser"]}" - end - end if pass =~ /[0-9a-fA-F]{32}:[0-9a-fA-F]{32}/ report_hash.merge!({:type => 'smb_hash'}) @@ -299,5 +278,4 @@ class Metasploit3 < Msf::Auxiliary end report_auth_info(report_hash) end - end