编写 Python 模块
一、为 Metasploit 编写 Python 模块
这是一个如何为 Metasploit 框架编写 Python 模块的示例,该模块通过 JSON-RPC 使用 Python 的 metasploit 库与框架进行通信,采用 stdin/stdout。外部 Python 模块应支持 Python 3.5 及更高版本,不再支持 Python 2.7。
1、执行
在模块的顶部包含以下行:
#!/usr/bin/env python3
确保文件被标记为可执行文件。
2、Python 库
该库目前支持几个函数调用,可以用来向 Metasploit 框架报告信息。可以通过以下代码将 metasploit 库加载到你的 Python 模块中:
from metasploit import module
Metasploit 库的位置会在执行 Python 模块之前自动添加到 PYTHONPATH 环境变量中。
3、描述模块
Metasploit 模块包含有关模块作者、与漏洞相关的其他信息来源的引用、模块描述、选项等内容。
Python 模块也需要包含这些元数据。数据的结构与 Ruby 编写的模块类似。以下是一个元数据的示例模板:
metadata = {
'name': '<name>',
'description': '''
<description>
''',
'authors': [
'<author>',
'<author>'
],
'date': 'YYYY-MM-DD',
'license': '<license>',
'references': [
{'type': 'url', 'ref': '<url>'},
{'type': 'cve', 'ref': 'YYYY-#'},
{'type': 'edb', 'ref': '#'},
{'type': 'aka', 'ref': '<name>'}
],
'type': '<module type>',
'options': {
'<name>': {'type': 'address', 'description': '<description>', 'required': <True/False>, 'default': None},
'<name>': {'type': 'string', 'description': '<description>', 'required': <True/False>, 'default': None},
'<name>': {'type': 'string', 'description': '<description>', 'required': <True/False>, 'default': None}
}
}
4、模块类型
如元数据模板信息所示,模块还包括一个类型。模块类型用于选择一个 ERB 模板,该模板生成该模块的 Ruby 文档。可以在这里找到 ERB 模板。目前可用的模板如下:
-
remote_exploit_cmd_stager
-
capture_server
-
dos
-
single_scanner
-
multi_scanner
-
remote_exploit_cmd_stager 模块类型用于编写命令执行或代码注入漏洞的利用,并根据指定的命令启动器(command stager)提供要注入到易受攻击代码中的命令。
-
capture_server 模块类型用于设计一个模拟服务的模块,用于捕获连接客户端的凭据。
-
dos 模块类型用于发送数据包到远程服务,导致该服务崩溃或使其无法使用。
-
single_scanner 模块类型用于创建扫描主机的模块,不进行批处理。
-
multi_scanner 模块类型用于批量扫描主机的模块。
batch_size
选项在 multi_scanner ERB 模板中注册,默认为 200。
5、选项
元数据中的选项字典是模块在加载到 msfconsole 时可用的选项。选项可以是必需的(对于模块运行是必要的)或非必需的(提供附加功能)。
6、通信
为了将元数据以及 Python 模块的启动函数传递给 msfconsole,可以使用 module.run()
函数。module.run()
函数接受两个参数,第一个是元数据,第二个是执行模块时 msfconsole 调用的回调函数。代码示例如下:
def run(args):
# 你的代码在这里
pass
if __name__ == '__main__':
module.run(metadata, run)
当 msfconsole 向 Python 模块发送 describe
请求时,模块会返回元数据。当 msfconsole 向模块发送 run
请求时,回调函数(在本例中为 run
)将会被调用,并传入 msfconsole 提供的参数。
7、日志处理
可以设置并使用 LogHandler
来在 Python 模块执行过程中向框架传递状态信息。以下是使用 LogHandler
的代码示例:
import logging
from metasploit import module
module.LogHandler.setup(msg_prefix='logging test: ')
logging.info('info')
logging.error('error')
logging.warning('warning')
logging.debug('debug')
module.LogHandler.setup()
函数用于创建一个 Handler 和 Formatter,这将调用 module.log()
并传递适当的日志级别。
二、完整示例
1、示例代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 标准模块
import logging
# 附加模块
dependencies_missing = False
try:
import requests
except ImportError:
dependencies_missing = True
from metasploit import module
metadata = {
'name': 'Python Module Example',
'description': '''
Python 与 msfconsole 的通信。
''',
'authors': [
'Jacob Robles'
],
'date': '2018-03-22',
'license': 'MSF_LICENSE',
'references': [
{'type': 'url', 'ref': 'https://blog.rapid7.com/2017/12/28/regifting-python-in-metasploit/'},
{'type': 'aka', 'ref': 'Coldstone'}
],
'type': 'single_scanner',
'options': {
'targeturi': {'type': 'string', 'description': '基础路径', 'required': True, 'default': '/'},
'rhost': {'type': 'address', 'description': '目标地址', 'required': True, 'default': None}
}
}
def run(args):
module.LogHandler.setup(msg_prefix='{} - '.format(args['rhost']))
if dependencies_missing:
logging.error('模块依赖 (requests) 丢失,无法继续')
return
# 你的代码在这里
try:
r = requests.get('https://{}/{}'.format(args['rhost'], args['targeturi']), verify=False)
except requests.exceptions.RequestException as e:
logging.error('{}'.format(e))
return
logging.info('{}...'.format(r.text[0:50]))
if __name__ == '__main__':
module.run(metadata, run)
2、说明
该示例向指定的 rhost
和 targeturi
发送一个 GET 请求,并调用 logging.info()
显示返回结果的前 50 个字符,输出将在 msfconsole 中显示。
3、调试 Python 模块
如果你想直接在 Metasploit 框架文件夹中作为独立程序运行外部模块,可以指定 Python 路径来包含 Metasploit 库支持,并直接运行模块:
$ PYTHONPATH=./lib/msf/core/modules/external/python:$PYTHONPATH python3 ./modules/auxiliary/scanner/wproxy/att_open_proxy.py
Python 模块将等待从 stdin 接收 JSON-RPC 输入。输入请求以运行模块:
{ "jsonrpc": "2.0", "id": "1337", "method": "run", "params": { "rhosts": ["127.0.0.1"], "rport": "49152" } }
你会看到 JSON-RPC 响应打印到 stdout:
{"jsonrpc": "2.0", "method": "message", "params": {"level": "debug", "message": "127.0.0.1:49152 - Connected"}}
{"jsonrpc": "2.0", "method": "message", "params": {"level": "debug", "message": "127.0.0.1:49152 - Received 5 bytes"}}
{"jsonrpc": "2.0", "method": "message", "params": {"level": "info", "message": "127.0.0.1:49152 - Does not match"}}
{"jsonrpc": "2.0", "method": "message", "params": {"level": "debug", "message": "127.0.0.1:49152 - Does not match with: bytearray(b'xxxxx')"}}
你也可以通过管道传输 JSON-RPC 请求进行自动化:
echo '{ "jsonrpc": "2.0", "id": "1337", "method": "run", "params": { "rhosts": ["127.0.0.1"], "rport": "49152" } }' | PYTHONPATH=./lib/msf/core/modules/external/python:$PYTHONPATH python3 ./modules/auxiliary/scanner/wproxy/att_open_proxy.py
Python 外部模块也可以通过命令行选项直接运行:
$ PYTHONPATH=./lib/msf/core/modules/external/python:$PYTHONPATH python3.9 ./modules/auxiliary/scanner/wproxy/att_open_proxy.py --help
输出:
usage: att_open_proxy.py [-h] --rhosts RHOSTS [--rport RPORT] [ACTION]
The Arris NVG589 and NVG599 routers configured with AT&T U-verse firmware 9.2.2h0d83 expose an un-authenticated proxy that allows connecting from WAN to LAN by MAC address.
positional arguments:
ACTION The action to take (['run'])
optional arguments:
-h, --help show this help message and exit
--rport RPORT The target port, (default: 49152)
required arguments:
--rhosts RHOSTS The target address
例如,运行:
PYTHONPATH=./lib/msf/core/modules/external/python:$PYTHONPATH python3 ./modules/auxiliary/scanner/wproxy/att_open_proxy.py --rhosts 127.0.0.1 --rport 49152
对于漏洞利用模块,负载使用 Base64 编码并通过顶层 payload_encoded
键进行指定,这里实现了该功能。以下是(现在已删除的)ms17_010_eternalblue_win8.py
模块运行的示例:
$ cat options.json
{
"jsonrpc": "2.0",
"id": "1337",
"method": "run",
"params": {
"VERBOSE": true,
"RHOST": "192.168.144.131",
"RPORT": "445",
"GroomAllocations": 13,
"ProcessName": "spoolsv.exe",
"SMBUser": "test",
"SMBPass": "123456",
"payload_encoded": "/EiD5PDozAAA...etc...==="
}
}
$ cat options.json | PYTHONPATH=./lib/msf/core/modules/external/python:$PYTHONPATH python3 modules/exploits/windows/smb/ms17_010_eternalblue_win8.py
{"jsonrpc": "2.0", "method": "message", "params": {"level": "info", "message": "shellcode size: 1221"}}
{"jsonrpc": "2.0", "method": "message", "params": {"level": "info", "message": "numGroomConn: 13"}}
{"jsonrpc": "2.0", "method": "message", "params": {"level": "info", "message": "Target OS: Windows 10 Pro 10240"}}
{"jsonrpc": "2.0", "method": "message", "params": {"level": "info", "message": "got good NT Trans response"}}
{"jsonrpc": "2.0", "method": "message", "params": {"level": "info", "message": "got good NT Trans response"}}
{"jsonrpc": "2.0", "method": "message", "params": {"level": "info", "message": "SMB1 session setup allocate nonpaged pool success"}}
{"jsonrpc": "2.0", "method": "message", "params": {"level": "info", "message": "SMB1 session setup allocate nonpaged pool success"}}
3、添加断点到 Python 代码
要在 Python 代码中添加断点,可以使用以下代码片段。请注意,交互式断点只会在作为独立 Python 脚本运行外部模块时有效,而在 msfconsole 中运行时不会生效:
import pdb; pdb.pry
4、编码风格
Metasploit 中的所有 Python 代码都应遵循 PEP 8 标准。与 Metasploit 的 Ruby 风格相比,最大的差异在于:
- 函数之间有两个空行(但类方法之间不需要)
- 不同类型的代码(如导入和元数据)之间需要两个空行(见上文)
- 使用四个空格进行缩进
编写模块时的一些编码注意事项:
- 使用
"foo {}".format('bar')
替代%
插值 - 保持回调方法简短且易读。如果方法变得复杂,可以将子任务拆分为命名清晰的函数
- 变量名应具有描述性、可读且简短(参考命名指南)
- 如果需要使用 Python 3 特性,在模块中使用
#!/usr/bin/env python3
作为 shebang - 如果你有很多 Python 2.7 的遗留代码或需要一个 2.7 库,可以使用
#!/usr/bin/env python2.7
(特别是 macOS 默认不提供 python2 执行文件) - 如果可能,保持模块兼容 Python 2 和 3,并使用
#!/usr/bin/env python
作为 shebang
5、常见问题解答
为什么在 msfconsole 中搜索不到该模块?
模块可能存在错误并且未能在 msfconsole 中加载。检查框架日志文件 ~/.msf4/logs/framework.log
查看错误信息。此外,如果模块没有标记为可执行文件,它在 msfconsole 中也不会显示。
为什么 Python 模块的输出没有显示在 msfconsole 中?
外部模块通过 JSON-RPC 与框架进行通信。如果你的 Python 模块包含 print
语句,框架可能不会识别这些作为 JSON-RPC 请求。使用 LogHandler
或 module.log()
来发送状态信息,这些信息将显示在 msfconsole 中。