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扫描器,虽然自己也一直在用,但是存在一些问题:

  1. 速度慢,虽然采用python gevent协程并发,但是python语言emm,慢是真的挺慢的
  2. 内存管理差,在实际扫描前会生成所有task队列,如果目标和poc都比较多的情况下,可能会产生一个几百万长度的队列...
  3. 没了,剩下的都是优点了

其实poc扫描器还有很多,比如goby就挺不错的,但是不开源,于是在阅读pocassist之前,有两个点是我重点关注的:

  1. 并发管理、资源调度是怎么做的
  2. 有没有做到额外的优化,比如如果两个poc的发包是一样的,那么有没有合并这两个poc扫描的包。

大概只看了一个多小时源码,细节并没有看的特别仔细,主要是为了熟悉一下yaml格式poc的扫描器写法,写的不清楚/有误的地方,还请见谅。

1. 代码阅读方法

为什么一开始就写代码阅读的方法,因为我并不打算写成漏洞分析那样,把堆栈调用写出来分析,所以文章将会比较粗糙。这里先记录一下我阅读源码的方式,授人以渔?

1.1 熟悉框架

下载程序后,先从入口出发,大致了解一下程序框架,比如golang项目的目录都是比较统一的,

  • cmd目录下一般是入口文件
  • src/pkg目录下一般是源码
  • config目录下一般是配置文件
  • assets/static一般是静态资源
    ...

了解了目录结构,我们就可以从入口出发,看一下程序的框架,这里不需要去阅读函数内部是怎么实现的,只需要关注这个函数是干嘛的,比如pocassist,我第一遍看,只需要了解到其框架为:

  1. 解析命令行
  2. 解析配置文件(日志输出、数据库配置、设置qps、fasthttp客户端、jwt秘钥等)
  3. 初始化web route
  4. 运行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

5. 参考

1 + 5 =
快来做第一个评论的人吧~