怎么编写 HTTP LoginScanner 登录扫描模块 - L-codes/metasploit-framework GitHub Wiki

这是关于如何使用最新的 LoginScanner 和 Credential API 编写 HTTP 登录模块的分步指南.

在我们开始之前, 阅读创建 Metasploit 框架登录扫描模块 可能是一个好主意, 它深入解释了 API. LoginScanner API 可以在 lib/metasploit/framework/loginscanner 目录中找到, Credential API 可以作为 metasploit-credential gem 在这里找到. 你很可能希望在编写登录模块时阅读它们.

第 1 步: 设置目标环境

对于我们的演示, 我们将使用 Symantec Web Gateway. 供应商的网站上提供了试用版. 显然, 下载/安装它将是你的第一步.

第 2 步: 设置客户端

设置客户端的目的是对登录请求和响应进行采样. 通常你可以这样做:

  • 一个网络浏览器和一个嗅探器

    1. 对于嗅探器, 你可以下载 Wireshark Wireshark 并运行它.
    2. 使用网络浏览器登录.
    3. 返回 Wireshark 并保存 HTTP 请求, 这正是你将在登录模块中发送的内容. 你还需要保存 HTTP 响应, 以便检查成功和失败的登录.
  • 带有 Burp 的浏览器

    Burp 是一种用于对 Web 应用程序进行安全测试的工具. 你可以从供应商的网站下载免费版本. 在某些情况下, Burp 比嗅探器要好得多, 因为你可以修改 HTTP 请求, 它也是捕获 HTTPS 流量的一种非常方便的方法.

    这就是你要做的.

    1. 启动 Burp.
    2. 配置 Web 浏览器的代理, 以便 Burp 可以转发流量.
    3. 使用网络浏览器登录.
    4. 回到 Burp, 你可以找到所有请求和响应的历史记录.

对于我们的示例, 这是浏览器发送到 Symantec Web Gateway 的请求:

POST /spywall/login.php HTTP/1.1
Host: 192.168.1.176
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:27.0) Gecko/20100101 Firefox/27.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://192.168.1.176/spywall/login.php
Cookie: PHPSESSID=otgam4mgjrl00h2esk3o2npt05
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 54

USERNAME=gooduser&PASSWORD=GoodPassword&loginBtn=Login

这是 Symantec Web Gateway 为成功登录而返回的响应:

HTTP/1.1 302 Found
Date: Tue, 12 May 2015 19:32:31 GMT
Server: Apache
X-Frame-Options: SAMEORIGIN
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: PHPSESSID=vmb56vhd7740oqcmth8cqtagq5; path=/; secure; HttpOnly
Location: https://192.168.1.176/spywall/executive_summary.php
Content-Length: 0
Keep-Alive: timeout=15, max=5000
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

失败的登录响应是 HTTP 200, 正文中包含以下消息:

We're sorry, but the username or password you have entered is incorrect.  Please retype your username and password. The username and password are case sensitive.

第 3 步: 从 LoginScanner 模板开始

你的登录模块主要由三个组件组成: LoginScanner 部分、辅助部分和 rpsec. 实际的 HTTP 请求和响应在 LoginScanner 部分中处理, 所以我们将从那里开始.

你最基本的 HTTP LoginScanner 模板将如下所示:

require 'metasploit/framework/login_scanner/http'

module Metasploit
  module Framework
    module LoginScanner
      class SymantecWebGateway < HTTP


        # 尝试登录服务器
        #
        # @param [Metasploit::Framework::Credential] 凭证信息对象
        # @return [Result] 指示成功或失败的 Result 对象
        def attempt_login(credential)

        end

      end
    end
  end
end

将其保存在 lib/metasploit/framework/login_scanner/ 下.

#attempt_login 方法

#attempt_login 会自动调用. 你可以在那里编写整个登录代码, 但最好分解为多个方法, 以便代码更清晰, 更容易记录和 rspec. 通常, 你希望#attempt_login 做的只是专注于制作 Result 对象, 将其传递给自定义 #login 例程, 然后返回 Result 对象. 它几乎总是看起来像这样

def attempt_login(credential)
  # 默认结果对象
  result_opts = {
    credential: credential,
    status: Metasploit::Model::Login::Status::INCORRECT,
    proof: nil,
    host: host,
    port: port,
    protocol: 'tcp'
  }

  # 合并登录结果
  # credential.public 为用户名
  # credential.private 为密码
  result_opts.merge!(do_login(credential.public, credential.private))

  # 返回 Result 对象
  Result.new(result_opts)
end

请注意:

  • 默认情况下, 我们的 proof 是 nil.
  • 状态是 Metasploit::Model::Login::Status::INCORRECT.
  • 我们正在调用#do_login, 这是我们的自定义登录方法.
  • #do_login 方法必须在我们返回 Result 对象之前更新状态和 proof.

