编写 Metasploit 框架登录扫描模块 - L-codes/metasploit-framework GitHub Wiki

你想在 Metasploit 中编写一个登陆扫描器模块? 在开始之前, 你需要了解一些事项. 本文将尝试说明创建有效的 bruteforce /登录扫描器模块所涉及的所有对象.

Credential 对象

Metasploit::Framework::Credential (lib/metasploit/framework/credential.rb)

这些参数代表了我们现在如何思考凭据的最基本概念.

  • public: 凭证的公共部分是指可以公开知道的部分. 在几乎指的都是用户名.
  • private: 凭证的私有部分, 这是应该保密的部分. 当前代表: 密码, SSH 密钥, NTLM 哈希等.
  • private_type: 这定义了上面定义凭证私有部分类型.
  • realm: 这代表凭证有效的范围, 如域名等.
  • realm_key: 这定义了上面定义的 Realm 类型. 示例包括: Active Directory 域, Postgres 数据库等 (参考: Metasploit::Model::Realm::Key::SHORT_NAMES).
  • paired: 此属性是一个布尔值, 用于设置凭据是否必须同时具有公共和私有才能有效.

所有 LoginScanner 都使用 Credential 对象作为其尝试的基础.

Result 对象

Metasploit::Framework::LoginScanner::Result (lib/metasploit/framework/login_scanner/result.rb)

下面的属性是由 LoginScanner#scan! 方法生成的 Result 对象, 使用 each 遍历每个结果. 属性包含:

  • access_level: 访问的权限级别.
  • credential : 凭证对象的结果.
  • proof: 证明结果有效的字符串内容
  • status: The status of the login attempt. These values come from Metasploit::model::Login::Status , examples include "Incorrect", "Unable to Connect", "Untried" etc
  • status: 尝试登录的状态. 这些状态值来自 Metasploit::Model::Login::Status, 包含 "Incorrect", "Unable to Connect", "Untried" 等.

CredentialCollection

Metasploit::Framework::CredentialCollection (lib/metasploit/framework/credential_collection.rb)

这个类是从模块的 datastore 参数中读取凭证参数, 然后 CredentialCollection#each 方法生成 Credential 对象. 它可以用字典文件选项、直接的用户名和密码选项、还可以使用用户名作为密码和空密码选项. 将作为 cred_details 参数传入到 LoginScanner 对象, 响应 #each 方法来遍历所选的凭证.

如 (modules/auxiliary/scanner/ftp/ftp_login.rb):

cred_collection = Metasploit::Framework::CredentialCollection.new(
        blank_passwords: datastore['BLANK_PASSWORDS'],
        pass_file: datastore['PASS_FILE'],
        password: datastore['PASSWORD'],
        user_file: datastore['USER_FILE'],
        userpass_file: datastore['USERPASS_FILE'],
        username: datastore['USERNAME'],
        user_as_pass: datastore['USER_AS_PASS'],
        prepended_creds: anonymous_creds
    )

LoginScanner 基类

Metasploit::Framework::LoginScanner::Base (lib/metasploit/framework/login_scanner/base.rb)

这是一个包含所有 LoginScanner 基本行为的 Ruby 模块, 所有 LoginScanner 类应包含该类.

spec 中测试 LoginScanner 都会将此作为类行为范例, spec 下会以下面的语法包含在测试中:

it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false

其中 has_realm_keyhas_default_realm 应根据你的 LoginScanner 是否具有这些设置进行设置. (稍后会详细介绍)

LoginScanner 始终会尝试收集 Credential 来尝试测试一个主机和端口. 所以每个 LoginScanner 仅会尝试登录一个指定服务目标.

属性

  • connection_timeout: 等待连接的 timeout
  • cred_details: 指定一个通过 #each 方法生成凭证的对象 (如 CredentialCollection 或 Array)
  • host: 指定目标主机的地址 (如 IP、域名).
  • port: 指定目标服务的端口号.
  • proxies: 通过 proxy 来进行连接 (部分扫描可能不支持).
  • stop_on_success: 是否找到成功登录的凭证后就停止尝试.

