编写 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" etcstatus
: 尝试登录的状态. 这些状态值来自 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_key
和 has_default_realm
应根据你的 LoginScanner 是否具有这些设置进行设置. (稍后会详细介绍)
LoginScanner 始终会尝试收集 Credential 来尝试测试一个主机和端口. 所以每个 LoginScanner 仅会尝试登录一个指定服务目标.
属性
connection_timeout
: 等待连接的 timeoutcred_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 withset_sane_defaults
. If the port isn't set by the user it will useDEFAULT_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 域或数据库名称之类的 realm
的 LoginScanner
.
-
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_level
、last_attempted_at
和 status
等信息, 再提交到 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