pocassist源码阅读
文章最后更新时间为:2021年08月18日 23:45:29
0. 背景
xray的出现,带来了yaml格式的poc扫描脚本,但是由于xray是不开源的,所以也没有研究过这种扫描器的加载方式和优缺点。直到某一天看到nuclei,让我有想法看看yaml格式的poc扫描器是怎么写的,以至于为什么使用yaml作为poc的编写方式,但是鸽到现在...
前段时间又看到各大论坛公众号转的pocassist,也看到了作者写的扫描器构思文章:https://forum.butian.net/share/160 。于是终于把阅读源码提上了日程。
之前我也用python写过poc扫描器,虽然自己也一直在用,但是存在一些问题:
- 速度慢,虽然采用python gevent协程并发,但是python语言emm,慢是真的挺慢的
- 内存管理差,在实际扫描前会生成所有task队列,如果目标和poc都比较多的情况下,可能会产生一个几百万长度的队列...
- 没了,剩下的都是优点了
其实poc扫描器还有很多,比如goby就挺不错的,但是不开源,于是在阅读pocassist之前,有两个点是我重点关注的:
- 并发管理、资源调度是怎么做的
- 有没有做到额外的优化,比如如果两个poc的发包是一样的,那么有没有合并这两个poc扫描的包。
大概只看了一个多小时源码,细节并没有看的特别仔细,主要是为了熟悉一下yaml格式poc的扫描器写法,写的不清楚/有误的地方,还请见谅。
1. 代码阅读方法
为什么一开始就写代码阅读的方法,因为我并不打算写成漏洞分析那样,把堆栈调用写出来分析,所以文章将会比较粗糙。这里先记录一下我阅读源码的方式,授人以渔?
1.1 熟悉框架
下载程序后,先从入口出发,大致了解一下程序框架,比如golang项目的目录都是比较统一的,
- cmd目录下一般是入口文件
- src/pkg目录下一般是源码
- config目录下一般是配置文件
- assets/static一般是静态资源
...
了解了目录结构,我们就可以从入口出发,看一下程序的框架,这里不需要去阅读函数内部是怎么实现的,只需要关注这个函数是干嘛的,比如pocassist,我第一遍看,只需要了解到其框架为:
- 解析命令行
- 解析配置文件(日志输出、数据库配置、设置qps、fasthttp客户端、jwt秘钥等)
- 初始化web route
- 运行web server
到这里整个程序就很简单,至于web server每个route的执行路径,后面我们再看实现细节。
1.2 由浅入深
这部分总结来说就是一句话:打断点,熟悉一遍关键动作的链路
只需要挑选关键的路径,比如执行一次扫描任务,然后给对应的route打上断点,看下这个请求从requests到response是一步步经过了哪些函数,做了哪些操作。
比如在pocassist中,我发起一次redis未授权的poc任务,经过的函数调用为:
api/routers/route.go
api/routers/v1/scan/scan.go func Url(c *gin.Context)
poc/rule/parallel.go func TaskConsumer()
poc/rule/parallel.go func RunPlugins(item *TaskItem)
poc/rule/run.go func RunPoc(inter interface{}, debug bool) (result *util.ScanResult, err error)
poc/rule/controller.go func InitPocController(req *RequestController, plugin *Plugin, cel *CelController, handles []HandlerFunc) *PocController
poc/rule/controller.go func (controller *PocController) Next()
poc/rule/handler.go func ExecScriptHandle(ctx controllerContext)
只要把这个链路上涉及到的函数和技巧搞明白,也就基本上了解了pocassist的运行流程。
2. 资源调度、并发管理
在 https://forum.butian.net/share/160 中看到作者描述pocassist提供了:
- 扫描任务调度
- 并发控制
- 速率控制
- 资源控制:避免无节制的占用主机资源(内存/cpu/带宽)
- 日志:记录检测过程中所有里程碑日志、错误日志、网络请求、响应,方便后续的回溯。
于是看这部分是怎么实现的:
1.日志
日志主要是使用了zap数据库,解析了配置文件中的
logconfig:
compress: false
max_age: 365
max_backups: 1
max_size: 50
没什么好说的
2.并发控制
这里主要是使用了ants进行的goroutine并发管理:
// poc/rule/parallel.go
// 并发测试
func RunPlugins(item *TaskItem){
// 限制插件并发数
var wg sync.WaitGroup
parallel := conf.GlobalConfig.PluginsConfig.Parallel
p, _ := ants.NewPoolWithFunc(parallel, func(item interface{}) {
RunPoc(item, false)
wg.Done()
})
defer p.Release()
oreq := item.OriginalReq
plugins := item.Plugins
task := item.Task
log.Info("[rule/parallel.go:TaskConsumer start scan]", oreq.URL.String())
for i := range plugins {
item := &ScanItem{oreq, &plugins[i], task}
wg.Add(1)
p.Invoke(item)
}
wg.Wait()
db.DownTask(task.Id)
}
默认是10个goroutine进行并发,没什么特别的地方。
3.速率控制
速率控制是使用了golang的time/rate:
//util/requests
// 创建一个limiter 限制请求速率
var limiter *rate.Limiter
// 根据配置文件初始化limiter
func InitRate() {
msQps := conf.GlobalConfig.HttpConfig.MaxQps / 10
limit := rate.Every(100 * time.Millisecond)
limiter = rate.NewLimiter(limit, msQps)
}
func LimitWait() {
limiter.Wait(context.Background())
}
...省略
func DoFasthttpRequest(req *fasthttp.Request, redirect bool) (*proto.Response, error) {
LimitWait() //调用函数进行速率限制,如果超过了limit,则会卡在这里等待别的任务
...省略
使用了golang的golang.org/x/time/rate官方库,之前没用过,特地去搜了一下,实现原理类似于一个队列,满了就堵住,空了就开放。
4.资源控制
tasks任务的生成和消费,使用了两个goroutine,其中channel长度设置为8,所以内存不会爆满,这部分在
// api/routers/v1/scan/scan.go
...忽略
go rule.TaskProducer(taskItem)
go rule.TaskConsumer()
...忽略
5.扫描任务调度
没发现这部分代码...
3. poc加载与调用
整个poc加载与调用位于poc/rule目录下面:
1.poc加载
poc在数据库中是以json格式存储的,网页涉及到的poc的增删改查就不说了,每次扫描都会从数据库中加载一次poc列表。代码如下:
// poc/rule/parallel.go
// 从数据库 中加载 POC
func LoadDbPlugin(lodeType string, array []string) ([]Plugin, error) {
// 数据库数据
var dbPluginList []db.Plugin
// plugin对象
var plugins []Plugin
switch lodeType {
case LoadMulti:
// 多个
tx := db.GlobalDB.Where("vul_id IN ? AND enable = ?", array, 1).Find(&dbPluginList)
if tx.Error != nil {
log.Error("[rule/parallel.go:LoadDbPlugin load multi err]", tx.Error)
return nil, tx.Error
}
default:
// 默认执行全部启用规则
tx := db.GlobalDB.Where("enable = ?", 1).Find(&dbPluginList)
if tx.Error != nil {
log.Error("[rule/parallel.go:LoadDbPlugin load all err]", tx.Error)
return nil, tx.Error
}
}
log.Error("[rule/parallel.go:LoadDbPlugin load plugin number]", len(dbPluginList))
for _, v := range dbPluginList {
poc, err := ParseJsonPoc(v.JsonPoc)
if err != nil {
continue
}
plugin := Plugin{
VulId: v.VulId,
Affects: v.Affects,
JsonPoc: poc,
Enable: v.Enable,
}
plugins = append(plugins, plugin)
}
return plugins, nil
}
2.poc执行
poc的执行九九归一到一个函数RunPoc()
// poc/rule/run.go
// 函数有删减
func RunPoc(inter interface{}, debug bool) (result *util.ScanResult, err error) {
scanItem := inter.(*ScanItem)
var requestController RequestController
var celController CelController
// 设置poc运行期间的各类请求 Original、New、Fast等
err = requestController.Init(scanItem.OriginalReq)
// handle有两种:ExecExpressionHandle、ExecScriptHandle,详见handle.go
handles := getHandles(scanItem.Plugin.Affects)
// 初始化 cel env,向 cel 环境中注入类型、方法
err = celController.Init(scanItem.Plugin.JsonPoc)
// 处理poc中的set字段,这部分可以参考xray的poc说明
err = celController.InitSet(scanItem.Plugin.JsonPoc, requestController.New)
switch scanItem.Plugin.Affects {
// 影响为参数类型
case AffectAppendParameter, AffectReplaceParameter:
// 初始化controller并且执行
controller := InitPocController(&requestController, scanItem.Plugin, &celController, handles)
// 这部分是关键,会依次顺序执行controller中handle
controller.Next()
...
case AffectDirectory, AffectServer, AffectURL, AffectContent:
...
case AffectScript:
...
// 默认返回没有漏洞
return &util.InVulnerableResult, nil
}
其中controller.Next()为:
func (controller *PocController) Next() {
for controller.Index < int64(len(controller.Handles)) {
controller.Handles[controller.Index](controller)
controller.Index++
}
}
依次执行controller的handle函数,在redis未授权poc中,也就是ExecScriptHandle:
func ExecScriptHandle(ctx controllerContext) {
pocName := ctx.GetPocName()
scanFunc := scripts.GetScriptFunc(pocName)
if scanFunc == nil {
log.Error("[rule/handle.go:ExecScriptHandle error] ", "scan func is nil")
ctx.Abort()
return
}
log.Info("[rule/handle.go:ExecScriptHandle script start]" + pocName)
var isHTTPS bool
// 处理端口
defaultPort := 80
originalReq := ctx.GetOriginalReq()
if originalReq == nil {
log.Error("[rule/handle.go:ExecScriptHandle error] ", "original request is nil")
ctx.Abort()
return
}
if originalReq.URL.Scheme == "https" {
isHTTPS = true
defaultPort = 443
}
if originalReq.URL.Port() != "" {
port, err := strconv.ParseUint(originalReq.URL.Port(), 10, 16)
if err != nil {
ctx.Abort()
return
}
defaultPort = int(port)
}
args := &scripts.ScriptScanArgs{
Host: originalReq.URL.Hostname(),
Port: uint16(defaultPort),
IsHTTPS: isHTTPS,
}
result, err := scanFunc(args)
if err != nil {
log.Error("[rule/handle.go:ExecScriptHandle error] ", err)
ctx.Abort()
return
}
ctx.SetResult(result)
ctx.Abort()
}
到这里一次redis未授权poc扫描就结束了
在pocassist中poc中涉及到两种:http poc和脚本poc。比如redis未授权需要发送socket包,无法使用http的poc,所以作者实现了script poc的加载,这部分poc无法在网页上增删改查。而一些web漏洞,则可以通过网页进行管理,才会涉及到cel表达式,下面是普通http poc的handler
func ExecExpressionHandle(ctx controllerContext) {
var result bool
var err error
poc := ctx.GetPoc()
if poc == nil {
log.Error("[rule/handle.go:ExecExpressionHandle error] ", "poc is nil")
return
}
if poc.Groups != nil {
result, err = ctx.Groups(ctx.IsDebug())
} else {
result, err = ctx.Rules(poc.Rules, ctx.IsDebug())
}
if err != nil {
log.Error("[rule/handle.go:ExecExpressionHandle error] ", err)
return
}
if result {
ctx.Abort()
}
return
}
可以看出调用了controller的Rules接口和Groups接口,对每一个规则进行遍历、扫描、检测,兼容了xray的poc语法。
在http的poc中,作者又细分了几种poc的类型:
- directory 目录型扫描。检测目标为目录
- text 页面内容检测。检测目标为原始请求的响应,因此直接使用原始请求请求头。
- url url级漏洞检测。检测路径为原始请求的uri,除了路径外,均使用规则定义。
- server server级漏洞检测。检测路径为原始请求的server:port+规则中定义的path,其他均使用规则定义。
- param 参数级漏洞检测。pocassist 将认为检测目标为原始请求中的参数。
这部分内容就不多说了,可以参考:https://pocassist.jweny.top/pocedit
4. 总结
目前发现好的地方:
- 资源、内存、并发的控制
- 网页可视化,可能对于别人来说比较方便吧,我个人还是喜欢命令行
感觉有些不足的地方:
- poc、漏洞描述、涉及组件三部分有点重合,作为脚本小子,我是不喜欢写个poc之前还去写很长的漏洞描述
- 没有做到我一开始期待看到的一点:如果两个poc的发包是一样的,并没有合并这两个poc扫描的包。
- poc管理目前只支持http类型的poc,对于需要发送tcp包的poc暂不支持。
后面有时间会继续阅读: https://github.com/projectdiscovery/nuclei