方法

  • each_credential : 你不必为此方法担心. 它遍历 cred_details 中的内容, 进行一些规范化, 并尝试确保正确设置每个凭据以供 LoginScanner 使用. 产生每个凭证到 block 中.
         def each_credential
           cred_details.each do |raw_cred|
             # This could be a Credential object, or a Credential Core, or an Attempt object
             # so make sure that whatever it is, we end up with a Credential.
             credential = raw_cred.to_credential

             if credential.realm.present? && self.class::REALM_KEY.present?
               credential.realm_key = self.class::REALM_KEY
               yield credential
             elsif credential.realm.blank? && self.class::REALM_KEY.present? && self.class::DEFAULT_REALM.present?
               credential.realm_key = self.class::REALM_KEY
               credential.realm     = self.class::DEFAULT_REALM
               yield credential
             elsif credential.realm.present? && self.class::REALM_KEY.blank?
               second_cred = credential.dup
               # Strip the realm off here, as we don't want it
               credential.realm = nil
               credential.realm_key = nil
               yield credential
               # Some services can take a domain in the username like this even though
               # they do not explicitly take a domain as part of the protocol.
               second_cred.public = "#{second_cred.realm}\\#{second_cred.public}"
               second_cred.realm = nil
               second_cred.realm_key = nil
               yield second_cred
             else
               yield credential
             end
           end
         end
  • set_sane_defaults: 该方法在 initialize 方法结束前调用, 作用是如 LoginScanner 初始化时未指定 connection_timeout 值则设置默认为 30 秒.
         # This is a placeholder method. Each LoginScanner class
         # will override this with any sane defaults specific to
         # its own behaviour.
         # @abstract
         # @return [void]
         def set_sane_defaults
           self.connection_timeout = 30 if self.connection_timeout.nil?
         end
  • attempt_login: 此方法只是在 Base 模块上声明在 Mixin 时需要定义改方法. 每个 LoginScanner 类都会覆盖该方法, 接收一个 Credential 对象, 尝试登录后, 返回一个 Result 对象.

Metasploit::Framework::LoginScanner::FTP (lib/metasploit/framework/login_scanner/ftp.rb) 中的 attempt_login 方法为例:

# (see Base#attempt_login)
       def attempt_login(credential)
         result_options = {
             credential: credential
         }

         begin
           success = connect_login(credential.public, credential.private)
         rescue ::EOFError,  Rex::AddressInUse, Rex::ConnectionError, Rex::ConnectionTimeout, ::Timeout::Error
           result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
           success = false
         end


         if success
           result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
         elsif !(result_options.has_key? :status)
           result_options[:status] = Metasploit::Model::Login::Status::INCORRECT
         end

         ::Metasploit::Framework::LoginScanner::Result.new(result_options)

       end
  • scan! : 此方法是你要关注的主要方法. 此方法可以完成几件事.
    • 会先调用 valid! 调用 ActiveModel::Validations#valid? 验证各个参数的有效性, 如有错误触发 Metasploit::Framework::LoginScanner::Invalid 异常.
    • 它会跟踪连接错误计数, 并且如果我们有太多连接错误或连续出现太多错误, 它将自动退出.
    • 它通过调用 each_credential 来遍历所有凭据到 block 中.
    • 在 block 中, 传递每个凭证给 #attempt_login.
    • 如果 #scan! 跟 block 则会传递 Result 到 block 中.
    • 如果设置了 stop_on_success, 状态成功也会提前退出.
          # Attempt to login with every {Credential credential} in
          # {#cred_details}, by calling {#attempt_login} once for each.
          #
          # If a successful login is found for a user, no more attempts
          # will be made for that user.
          #
          # @yieldparam result [Result] The {Result} object for each attempt
          # @yieldreturn [void]
          # @return [void]
          def scan!
            valid!

            # Keep track of connection errors.
            # If we encounter too many, we will stop.
            consecutive_error_count = 0
            total_error_count = 0

            successful_users = Set.new

            each_credential do |credential|
              next if successful_users.include?(credential.public)

              result = attempt_login(credential)
              result.freeze

              yield result if block_given?

              if result.success?
                consecutive_error_count = 0
                break if stop_on_success
                successful_users << credential.public
              else
                if result.status == Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
                  consecutive_error_count += 1
                  total_error_count += 1
                  break if consecutive_error_count >= 3
                  break if total_error_count >= 10
                end
              end
            end
            nil
          end

常量

尽管未在 Base 上定义, 但每个 LoginScanner 都有一系列可以在其上定义的常量, 以帮助关键行为.

  • DEFAULT_PORT: DEFAULT_PORT is a simple constant for use with set_sane_defaults. If the port isn't set by the user it will use DEFAULT_PORT. This is put in a constant so it can be quickly referenced from outside the scanner.
  • DEFAULT_PORT: DEFAULT_PORT 是一个简单的常量, 可与 set_sane_defaults 一起使用. 如用户未设置, 将会使用 DEFAULT_PORT. 这是一个常数, 因此可以从扫描器外部快速引用.

