漏洞验证框架的构思与实现(二) - jweny

admin 2021年12月31日14:44:43评论262 views字数 7398阅读24分39秒阅读模式

历史内容:

本篇主要聊一聊框架核心模块:规则体系。

0x01 写在前面

在上篇文章提到过,xray 有着完善的规则体系,而且业内同志提交的 poc 积极性很高,所以更新较快,质量也很高,有很多值得借鉴之处。因此我想在 xray 规则的基础上设计一套规则体系,既能兼容 xray 的 poc,又能融入自己的一些想法。

xray poc 的运行原理,简单来说就是根据自定义的规则原始请求变形,然后获取变形后的响应,再检查响应是否匹配规则中定义的表达式。

我认为这里有几个关键点:

  1. 原始请求来源
  2. 兼容 xray 现在规则
  3. 扩展 xray:组包时更细致的分类

0x02 原始请求来源

原始请求就是根据待检测目标构造 HTTP 请求包,构造原始包时通常有以下来源:

  1. url 。根据输入的 url 补充基础请求头,生成基础的 get http 请求,类似于Burpsuite RepeaterParse URL As Request

核心代码:

func GenOriginalReq(url string) (*http.Request, error) {
    // 生成原始请求,如果没有协议默认使用http
    if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
    } else {
        url = "http://" + url
    }
    originalReq, err := http.NewRequest("GET", url, nil)
    if err != nil {
        log.Error("util/requests.go:GenOriginalReq original request gen error", url, err)
        return nil, err
    }
    originalReq.Header.Set("Host", originalReq.Host)
    originalReq.Header.Set("Accept-Encoding", "gzip, deflate")
    originalReq.Header.Set("Accept","*/*")
    originalReq.Header.Set("User-Agent", conf.GlobalConfig.HttpConfig.Headers.UserAgent)
    originalReq.Header.Set("Accept-Language","en")
    originalReq.Header.Set("Connection","close")

    return originalReq, nil
}
  1. 直接输入 http 报文文件。解析该文件,生成 http 请求,类似于Burpsuite RepeaterParse from file。go 语言可使用原生 http 包的http.ReadRequest方法实现。

核心代码:

func GenOriginalReqFromRaw(filePath string) (*http.Request, error)  {
    raw, err := ioutil.ReadFile(filePath)
    if err != nil {
        c.JSON(msg.ErrResp("请求报文文件解析失败"))
        return nil, err
    }

    oreq, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(raw)))
    if err != nil {
        return nil, err
    }
    if !oreq.URL.IsAbs() {
        scheme := "http"
        oreq.URL.Scheme = scheme
        oreq.URL.Host = oreq.Host
    }
    return oreq, nil
}
  1. 三是通过代理的形式监听获取(如 xray 浏览器代理模式)

0x03 兼容 xray

xray 的规则体系是基于 Common Expression Language ( cel 表达式) 实现的。Google 官方提供了 go 语言的 cel 包 cel-go。通过向 cel 环境注入变量、方法,即可在该环境内动态的执行表达式,并返回 bool 型结果(符合 or 不符合)。

cel表达式

运行 cel 表达式有三个过程:

  1. 构建 cel 环境。初始化一个 cel.Env
  2. 向 cel 环境中注入类型、方法。xray 规则所有的变量、方法,可参考这里。为兼容 xray ,依照文档实现所有类型、方法(包括文档中未提及的 icontains 方法),完整实现
  3. 计算表达式。计算表达式的逻辑很简单,传入表达式、cel 环境、以及要检测变量列表即可。对于 xray 来说,要检测的变量列表,除了注入到环境的几个类型(UrlType、Request、Response、Reverse)之外,还要考虑 Set 中自定义的变量,表达式中使用 {{}}的部分需要替换成 Set 的 Value。完整实现

值得一提的是,set 中自定义的变量可能引用之前的变量,所以 set 一定是要有序加载的,,因此建议不要使用 go 的map 去保存 set(因为 map 是无序的)。可以使用gopkg.in/yaml.v2包的 MapSlice保存。

我们可以定一个 cel controller 去管理整个 cel 的生命周期,完整实现

构造请求

上面提到过, poc 的运行阶段,一共有两个请求:一是原始请求,二是根据规则构造的请求。