自定义登录方式

好的, 现在让我们谈谈构建这个#do_login 方法. 这是我们发送之前采样的相同 HTTP 请求的地方.

如果你已经熟悉编写发送 HTTP 请求的 Metasploit 模块, 那么首先想到的可能是使用 HttpClient. 好吧, 在这里你根本不能这样做, 所以我们不得不退回到 Rex::Proto::Http::Client. 对你来说幸运的是, 我们通过创建另一个名为 #send_request 的请求使这一切变得更容易, 这里有一个如何使用它的示例:

send_request({'uri'=>'/'})

你将在很大程度上依赖此方法来完成你需要在此处完成的大部分工作.

好的, 现在, 让我们继续讨论如何使用 #send_request 发送登录请求. 请记住, 在登录请求中, 实际上有一个 PHPSESSID cookie, 你应该先获取它. 通常, 当你第一次请求登录页面时, Web 应用程序会为你提供会话 cookie, 这种情况经常发生.

下面是一个如何获取 PHPSESSID 的示例:

def get_session_id
  login_uri = normalize_uri("#{uri}/spywall/login.php")
  res = send_request({'uri' => login_uri})
  sid = res.get_cookies.scan(/(PHPSESSID=\w+);*/).flatten[0] || ''
  return sid
end

现在你有了会话 ID, 你终于可以发出登录请求了. 请记住, 在示例中, 我们必须将用户名、密码、loginBtn 作为 POST 请求提交. 所以让我们用 #send_request 来做到这一点:

protocol  = ssl ? 'https' : 'http'
peer      = "#{host}:#{port}"
login_uri = normalize_uri("#{uri}/spywall/login.php")

res = send_request({
  'uri' => login_uri,
  'method' => 'POST',
  'cookie' => get_session_id,
  'headers' => { 'Referer' => "#{protocol}://#{peer}/#{login_uri}" },
  'vars_post' => {
    'USERNAME' => username,
    'PASSWORD' => password,
    'loginBtn' => 'Login' # 在 HTML 表单中找到
  }
})

现在请求已发送, 我们需要检查响应 (res 的变量). 通常, 你有几个选择来确定成功登录:

  • 检查 HTTP 响应代码. 在这种情况下, 我们有一个 302 (重定向) , 但知道有时响应代码可能会撒谎, 所以这不应该是你的首选.
  • 检查 HTML. 对于某些 Web 应用程序, 你可能会收到"成功登录"消息, 你可以对其进行正则表达式. 这很可能是最准确的方法.
  • 检查 location 头. 在我们的例子中, 赛门铁克返回 302 并且不包含正文. 但它会将我们重定向到位置标头中的 spywall/executive_summary.php 页面, 因此我们可以使用它. 我们还可以尝试使用更新的会话 ID 访问 execution_summary.php, 并确保我们可以实际看到管理界面, 但请求额外的页面会增加性能损失, 所以这取决于你.

最后, 你的自定义登录方法可能如下所示:

def do_login(username, password)
  protocol  = ssl ? 'https' : 'http'
  peer      = "#{host}:#{port}"
  login_uri = normalize_uri("#{uri}/spywall/login.php")

  res = send_request({
    'uri' => login_uri,
    'method' => 'POST',
    'cookie' => get_session_id,
    'headers' => {
      'Referer' => "#{protocol}://#{peer}/#{login_uri}"
    },
    'vars_post' => {
      'USERNAME' => username,
      'PASSWORD' => password,
      'loginBtn' => 'Login' # 在 HTML 表单中找到
    }
  })

  if res && res.headers['Location'].include?('executive_summary.php')
    return {:status => LOGIN_STATUS::SUCCESSFUL, :proof => res.to_s}
  end

  {:proof => res.to_s}
end

你可以返回的结果状态有:

常量 目的
Metasploit::Model::Login::Status::DENIED_ACCESS 访问被拒绝
Metasploit::Model::Login::Status::DISABLED 帐户被禁用
Metasploit::Model::Login::Status::INCORRECT 凭据不正确
Metasploit::Model::Login::Status::LOCKED_OUT 帐户已被锁定
Metasploit::Model::Login::Status::NO_AUTH_REQUIRED 无身份验证
Metasploit::Model::Login::Status::SUCCESSFUL 成功登录
Metasploit::Model::Login::Status::UNABLE_TO_CONNECT 无法连接服务器
Metasploit::Model::Login::Status::UNTRIED 凭据尚未尝试
Metasploit::Model::Login::Status::ALL 以上所有 (一个数组)

完成后, 你的代码将如下所示:

https://github.com/rapid7/metasploit-framework/blob/master/lib/metasploit/framework/login_scanner/symantec_web_gateway.rb

第四步: 编写辅助模块

辅助模块更像是一个用户界面. 你描述模块的功能、处理选项、初始化对象和进行报告.

在我们的例子中, 一个基本的辅助模块模板是这样的:

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

require 'msf/core'
require 'metasploit/framework/login_scanner/symantec_web_gateway'
require 'metasploit/framework/credential_collection'

class MetasploitModule < Msf::Auxiliary

  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::AuthBrute
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner

  def initialize(info={})
    super(update_info(info,
      'Name'        => 'Symantec Web Gateway Login Utility',
      'Description' => %q{
        This module will attempt to authenticate to a Symantec Web Gateway.
      },
      'Author'      => [ 'sinn3r' ],
      'License'     => MSF_LICENSE,
      'DefaultOptions' =>
        {
          'RPORT'      => 443,
          'SSL'        => true,
          'SSLVersion' => 'TLS1'
        }
    ))
  end

  def run_host(ip)
  end

end

将其保存在 modules/auxiliary/scanner/http/ 下

我们的主要方法是 #run_host, 所以我们将从那里开始. 但在此之前, 我们必须初始化你的 LoginScanner 对象. 以下是你可能会如何编写它的示例.

def scanner(ip)
  @scanner ||= lambda {
    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']
    )

    return Metasploit::Framework::LoginScanner::SymantecWebGateway.new(
      configure_http_login_scanner(
        host: ip,
        port: datastore['RPORT'],
        cred_details:       cred_collection,
        stop_on_success:    datastore['STOP_ON_SUCCESS'],
        bruteforce_speed:   datastore['BRUTEFORCE_SPEED'],
        connection_timeout: 5
      ))
    }.call
end

请注意, 可以多次调用此扫描仪方法, 但使用 lambda 将允许 LoginScanner 对象仅初始化一次. 在第一次之后, 每次调用该方法时, 它只会返回 @scanner 而不是再次执行整个初始化过程.

在某些情况下, 你可能需要传递更多 datastore 选项, 也许不需要. 例如, 如果你想允许 URI 是可配置的 (它也已经是 Metasploit::Framework::LoginScanner::HTTP 中的访问器) , 那么你必须创建 datastore['URI'] 并将其传递给 configure_http_login_scanner, 像这样:

uri: datastore['URI']

然后在你的 LoginScanner 中, 将 uri 传递给 #send_request:

send_request({'uri'=>uri})

此时, scanner 方法保存了我们的 Metasploit::Framework::LoginScanner::SymantecWebGateway 对象. 如果我们调用#scan!方法, 它会触发我们之前写的#attempt_login 方法, 然后产生 Result 对象. 基本上是这样的:

scanner(ip).scan! do |result|
  # result = Our Result object
end

使用 Result 对象, 我们可以开始报告. 在大多数情况下, 你可能会使用 #create_credential_login 来报告登录成功. 并使用 #invalidate_login 报告一个坏的.

报告有效凭证

凭证 API 对凭证有很多了解, 例如何时使用、如何使用、尝试服务、目标 IP、端口等. 因此, 当我们报告时, 这就是我们为每个凭证存储的信息量. 为了使凭证报告易于使用, 你需要做的就是调用 #store_valid_credential 方法, 如下所示:

store_valid_credential(
  user: result.credential.public,
  private: result.credential.private,
  private_type: :password, # 可选
  proof: nil, # 可选
)

报告无效凭据

这是你可以使用的另一个示例:

# 报告无效凭证
#
# @param [String] 目标 IP
# @param [Fixnum] 目标端口
# @param [Result] Result 对象
# @return [void]
def report_bad_cred(ip, rport, result)
  invalidate_login(
    address: ip,
    port: rport,
    protocol: 'tcp',
    public: result.credential.public,
    private: result.credential.private,
    realm_key: result.credential.realm_key,
    realm_value: result.credential.realm,
    status: result.status,
    proof: result.proof
  )
end

至此, 你已经基本完成了辅助模块. 它可能看起来像这样: https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/scanner/http/symantec_web_gateway_login.rb

测试

最后, 确保你的模块确实有效.

测试是否成功登录:

msf auxiliary(symantec_web_gateway_login) > run

[+] 192.168.1.176:443 SYMANTEC_WEB_GATEWAY - Success: 'sinn3r:GoodPassword'
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf auxiliary(symantec_web_gateway_login) >

测试登录失败:

msf auxiliary(symantec_web_gateway_login) > run

[-] 192.168.1.176:443 SYMANTEC_WEB_GATEWAY - Failed: 'sinn3r:BadPass'
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf auxiliary(symantec_web_gateway_login) >