接下来的两个常量由 LoginScanner.classes_for_services 方法调用, 实际上 Metasploit::Framework::LoginScanner.classes_for_service(<Mdm::service>) 将返回一个 LoginScanner 类的数组, 这些类对于尝试使用该特定服务可能很有用.

  • LIKELY_PORTS : 记录可疑的端口号数组.

  • LIKELY_SERVICE_NAMES : 与上面类似, 单记录的是服务名.

  • PRIVATE_TYPES : 这个常量记录 Symbol 类型数组, 指定支持的私有凭证类型有哪些. 如: :password, :ntlm_hash, :ssh_key

下面的常量是必须处理如 AD 域或数据库名称之类的 realmLoginScanner.

  • REALM_KEY: 期望扫描器针对的领域类型.

  • DEFAULT_REALM: 指定扫描器的默认领域名称 (如 AD 域的 WORKSTATION).

  • CAN_GET_SESSION: 布尔值, 是否期望以某种方式从该扫描仪找到具有凭据的会话.

示例 1 ( Metasploit::Framework::LoginScanner::FTP)

       DEFAULT_PORT         = 21
       LIKELY_PORTS         = [ DEFAULT_PORT, 2121 ]
       LIKELY_SERVICE_NAMES = [ 'ftp' ]
       PRIVATE_TYPES        = [ :password ]
       REALM_KEY           = nil

示例 2 ( Metasploit::Framework::LoginScanner::SMB)

        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

合并成一个模块

因此, 下一步是在实际模块中使用全新的 LoginScanner

让我们看看 ftp_login 模块:

def run_host(ip)

每个 "爆破/登录" 模块都应该是一个 Msf::Auxiliary::Scanner, 并且应该使用 run_host 方法, 该方法将为每个 RHOST 运行一次.

凭证收集

    cred_collection = Metasploit::Framework::CredentialCollection.new(
        blank_passwords: datastore['BLANK_PASSWORDS'],
        pass_file: datastore['PASS_FILE'],
        password: datastore['PASSWORD'],
        user_file: datastore['USER_FILE'],
        userpass_file: datastore['USERPASS_FILE'],
        username: datastore['USERNAME'],
        user_as_pass: datastore['USER_AS_PASS'],
        prepended_creds: anonymous_creds
    )

在这里我们看到使用 datastore 的选项值创建了 CredentialCollection. 我们传入用于创建凭证的选项, 例如单词列表, 用户名和密码, 是否尝试将用户名用作密码以及是否尝试空白密码.

这里有一个 prepended_creds 参数, FTP 是使用该功能的模块之一, 这是 CredentialCollection 优先遍历的 Credential 对象数组, 这里主要是为了优先测试 anonymous 账号.

初始化扫描器

scanner = Metasploit::Framework::LoginScanner::FTP.new(
        host: ip,
        port: rport,
        proxies: datastore['PROXIES'],
        cred_details: cred_collection,
        stop_on_success: datastore['STOP_ON_SUCCESS'],
        connection_timeout: 30
    )

在这里, 创建了一个 LoginScanner 实例. 设置了 IP 和端口号, 并从 datastore 中获取了用户提供的代理信息和是否在成功登录后停止对该目标继续尝试. cred_collection 会为扫描器提供所有需要尝试的凭证.

这为我们提供了 scanner 对象, 所有对象均已配置并可以使用.

Scan Block

 scanner.scan! do |result|
      credential_data = result.to_h
      credential_data.merge!(
          module_fullname: self.fullname,
          workspace_id: myworkspace_id
      )
      if result.success?
        credential_core = create_credential(credential_data)
        credential_data[:core] = credential_core
        create_credential_login(credential_data)

        print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}"
      else
        invalidate_login(credential_data)
        print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
      end
    end

启动 scanner, scan! 方法接受一个 block, 并将每次尝试的结果以 Result 对象返回到 block 中. 我们检查结果的状态, 看是否成功.

Result 对象使用 .to_h 方法转为 Hash 对象, 是为兼容 create_credential 方法接收参数对象. 我们将模块信息和 workspace_id 合并到 Hash 中.

如果成功, 我们将构建一些信息哈希并调用 create_credential. 这是 Metasploit::Credential::Creation 的 mixin 中的 lib/metasploit/credential/creation.rb 下的 这是 metasploit-credential gem 中 lib/metasploit/credential/creation.rb 文件下定义 Metasploit::Credential::Creation 模块的 create_credential 方法, 而该模块在 Report 中 Mixin 调用, 可直接调用该方法.

