Path: blob/master/lib/metasploit/framework/login_scanner/base.rb
19813 views
require 'metasploit/framework/login_scanner'12module Metasploit3module Framework4module LoginScanner56# This module provides the base behaviour for all of7# the LoginScanner classes. All of the LoginScanners8# should include this module to establish base behaviour9module Base10extend ActiveSupport::Concern11include ActiveModel::Validations1213included do14# @!attribute framework15# @return [Object] The framework instance object16attr_accessor :framework17# @!attribute framework_module18# @return [Object] The framework module caller, if available19attr_accessor :framework_module20# @!attribute connection_timeout21# @return [Integer] The timeout in seconds for a single SSH connection22attr_accessor :connection_timeout23# @!attribute cred_details24# @return [CredentialCollection] Collection of Credential objects25attr_accessor :cred_details26# @!attribute host27# @return [String] The IP address or hostname to connect to28attr_accessor :host29# @!attribute port30# @return [Integer] The port to connect to31attr_accessor :port32# @!attribute host33# @return [String] The local host for outgoing connections34attr_accessor :local_host35# @!attribute port36# @return [Integer] The local port for outgoing connections37attr_accessor :local_port38# @!attribute proxies39# @return [String] The proxy directive to use for the socket40attr_accessor :proxies41# @!attribute stop_on_success42# @return [Boolean] Whether the scanner should stop when it has found one working Credential43attr_accessor :stop_on_success44# @!attribute bruteforce_speed45# @return [Integer] The desired speed, with 5 being 'fast' and 0 being 'slow.'46attr_accessor :bruteforce_speed47# @!attribute sslkeylogfile48# @return [String] The SSL key log file path49attr_accessor :sslkeylogfile5051validates :connection_timeout,52presence: true,53numericality: {54only_integer: true,55greater_than_or_equal_to: 156}5758validates :cred_details, presence: true5960validates :host, presence: true6162validates :port,63presence: true,64numericality: {65only_integer: true,66greater_than_or_equal_to: 1,67less_than_or_equal_to: 6553568}6970validates :stop_on_success,71inclusion: { in: [true, false] }7273validates :bruteforce_speed,74numericality: {75allow_nil: true,76only_integer: true,77greater_than_or_equal_to: 0,78less_than_or_equal_to: 579}8081validate :host_address_must_be_valid8283validate :validate_cred_details8485# @param attributes [Hash{Symbol => String,nil}]86def initialize(attributes={})87attributes.each do |attribute, value|88public_send("#{attribute}=", value)89end90set_sane_defaults91end9293# Attempt a single login against the service with the given94# {Credential credential}.95#96# @param credential [Credential] The credential object to attempt to97# login with98# @return [Result] A Result object indicating success or failure99# @abstract Protocol-specific scanners must implement this for their100# respective protocols101def attempt_login(credential)102raise NotImplementedError103end104105# @note Override this to detect that the service is up, is the right106# version, etc.107# @return [false] Indicates there were no errors108# @return [String] a human-readable error message describing why109# this scanner can't run110def check_setup111false112end113114# @note Override this to set a timeout that makes more sense for115# your particular protocol. Telnet already usually takes a really116# long time, while MSSQL is often lickety-split quick. If117# overridden, the override should probably do something sensible118# with {#bruteforce_speed}119#120# @return [Integer] a number of seconds to sleep between attempts121def sleep_time122case bruteforce_speed123when 0; 60 * 5124when 1; 15125when 2; 1126when 3; 0.5127when 4; 0.1128else; 0129end130end131132# A threadsafe sleep method133#134# @param time [Integer] number of seconds (can be a Float), defaults135# to {#sleep_time}136#137# @return [void]138def sleep_between_attempts(time=self.sleep_time)139::IO.select(nil,nil,nil,time) unless sleep_time.zero?140end141142def each_credential143cred_details.each do |raw_cred|144145# This could be a Credential object, or a Credential Core, or an Attempt object146# so make sure that whatever it is, we end up with a Credential.147credential = raw_cred.to_credential148149if credential.realm.present? && self.class::REALM_KEY.present?150# The class's realm_key will always be the right thing for the151# service it knows how to login to. Override the credential's152# realm_key if one exists for the class. This can happen for153# example when we have creds for DB2 and want to try them154# against Postgres.155credential.realm_key = self.class::REALM_KEY156yield credential157elsif credential.realm.blank? && self.class::REALM_KEY.present? && self.class::DEFAULT_REALM.present?158# XXX: This is messing up the display for mssql when not using159# Windows authentication, e.g.:160# [+] 10.0.0.53:1433 - LOGIN SUCCESSFUL: WORKSTATION\sa:msfadmin161# Realm gets ignored in that case, so it still functions, it162# just gives the user bogus info163credential.realm_key = self.class::REALM_KEY164credential.realm = self.class::DEFAULT_REALM165yield credential166elsif credential.realm.present? && self.class::REALM_KEY.blank?167second_cred = credential.dup168# This service has no realm key, so the realm will be169# meaningless. Strip it off.170credential.realm = nil171credential.realm_key = nil172yield credential173# Some services can take a domain in the username like this even though174# they do not explicitly take a domain as part of the protocol.175# e.g., telnet176second_cred.public = "#{second_cred.realm}\\#{second_cred.public}"177second_cred.realm = nil178second_cred.realm_key = nil179yield second_cred180else181yield credential182end183end184end185186# Attempt to login with every {Credential credential} in187# {#cred_details}, by calling {#attempt_login} once for each.188#189# If a successful login is found for a user, no more attempts190# will be made for that user.191#192# @yieldparam result [Result] The {Result} object for each attempt193# @yieldreturn [void]194# @return [void]195def scan!196valid!197198# Keep track of connection errors.199# If we encounter too many, we will stop.200consecutive_error_count = 0201total_error_count = 0202203successful_users = Set.new204ignored_users = Set.new205first_attempt = true206207each_credential do |credential|208# Skip users for whom we've have already found a password209if successful_users.include?(credential.public)210# For Pro bruteforce Reuse and Guess we need to note that we211# skipped an attempt.212if credential.parent.respond_to?(:skipped)213credential.parent.skipped = true214credential.parent.save!215end216next217end218219# Users that went into the lock-out list220if ignored_users.include?(credential.public)221if credential.parent.respond_to?(:skipped)222credential.parent.skipped = true223end224next225end226227if first_attempt228first_attempt = false229else230sleep_between_attempts231end232233result = attempt_login(credential)234result.freeze235236yield result if block_given?237238if result.success?239consecutive_error_count = 0240successful_users << credential.public241break if stop_on_success242elsif result.status == Metasploit::Model::Login::Status::LOCKED_OUT243ignored_users << credential.public244elsif result.status == Metasploit::Model::Login::Status::INVALID_PUBLIC_PART245ignored_users << credential.public246elsif result.status == Metasploit::Model::Login::Status::DISABLED247ignored_users << credential.public248else249if result.status == Metasploit::Model::Login::Status::UNABLE_TO_CONNECT250consecutive_error_count += 1251total_error_count += 1252break if consecutive_error_count >= 3253break if total_error_count >= 10254end255end256rescue => e257if framework_module258prefix = framework_module.respond_to?(:peer) ? "#{framework_module.peer} - LOGIN FAILED:" : "LOGIN FAILED:"259framework_module.print_warning("#{prefix} #{credential.to_h} - Unhandled error - scan may not produce correct results: #{e.message} - #{e.backtrace}")260end261elog("Scan Error: #{e.message}", error: e)262consecutive_error_count += 1263total_error_count += 1264break if consecutive_error_count >= 3265break if total_error_count >= 10266end267nil268end269270# Raise an exception if this scanner's attributes are not valid.271#272# @raise [Invalid] if the attributes are not valid on this scanner273# @return [void]274def valid!275unless valid?276raise Metasploit::Framework::LoginScanner::Invalid.new(self)277end278nil279end280281282private283284# This method validates that the host address is both285# of a valid type and is resolveable.286# @return [void]287def host_address_must_be_valid288if host.kind_of? String289begin290resolved_host = ::Rex::Socket.getaddress(host, true)291if host =~ /^\d{1,3}(\.\d{1,3}){1,3}$/292unless host =~ Rex::Socket::MATCH_IPV4293errors.add(:host, "could not be resolved")294end295end296self.host = resolved_host297rescue298errors.add(:host, "could not be resolved")299end300else301errors.add(:host, "must be a string")302end303end304305# This is a placeholder method. Each LoginScanner class306# will override this with any sane defaults specific to307# its own behaviour.308# @abstract309# @return [void]310def set_sane_defaults311self.connection_timeout = 30 if self.connection_timeout.nil?312end313314# This method validates that the credentials supplied315# are all valid.316# @return [void]317def validate_cred_details318unless cred_details.respond_to? :each319errors.add(:cred_details, "must respond to :each")320end321322if cred_details.empty?323errors.add(:cred_details, "can't be blank")324end325end326327end328329330end331332end333end334end335336337