pocsuite3部分源码浅析
pocsuite3部分源码浅析 目录 目录结构 poc模板 参数存储 命令行参数获取 系统初始化 初始化系统参数 参数格式化与合法检测 读取目标 动态加载PoC 动态加载插件 多线程初始化 输出插件初始化 多线程模型 输出处理 最近看了一下Pocsuite3的源码,跟着部分功能读了一遍,在这里记录一下 Pocsuite3 采用 Python3 编写,支持验证,利用及 shell 三种模式
pocsuite3部分源码浅析
目录
最近看了一下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模板
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

动态加载PoC
这部分是从pocs文件夹中将poc脚本以模块的形式加载到系统中,在_set_pocs_modules()实现
前面一部分的代码功能主要是判断conf.poc是文件还是目录
- 文件:拼接绝对路径,加入
_pocs列表 - 目录:读取目录所有文件名,并拼接出绝对路径,加入_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(),显示来自各种搜索引擎的比较数据
