pocsuite3部分源码浅析
2024-1-4 14:14:57 Author: xz.aliyun.com(查看原文) 阅读量:1 收藏

目录

最近看了一下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_optionscmd_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_OPTIONScmd_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_rcepocs_thinkphp_rce2pocs_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:使用PocLoaderexec_module()函数加载模块

这里相当于要用PocLoaderexec_module方法加载file_pathmodule_name

然后要看PocLoaderexec_module方法

filename是poc的绝对路径,poc_code是poc文件的内容

check_requires会获取加载的poc信息检查导入的模块,通过__import__函数依次导入,如果导入不成功的会提示需要安装哪个模块

最后compilepoc_codethinkphp_rce.py的文件内容)进行编译,得到字节码,下次再执行poc时可以不用再转为字节码,加快系统运行速度,exec执行compile得到的字节码

这里相当于直接执行poc_codethinkphp_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函数核心代码

这里的targetpoc_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(),显示来自各种搜索引擎的比较数据


文章来源: https://xz.aliyun.com/t/13229
如有侵权请联系:admin#unsafe.sh