笔者是个基层的"安全研究员",poc 编写算是我最基础的日常工作。本文描述的是我所构思的一款漏洞验证框架以及具体实现。
历史内容:
本篇主要聊一聊框架核心模块:规则体系。
0x01 写在前面
在上篇文章提到过,xray 有着完善的规则体系,而且业内同志提交的 poc 积极性很高,所以更新较快,质量也很高,有很多值得借鉴之处。因此我想在 xray 规则的基础上设计一套规则体系,既能兼容 xray 的 poc,又能融入自己的一些想法。
xray poc 的运行原理,简单来说就是根据自定义的规则
对原始请求
变形,然后获取变形后的响应,再检查响应是否匹配规则中定义的表达式。
我认为这里有几个关键点:
- 原始请求来源
- 兼容 xray 现在规则
- 扩展 xray:组包时更细致的分类
0x02 原始请求来源
原始请求就是根据待检测目标构造 HTTP 请求包,构造原始包时通常有以下来源:
-
url 。根据输入的 url 补充基础请求头,生成基础的 get http 请求,类似于
Burpsuite Repeater
的Parse 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 }
-
直接输入 http 报文文件。解析该文件,生成 http 请求,类似于
Burpsuite Repeater
的Parse 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 }
-
通过代理的形式监听获取(如 xray 浏览器代理模式)
0x03 兼容 xray
xray 的规则体系是基于 Common Expression Language ( cel 表达式) 实现的。Google 官方提供了 go 语言的 cel 包 cel-go。通过向 cel 环境注入变量、方法,即可在该环境内动态的执行表达式,并返回 bool 型结果(符合 or 不符合)。
cel表达式
运行 cel 表达式有三个过程:
- 构建 cel 环境。初始化一个 cel.Env
- 向 cel 环境中注入类型、方法。xray 规则所有的变量、方法,可参考这里。为兼容 xray ,依照文档实现所有类型、方法(包括文档中未提及的 icontains 方法),完整实现。
- 计算表达式。计算表达式的逻辑很简单,传入表达式、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
去保存构造的包。
检测链控制
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 中运行的流程如下:
- 获取原始请求,根据规则进行变形,构造请求
- 初始化 cel 环境:注入变量、方法
- 初始化 cel 变量列表:注入set定义的自定义变量、注入当前请求(构造的请求)
- 发起构造后的请求,并将响应写入cel变量列表
- 执行表达式
0x04 扩展xray
xray 以目录为单位进行扫描,足够覆盖绝大多数场景,但我认为还可以再细分下。
- 目录级的定义可以再完整些。以
testphp.vulnweb.com/aaa/bbb/ccc.html
为例,如果以目录为单位的话,应该扫描三个目录:/
、/aaa/
、bbb
。 - 某些通用漏洞的组件通常独占端口且部署在一级目录,为减少请求数,只扫描一级目录
/
。 - 每个请求的响应(包括原始请求和每个变形后的请求)都应该检查一遍响应,例如是否为常见 CMS /框架的报错、绝对路径泄露、IP泄露、数据库报错等等。
参数存在 SSRF
或参数型的存储型 XSS
等,是需要逐一对原始请求的参数进行处理:直接替换为 payload 或者在原始参数后面拼 payload 。- 以上扫描只针对 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 参考
- 本文作者: jweny
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/271
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!