最近看了一下Pocsuite3的源码,跟着部分功能读了一遍,在这里记录一下
Pocsuite3 采用 Python3 编写,支持验证,利用及 shell 三种模式
验证:只验证漏洞是否存在,不进行攻击性行为
利用:上传木马文件获取权限
shell:与目标建立shell连接
用一个例子来介绍Pocsuite3的核心技术框架与细节
python cli.py -f "ip.txt" -r "pocs" -o "output.txt"
ip.txt
中存储的是web程序的IP地址
pocs
目录中存储的PoC脚本文件
output.txt
是将验证结果输出的文件
-f
是指读取ip.txt中的IP地址
-r
是指加载pocs目录下的poc文件
-o
是将结果输出到output.txt
这行命令要做的事情是获取ip.txt
中的IP地址,使用pocs
目录下的poc文件进行验证,最后将结果输出到output.txt
api:存放__init__.py,对要导入的包重命名,方便后续导入调用 data:存储用户需要使用数据,比如弱口令文件等等 lib:存储Pocsuite3的具体实现,包括获取目标IP,加载PoC文件,以及多线程验证等核心代码 modules:存储用户自定义的模块 plugins:存储用户自定义的插件 pocs:存储PoC文件,这里在pocs文件夹中给了3个poc文件用于测试 shellcodes:存储生成php,java,python等脚本语言的利用代码,以及反弹shell利用代码 cli.py:项目的入口文件 console.py:命令行界面
在命令行输入python cli.py -h
可以获取使用帮助
我在读的时候把它分成了这几个部分
首先是独立出来的PoC模板,用于统一PoC脚本格式。其次是输入,包括参数存储与命令行参数解析。然后是主程序,包括系统初始化和多线程运行PoC脚本。最后是输出,只有输出处理一部分用于输出格式化
首先介绍poc模板
python cli.py -n
生成模板,模板文件20231221_testpoc.py
下面来看一下这个文件
模板文件定义了DemoPOC
类,这个类是基于POCBase
的,类的属性就是输入的内容
然后定义了五个方法,用于用户完成具体的PoC逻辑,最后register_poc(DemoPOC)
注册PoC,实现动态加载
从第一行就可以看到所有poc都是基于POCBase
POCBase
在初始化时设置了一系列的属性,例如目标URL
,目标URL的协议
,目标URL的端口``POCBase
还定义了很多方法,不过最重要的是execute()
和_execute()
下面介绍execute()函数
execute
中调用build_url()
使用urlparse
解析target,获取target的端口,以及对应的协议
之后调用_execute()
来执行不同模式的攻击,默认是_verify()
,验证模式
以thinkphp_rce.py为例
先调用_check()
,然后获取URL和POST数据,parse_output
是将输出格式化为json格式
模拟HTTP请求,向目标地址的URL发生攻击payload,通过执行系统的echo语句输出关键词判断是否执行命令
然后介绍参数存储部分
在\lib\core\data.py
中定义了贯穿整个系统的五个变量
AttribDict()
是一种类似于字典的数据结构,但其中的数据不重复
conf:存储基本配置信息,后续涉及到的配置信息
kb:存储存储多线程运行PoC的配置信息,包括目标地址、加载的PoC、运行模式、输出结果、加载的PoC文件地址、多线程信息
cmd_line_options:是存储命令行输入的参数值
merged_options:存储输入值与默认值合并后的结果
paths:存储数据、插件、poc目录地址,还有临时目录、输出目录
lib\core\settings.py
中也会存储一些配置信息,例如banner信息,正则表达式,命令行解析白名单等
下面介绍输入命令后 pocsuite3解析的过程
在执行python cli.py -f "ip.txt" -r "pocs" -o "output.txt"
后,会进入cli.py的main函数
module_path()返回当前工作目录
check_environment()检查当前工作目录是否符合当前系统
set_paths()设置数据、插件、poc目录、临时目录、输出目录等,这些值是存储在paths变量中的
banner()在命令行打印banner信息
paths变量存储了pocsuite3的目录、插件目录、poc目录、用户目录等等,意味着加载PoC和插件时会自动从相应目录加载,可以不指定
下面介绍具体的输入参数解析过程
由cmd_line_parser()
函数实现对命令行参数的分组解析,以及定义DIY_OPTIONS
全局变量以列表形式存储不在默认参数中的参数
cmd_line_parser()
函数如图所示
argparse库的ArgumentParser创建解析器对象
add_argument() 添加一个新的命令行选项
add_argument_group() 给参数分组
parse_args() 根据add_argument()增加的选项自动解析命令行输入
在add_argument()
方法中,添加一个新的命令行选项,dest
指定了解析后的参数应该存储在哪个属性中,action="store_true"
指定了当这个选项存在时,对应的变量 (show_version
) 将被设置为 True,nargs='+'
代表多个参数以列表形式存储,没有输入的参数就会设置为默认值,一般是False或None
例如输入的命令是python cli.py -f "ip.txt" -r "pocs" -o "output.txt"
,经过解析返回变量args
args.url_file -> ip.txt
args.poc -> ["pocs"]
args.output_path -> output.txt
接下来介绍主程序的系统初始化部分
系统初始化部分可以细分成多个部分
它的功能根据输入的命令,对之前提到的conf、kb、merged_options等全局变量初始化
在init_options()
中实现
输入形参input_options
是cmd_line_parser()
返回结果的字典化
cmd_line_options
是之前提到的参数存储中用于存储命令行输入的参数的变量,通过字典的update
方法将命令行输入存储到cmd_line_options
,其内容如图
然后使用_set_conf_attributes()
对conf变量的初始化,默认模式的verify,默认端口为空,默认url也为空
conf如图所示,conf存储的都是默认值,cmd_line_options中存储的值是具体的,输入的值
_set_poc_options(input_options)
设置DIY_OPTIONS
,取出命令行输入的参数中不在白名单解析列表的参数
DIY_OPTIONS
是cmd_line_parser()
函数定义的全局变量,用来存储不在默认配置中的参数
然后是kb
的初始化,存储多线程验证poc相关的参数,在_set_kb_attributes()
中初始化kb
例如默认的IP列表是空,注册的PoC是空,多线程任务队列也是空
kb
如图所示
最后是merged_options
初始化,它用于存储合并后的选项。_merge_options()
用命令行输入的参数值覆盖conf
对应的参数值,最后将conf
赋值给merged_options
,这时conf
也是合并后的,conf与merged_options内容是相同的
接着是其他的功能,这6个功能与init()
中的这6个函数一一对应
用_cleanup_options()
实现,用于格式化conf中的参数值并检查参数值是否合法
先是对请求头的参数格式化,将user-agent的CRLF替换为空,将cookie转为字典存储
然后设置两个线程之间的请求间隔、请求超时时间,还有将输入的URL转为列表形式、以上我们的案例中都没有输入,所以会跳过
将输入poc转为列表形式,但是我们输入的在解析时已经转为列表形式了,之后判断输入的IP地址文件也就是ip.txt是否存在且可读
之后将输入插件转为列表形式,但是我们没输入,这里是预留的,如果用户写了插件就可以在这里把路径存储到conf中
最后是判断是否需要输出,需要注意的是我们使用的命令是有输出的,所以这里会在在conf.plugins
中加入['file_record']
,file_record
是它内置的输出插件
格式化与校验后的conf
变量
用_set_multiple_targets()
实现,来获取目标并存到kb
读取目标是指从ip.txt
中获取所有的IP地址
conf.ports
默认为空列表,conf.skip_target_port
默认为Fasle
例子对应conf.url_file
的处理逻辑
get_file_items
是读取ip.txt
内容,不读取不读取#
开头的行,然后把每行添加到列表中,最后返回该列表,返回内容是['192.168.45.128/24','127.0.0.1']
之后使用parse_target
解析
首先是CIDR地址,也就是带子网掩码的,使用ipaddress.ip_network
转为IP地址
然后是带域名的地址,用urllib.parse.urlparse
解析,并且去掉tcp://
,如果遇到http://
就不会删去
最后把每个目标IP加入到kb.targets
,后续访问kb.targets
就可以获取目标IP
这部分是从pocs文件夹中将poc脚本以模块的形式加载到系统中,在_set_pocs_modules()
实现
前面一部分的代码功能主要是判断conf.poc
是文件还是目录
_pocs
列表最后load_file_to_module
加载_pocs
列表中的poc路径
可以看到load_file_to_module
是整个动态加载的核心
先统一module_name
为前缀均为pocs_
且不带后缀名,例如pocs_thinkphp_rce
、pocs_thinkphp_rce2
、pocs_ecshop_rce
importlib.util.spec_from_file_location
:创建模块规范,告诉系统要加载file_path
(目标路径,比如pocs/thinkphp_rce.py
这个文件)为一个模块,命名为module_name
(例如pocs_thinkphp_rce
),并用自定义的PocLoader
作为加载器importlib.util.module_from_spec
:创建模块对象spec.loader.exec_module
:使用PocLoader
的exec_module()
函数加载模块这里相当于要用PocLoader
的exec_module
方法加载file_path
为module_name
然后要看PocLoader
的exec_module
方法
filename
是poc的绝对路径,poc_code
是poc文件的内容
check_requires
会获取加载的poc信息检查导入的模块,通过__import__
函数依次导入,如果导入不成功的会提示需要安装哪个模块
最后compile
对poc_code
(thinkphp_rce.py
的文件内容)进行编译,得到字节码,下次再执行poc时可以不用再转为字节码,加快系统运行速度,exec
执行compile
得到的字节码
这里相当于直接执行poc_code
(thinkphp_rce.py
的文件内容)
poc_code
中除了DemoPOC
定义外,都会有register_poc
函数,然后就要介绍该函数了
由于使用spec.loader.exec_module
动态加载,所以这里的module
就是之前的module_name
,例如pocs_thinkphp_rce
,之后生成DemoPOC
的实例化对象,也就是thinkphp_rce
的poc类,存储在kb.registered_pocs['pocs_thinkphp_rce']
中
kb.registered_pocs
如图
这个过程与动态加载PoC类似,在_set_plugins()
实现
由于指定了-o
参数,_cleanup_options()
中会在conf.plugins
中加入['file_record']
_set_plugins()
先在plugins目录中匹配conf.plugins
中的插件名,然后使用load_file_to_module
去加载,这里的逻辑与动态加载poc是相同的
模块名pocs_file_record
,路径是plugins/file_record.py
,加载器是PocLoader
,然后用exec_module()
加载,不同的是PoC文件使用register_poc()
注册,插件文件使用register_plugin()
注册
register_plugin()
用kb.plugins['results']['pocs_file_record']
存储输出的插件对象
这里把插件分为三类,获取目标的插件、获取poc的插件、和输出插件
后续在输出时使用kb.plugins['results']['pocs_file_record']
就可以访问到输出的插件对象
_set_task_queue()
初始化多线程设置,将目标IP与poc的模块名一一组合
kb.task_queue
是python中的Queue
,它可以确保数据在多个线程之间安全地传递
输出插件初始化
_init_results_plugins()
初始化输出插件,在_set_plugins()
中指定了kb.plugins.results
是'pocs_file_record'
这里相当于执行FileRecord
的初始化函数,功能是追加写模式打开output.txt
在start()
中使用run_threads()
建立多线程模型,多线程执行的函数是task_run()
这里总共765个任务,默认的线程数量是150,线程数量会选择任务数量与线程数之间最小的
run_threads()
核心部分
这里的思想是启动多个线程来共同消费一个队列,有点像生产者消费者模型
消费者:run_threads()用多线程执行task_run函数,
生产者:_set_task_queue()中把目标IP和PoC模块加入队列
然后创建150个线程执行task_run
函数,用thread.setDaemon(True)
将线程设置为守护线程,这意味着当主线程结束时,守护线程也会被自动终止,最后检查所有线程是否都已完成
task_run
函数核心代码
这里的target
和poc_module
通过访问kb.task_queue
获得的。功能是执行PoC文件的execute()函数,来间接使用payload验证。系统会建立150个线程,从队列中获取目标IP和对应的PoC文件,然后执行PoC脚本中的verify
函数验证
输出处理由task_run()
中的result_plugins_handle()
和start()
中的task_done()
进行
task_run()中的result_plugins_handle()会执行file_record.py中的handle()
file_record.py中的handle()将验证结果以json格式写入output.txt
start()
中的task_done()
由三个函数组成
首先是show_task_result
函数,会把poc执行后的结果取出,然后格式化输出一下,例如总共尝试攻击多少网站,打成功几个
result_plugins_start()
会会执行file_record.py
中的start()
,关闭文件并命令行输出文件保存地址
最后是result_compare_handle()
,显示来自各种搜索引擎的比较数据