create_credential 会创建一个 Metasploit::Credential::Core 对象. 将其作为 core 值, 并可以附带如 access_levellast_attempted_atstatus 等信息, 再提交到 Metasploit::Credential::Login.

最后, 如有成功, 我们将结果输出到控制台.

如果发生错误, 调用 invalidate_login 方法. 这个方法也来自 Creation Mixin. 此方法会查看 credential:service 是否已经已存在 Login 对象. 如果存在, 则将它的状态更新为我们新获取到的状态. 这主要是考虑到由 Post 模块具有未尝试状态之类的东西创建的 Login 对象.

ftp_login 最终成品

综合所有这些, 我们得到一个新的 ftp_login 模块, 看起来像这样:

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'metasploit/framework/credential_collection'
require 'metasploit/framework/login_scanner/ftp'

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::Ftp
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::AuthBrute

  def proto
    'ftp'
  end

  def initialize
    super(
      'Name'        => 'FTP Authentication Scanner',
      'Description' => %q{
        This module will test FTP logins 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'      => 'todb',
      'References'     =>
        [
          [ 'CVE', '1999-0502'] # Weak password
        ],
      'License'     => MSF_LICENSE
    )

    register_options(
      [
        Opt::Proxies,
        Opt::RPORT(21),
        OptBool.new('RECORD_GUEST', [ false, "Record anonymous/guest logins to the database", false])
      ])

    register_advanced_options(
      [
        OptBool.new('SINGLE_SESSION', [ false, 'Disconnect after every login attempt', false])
      ]
    )

    deregister_options('FTPUSER','FTPPASS', 'PASSWORD_SPRAY') # Can use these, but should use 'username' and 'password'
    @accepts_all_logins = {}
  end


  def run_host(ip)
    print_status("#{ip}:#{rport} - Starting FTP login sweep")

    cred_collection = Metasploit::Framework::CredentialCollection.new(
        blank_passwords: datastore['BLANK_PASSWORDS'],
        pass_file: datastore['PASS_FILE'],
        password: datastore['PASSWORD'],
        user_file: datastore['USER_FILE'],
        userpass_file: datastore['USERPASS_FILE'],
        username: datastore['USERNAME'],
        user_as_pass: datastore['USER_AS_PASS'],
        prepended_creds: anonymous_creds
    )

    cred_collection = prepend_db_passwords(cred_collection)

    scanner = Metasploit::Framework::LoginScanner::FTP.new(
        host: ip,
        port: rport,
        proxies: datastore['PROXIES'],
        cred_details: cred_collection,
        stop_on_success: datastore['STOP_ON_SUCCESS'],
        bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
        max_send_size: datastore['TCP::max_send_size'],
        send_delay: datastore['TCP::send_delay'],
        connection_timeout: 30,
        framework: framework,
        framework_module: self,
        ssl: datastore['SSL'],
        ssl_version: datastore['SSLVersion'],
        ssl_verify_mode: datastore['SSLVerifyMode'],
        ssl_cipher: datastore['SSLCipher'],
        local_port: datastore['CPORT'],
        local_host: datastore['CHOST']
    )

    scanner.scan! do |result|
      credential_data = result.to_h
      credential_data.merge!(
          module_fullname: self.fullname,
          workspace_id: myworkspace_id
      )
      if result.success?
        credential_data[:private_type] = :password
        credential_core = create_credential(credential_data)
        credential_data[:core] = credential_core
        create_credential_login(credential_data)

        print_good "#{ip}:#{rport} - Login Successful: #{result.credential}"
      else
        invalidate_login(credential_data)
        vprint_error "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
      end
    end

  end


  # Always check for anonymous access by pretending to be a browser.
  def anonymous_creds
    anon_creds = [ ]
    if datastore['RECORD_GUEST']
      ['IEUser@', 'User@', '[email protected]', '[email protected]' ].each do |password|
        anon_creds << Metasploit::Framework::Credential.new(public: 'anonymous', private: password)
      end
    end
    anon_creds
  end

  def test_ftp_access(user,scanner)
    dir = Rex::Text.rand_text_alpha(8)
    write_check = scanner.send_cmd(['MKD', dir], true)
    if write_check and write_check =~ /^2/
      scanner.send_cmd(['RMD',dir], true)
      print_status("#{rhost}:#{rport} - User '#{user}' has READ/WRITE access")
      return 'Read/Write'
    else
      print_status("#{rhost}:#{rport} - User '#{user}' has READ access")
      return 'Read-only'
    end
  end


end