poc 规则直接决定了对原始请求如何变形。这里以 airflow 未授权访问漏洞的 poc 举例。

name: poc-yaml-airflow-unauth
rules:
  - method: GET
    path: /admin/
    expression: |
      response.status == 200 && response.body.bcontains(b"<title>Airflow - DAGs</title>") && response.body.bcontains(b"<h2>DAGs</h2>")

检测目标为testphp.vulnweb.com/aaa/bbb.html 时,原始请求就应该是:

GET /aaa/bbb.html HTTP/1.1
Host: testphp.vulnweb.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Accept-Encoding: gzip, deflate
Connection: close

根据 poc 规则,漏洞所在的 path 为/admin,如果airflow部署在二级目录/aaa/,那么 poc的请求就应该是/aaa/admin。规则中没有配置请求头,那么就使用原始请求的请求头。

xray 是以目录为单位进行扫描,因此 xray 变形后请求为:

GET /aaa/admin/ HTTP/1.1
Host: testphp.vulnweb.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Accept-Encoding: gzip, deflate
Connection: close

为了追求性能,尽可能的小内存占用,发起请求时,我使用了更高效的 fasthttp 替换掉原生的 http 包。因此定义了一个*fasthttp.Request去保存构造的 go 原生的 http对象。

完整实现

检测链控制

xray 的单个 rule 的表达式返回值是一个bool 型结果(符合 or 不符合),这个结果作为整个 rule 的值,也就是对应了一个请求/响应。

通常来说一个 poc 是有多个请求和响应的检测链。xray 中定义了两种检测链格式。

一是 rules 。 rule 组成的有序列表([]rule),值为 true 的 rule ,如果后面还有其他 rule ,则继续执行后续 rule ,如果后续没有其他 rule ,则表示该 poc 的结果是 true 。

二是 groups 。 rules 组成的列表map[string]rules,只要有一组 rules 执行成功,就认为该 poc 的结果为 true 。

所以原则上来说 groups 和 rules 只能存在一个。我这里才去的方式是先校验 groups 是否为空,非空才去执行rules。核心代码:

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
}

poc运行

基于以上分析,poc 中运行的流程如下:

  1. 获取原始请求,根据规则进行变形,构造请求
  2. 初始化 cel 环境:注入变量、方法
  3. 初始化 cel 变量列表:注入set定义的自定义变量、注入当前请求(构造的请求)
  4. 发起构造后的请求,并将响应写入cel变量列表
  5. 执行表达式

poc 运行过程的完整实现

0x04 扩展xray

xray 以目录为单位进行扫描,足够覆盖绝大多数场景,但我认为还可以再细分下。

  1. 目录级的定义可以再完整些。以testphp.vulnweb.com/aaa/bbb/ccc.html为例,如果以目录为单位的话,应该扫描三个目录://aaa/bbb
  2. 某些通用漏洞的组件通常独占端口且部署在一级目录,为减少请求数,只扫描一级目录/
  3. 每个请求的响应(包括原始请求和每个变形后的请求)都应该检查一遍响应,例如是否为常见 CMS /框架的报错、绝对路径泄露、IP泄露、数据库报错等等。
  4. 参数存在 SSRF参数型的存储型 XSS等,是需要逐一对原始请求的参数进行处理:直接替换为 payload 或者在原始参数后面拼 payload 。
  5. 以上扫描只针对 http web 应用。对于一些复杂的 poc 或者 tcp 扫描的漏洞(如 redis 未授权)等,应该加载支持自定义脚本。

因此我在原有 xray 规则基础上,新加了一个规则类型的字段,根据规则所属哪种类型,去执行不同的变形逻辑。

核心代码:

// 根据原始请求 + rule 生成并发起新的请求
func (controller *PocController) DoSingleRuleRequest(rule *Rule) (*proto.Response, error) {
    fastReq := controller.Request.Fast
    // fixReq : 根据规则对原始请求进行变形
    fixedFastReq := fasthttp.AcquireRequest()
    fastReq.CopyTo(fixedFastReq)
    curPath := string(fixedFastReq.URI().Path())

    affects := controller.Plugin.Affects

    switch affects {
    // 情况4 参数级
    case AffectAppendParameter, AffectReplaceParameter:
        for k, v := range rule.Headers {
            fixedFastReq.Header.Set(k, v)
        }
        return util.DoFasthttpRequest(fixedFastReq, rule.FollowRedirects)
    //  情况3 content级
    case AffectContent:
        return util.DoFasthttpRequest(fixedFastReq, rule.FollowRedirects)
    // 情况1 dir级
    case AffectDirectory:
        // 目录级漏洞检测 判断是否以 "/"结尾
        if curPath != "" && strings.HasSuffix(curPath, "/") {
            // 去掉规则中的的"/" 再拼
            curPath = fmt.Sprint(curPath, strings.TrimPrefix(rule.Path, "/"))
        } else {
            curPath = fmt.Sprint(curPath, "/" ,strings.TrimPrefix(rule.Path, "/"))
        }
    // 情况2
    case AffectServer:
        curPath = rule.Path
    // url级(直接使用原始请求头,只替换路径和完整post参数)
    case AffectURL:
        //curPath = curPath, strings.TrimPrefix(rule.Path, "/"))
    default:
    }
    // 兼容xray: 某些 POC 没有区分path和query
    curPath = strings.ReplaceAll(curPath, " ", "%20")
    curPath = strings.ReplaceAll(curPath, "+", "%20")

    fixedFastReq.URI().DisablePathNormalizing= true
    fixedFastReq.URI().Update(curPath)

    for k, v := range rule.Headers {
        fixedFastReq.Header.Set(k, v)
    }
    fixedFastReq.Header.SetMethod(rule.Method)

    // 处理multipart
    contentType := string(fixedFastReq.Header.ContentType())
    if strings.HasPrefix(strings.ToLower(contentType),"multipart/form-Data") && strings.Contains(rule.Body,"\n\n") {
        multipartBody, err := util.DealMultipart(contentType, rule.Body)
        if err != nil {
            return nil, err
        }
        fixedFastReq.SetBody([]byte(multipartBody))
    }else {
        fixedFastReq.SetBody([]byte(rule.Body))
    }
    return util.DoFasthttpRequest(fixedFastReq, rule.FollowRedirects)
}

DealMultipart方法是处理用到multipart的 poc (例如验证文件上传),应当按照 RFC 规定要将multipart的每个文件头和分隔符的\n转成\r\n

核心代码:

func DealMultipart(contentType string, ruleBody string) (result string, err error) {
    errMsg := ""
    // 处理multipart的/n
    re := regexp.MustCompile(`(?m)multipart\/form-Data; boundary=(.*)`)
    match := re.FindStringSubmatch(contentType)
    if len(match) != 2 {
        errMsg = "no boundary in content-type"
        //logging.GlobalLogger.Error("util/requests.go:DealMultipart Err", errMsg)
        return "", errors.New(errMsg)
    }
    boundary := "--" + match[1]
    multiPartContent := ""

    // 处理rule
    multiFile := strings.Split(ruleBody, boundary)
    if len(multiFile) == 0 {
        errMsg = "ruleBody.Body multi content format err"
        //logging.GlobalLogger.Error("util/requests.go:DealMultipart Err", errMsg)
        return multiPartContent, errors.New(errMsg)
    }

    for _, singleFile := range multiFile {
        //  处理单个文件
        //  文件头和文件响应
        spliteTmp := strings.Split(singleFile,"\n\n")
        if len(spliteTmp) == 2 {
            fileHeader := spliteTmp[0]
            fileBody := spliteTmp[1]
            fileHeader = strings.Replace(fileHeader,"\n","\r\n",-1)
            multiPartContent += boundary + fileHeader + "\r\n\r\n" + strings.TrimRight(fileBody ,"\n") + "\r\n"
        }
    }
    multiPartContent += boundary + "--" + "\r\n"
    return multiPartContent, nil
}

完整实现

0x05 总结

本文描述的 pocassist 的规则体系具体实现。在兼容 xray yaml 规则的基础上,对请求变形设计了更细致的分类。想了解具体实现的师傅可以先自行研究下源码,我也会在后续文档逐一分析具体的实现细节。

如果文章内有描述不清或其他问题,烦请各位师傅斧正。

0x06 参考

BY:先知论坛

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年12月31日14:44:43
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   漏洞验证框架的构思与实现(二) - jwenyhttps://cn-sec.com/archives/709460.html

发表评论

匿名网友 填写信息