漏洞验证框架的构思与实现

  • A+
所属分类:安全闲碎

本文首发于奇安信攻防社区

社区有奖征稿


· 基础稿费、额外激励、推荐作者、连载均有奖励,年度投稿top3还有神秘大奖!

· 将稿件提交至奇安信攻防社区(点击底部 阅读原文 ,加入社区)


点击链接了解征稿详情


历史内容:

  • 第一篇:https://forum.butian.net/share/160

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

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   }

2.直接输入 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}

3. 通过代理的形式监听获取(如 xray 浏览器代理模式)

0x03 兼容 xray

xray 的规则体系是基于 Common E xpression 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-unauthrules:  - method: GET    path: /admin/    E xpression: |      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.1Host: testphp.vulnweb.comUser-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0Accept-Encoding: gzip, deflateConnection: close

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

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

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

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

检测链控制

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 ExecE xpressionHandle(ctx controllerContext){    var result bool    var err error    poc := ctx.GetPoc()    if poc == nil {        log.Error("[rule/handle.go:ExecE xpressionHandle 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:ExecE xpressionHandle error] ", err)        return    }    if result {        ctx.Abort()    }    return}

poc运行

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

  1. 获取原始请求,根据规则进行变形,构造请求

  2. 初始化 cel 环境:注入变量、方法

  3. 初始化 cel 变量列表:注入set定义的自定义变量、注入当前请求(构造的请求)

  4. 发起构造后的请求,并将响应写入cel变量列表

  5. 执行表达式

漏洞验证框架的构思与实现

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,"nn") {        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转成rn

核心代码

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,"nn")        if len(spliteTmp) == 2 {            fileHeader := spliteTmp[0]            fileBody := spliteTmp[1]            fileHeader = strings.Replace(fileHeader,"n","rn",-1)            multiPartContent += boundary + fileHeader + "rnrn" + strings.TrimRight(fileBody ,"n") + "rn"        }    }    multiPartContent += boundary + "--" + "rn"    return multiPartContent, nil}

0x05 总结

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

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

所有的完整实现内容,可点击阅读原文进入社区查看。

0x06 参考

  • https://docs.xray.cool/#/guide/poc

  • https://github.com/chaitin/xray/tree/master/pocs

  • https://codelabs.developers.google.com/codelabs/cel-go#0

  • https://github.com/jjf012/gopoc

END



【版权说明】本作品著作权归带头大哥所有,授权补天漏洞响应平台独家享有信息网络传播权,任何第三方未经授权,不得转载。



漏洞验证框架的构思与实现
带头大哥

一个神秘而优秀的补天白帽子


敲黑板!转发≠学会,课代表给你们划重点了

复习列表





硬核黑客笔记-不聪明的蓝牙锁


你的跑步打卡数据是真实的吗?


Celery Redis未授权访问利用


无线电攻击初探


实战遇见到的好用提权方法集合


DIY 苹果无线充电绕过手机充电认证

漏洞验证框架的构思与实现


分享、点赞、在看,一键三连,yyds。
漏洞验证框架的构思与实现

点击阅读原文,加入社区,获取更多技术干货!

本文始发于微信公众号(补天平台):漏洞验证框架的构思与实现

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: