长亭百川云 - 文章详情

如何自定义PoC V1 版本

CT Stack

51

2022-05-25

配置见图

POC 结构

一个最基础的 POC 如下:

1name: poc-yaml-example-com
2rules:
3  - method: GET
4    path: "/"
5    expression: |
6      response.status==200 && response.body.bcontains(b'Example Domain')
7
8detail:
9  author: name(link)
10  links: 
11    - http://example.com

整个 POC 是一个键值对,其包含3个键:

  • name: string
  • set: []string (0.13.0 版本新增)
  • rules: []Rule
  • detail: map[string]string

name 是 POC 的名字

set 是用来自定义变量,比如是随机数、反连平台等。

rules 是一个由规则(Rule)组成的列表,后面会描述如何编写 Rule,并将其组成 rules。

detail 是一个键值对,内部存储需要返回给 xray 引擎的内容,如果无需返回内容,可以忽略。

如果想要贡献 poc,请阅读 [贡献POC] 章节,里面对 poc 的编写有更多的约束。

Rule

Rule 就是我们 POC 的灵魂,在 YAML 中一个 Rule 是一个键值对,其包含如下键:

  • method: string 请求方法
  • path: string 请求的完整 Path,包括 querystring 等
  • headers: map[string]string 请求 HTTP 头,Rule 中指定的值会被覆盖到原始数据包的 HTTP 头中
  • body: string 请求的Body
  • follow_redirects: bool 是否允许跟随300跳转
  • expression: string
  • search: string

根据这些键的作用,我们将其分为三类:

  1. methodpathheadersbodyfollow_redirects的作用是生成检测漏洞的数据包
  2. expression 的作用是判断该条 Rule 的结果
  3. search 的作用是从返回包中提取信息

对于第一部分的内容,用来将原始的扫描请求进行变形, 比如原请求是 GET,但这里制定了 POST, 那么发送的时候就会使用 POST,其他项类似,不在赘述,我们从第二部分开始介绍。

整体执行流程可以参照上述的生命周期。

如何编写expression表达式

如果说Rule是一个POC的灵魂,那么expression表达式就是Rule的灵魂。

正如spring使用SpEL表达式,struts2使用OGNL表达式,xray使用了编译性语言Golang,所以为了实现动态执行一些规则,我们使用了Common Expression Language (CEL)表达式。

关于CEL表达式项目的详细信息,可以参考https://github.com/google/cel-spec项目。如果你只是编写一些简单的规则,只需要阅读本文档的即可。

我们从上述示例中的表达式开始说起:

response.status==200 && response.body.bcontains(b'Example Domain')

CEL表达式通熟易懂,非常类似于一个Python表达式。上述表达式的意思是:返回包status等于200,且body中包含内容“Example Domain”

xray 通过类型注入技术自定义了4种数据类型,包括

  • request 原始扫描请求
  • response 当前 rule 的响应
  • reverse 反连平台类型
  • url url 类型,可以用过 request.urlresponse.urlreverse.url 调用

其中可以在 rule 的 expression 使用的类型有: requestresponse 和自定义的变量。

关于这些类型的详细属性,参照后续的清单。

expression表达式上下文还包含有一些常用的函数。比如上述 bcontains 用来匹配 bytes 是否包含,类似的,如果要匹配 string 的包含,可以使用 contains, 如:

response.content_type.contains("json")

xray 所有CEL文档中的函数,同时还包含xray引擎中自定义的函数,函数清单请参照下方清单部分。

值得注意的是,类似于python,CEL中的字符串可以有转义和前缀,如:

  • '\r\n' 表示换行
  • r'\r\n' 不表示换行,仅仅表示这4个字符。在编写正则时很有意义。
  • b'test' 一个字节流(bytes),在golang中即为[]byte

用一些简单的例子来解释大部分我们可能用到的表达式:

  • response.body.bcontains(b'test')
    • 返回包 body 包含 test,因为 body 是一个 bytes 类型的变量,所以我们需要使用 bcontains 方法,且其参数也是 bytes
  • response.body.bcontains(bytes(r1+'some value'+r2))
    • r1、r2是 randomLowercase 的变量,这里动态的判断 body 的内容
  • response.content_type.contains('application/octet-stream') && response.body.bcontains(b'\x00\x01\x02')
    • 返回包的 content-type 包含 application/octet-stream,且 body 中包含 0x000102 这段二进制串
  • response.content_type.contains('zip') && r'^PK\x03\x04'.bmatches(response.body)
    • 这个规则用来判断返回的内容是否是zip文件,需要同时满足条件:content-type 包含关键字 "zip",且 body 匹配上正则r'^PK\x03\x04'(就是zip的文件头)。因为 startsWith 方法只支持字符串的判断,所以这里没有使用。
  • response.status >= 300 && response.status < 400
    • 返回包的 status code 在 300~400 之间
  • (response.status >= 500 && response.status != 502) || r'<input value="(.+?)"'.bmatches(response.body)
    • 返回包status code大于等于500且不等于502,或者Body包含表单
  • response.headers['location']=="https://www.example.com"
    • headers 中 Location 等于指定值,如果 Location 不存在,该表达式返回 false
  • 'docker-distribution-api-version' in response.headers && response.headers['docker-distribution-api-version'].contains('registry/2.0')
    • headers 中包含 docker-distribution-api-version 并且 value 包含指定字符串,如果不判断 in,后续的 contains 会出错。
  • response.body.bcontains(bytes(response.url.path))
    • body 中包含 url 的 path

expression表达式返回的必须是一个bool类型的结果,这个结果作为整个Rule的值,而rules由多个Rule组成。值为true的Rule,如果后面还有其他Rule,则继续执行后续Rule,如果后续没有其他Rule,则表示该POC的结果是true;如果一个Rule的expression返回false,则不再执行后续Rule,也表示本POC的返回结果是false。

也就是说,一个POC的rules中,最后一个Rule的值,决定是否存在漏洞。

search的作用

一个Rule中,可以支持使用search来查找返回包中的内容;当然,如果不需要查找内容,则可以忽略search。

search是一个字符串类型的正则表达式,我们用一个简单的案例来进行说明。

1name: poc-yaml-example-com
2rules:
3  - method: GET
4    path: "/update"
5    expression: "true"
6    search: |
7      <input type="hidden" name="csrftoken" value="(?P<token>.+?)"
8  - method: POST
9    path: "/update"
10    body: |
11      id=';echo(md5(123));//&csrftoken={{token}}
12    expression: |
13      status == 200 && body.bcontains(b'202cb962ac59075b964b07152d234b70')

目标漏洞是一个简单的代码执行,但因为是POST请求,所以需要先获取当前用户的CSRF Token。所以,我们的POC分为两个Rule,第一个Rule发送GET请求,并使用search指定的正则提取返回包中的csrftoken表单值,此时expression直接执行表达式true,表示第一条规则一定执行成功;第二个Rule发送POST请求,此时,我们可以在path、body、headers中使用前一个规则search的结果,如{{token}}等。

{{}}中包含的名字是正则的提取的数据。如果正则没有匹配成功,这里不会进行替换。?P<var> 是正则表达式命名组的语法,可以到 https://regex101.com/r/VQndKy/1/ 调试和学习正则的语法。

自定义变量

在编写 poc 时,有时会遇到需要随机值的情况,如果只是单纯的随机值比较简单,可以直接使用 randomLowercase 等函数生产随机值。但经常性的,我们后续还需要用到该随机值,这就是自定义变量的用途了。

xray 的自定义变量通过 yaml 中的 set 实现,一个相对复杂的 case 如下:

1name: poc-yaml-example
2set:
3  r1: randomInt(5, 10)
4  r2: randomLowercase(r1)
5  requestType: request.content_type
6rules:
7  - method: GET
8    path: "/?{{r2}}"
9    expression: |
10      requestType.contains("json") && response.status==200 && md5(r2).contains('1')

该 poc 最终发出的 path 类似 /lxbfah,意为先在 (5,10)内随机取一个整数 r1, 然后随机生成一个 r1 长度的小写随机值,在发送请求时将该值作为 path 发出。 表达式内验证原始请求的 content-type 是否包含 json 以及 md5 后的 r2 是否包含 1 这个字符等。

上面的范例包含了自定义变量的一些规律和约束:

  • set 的 value 是表达式,可以使用所有全局函数
  • set 有先后次序,后面的变量可以使用前面的再次处理
  • set 中可以使用 requestreverse(下面讲到) 变量
  • 在 rules 的 expression 中可以直接使用自定义变量,非 expression 的部分需要使用 {{}} 包裹变量,因为其他位置不是表达式(类似 search)

更多复杂用法大家可以自行发挥。

规则组

!> 该功能从 xray 1.6.0 开始支持

有一部分漏洞存在多种入口或是多种触发条件,如果想要将支持多种情况,从已知的情报来看只能写多个 yaml poc。但是从分类上来讲,这几个分离的 poc 实际都是为了检测同一个漏洞,因此诞生了 groups 规则组的概念。

最常见的一个应用是同一漏洞在 windows 和 linux 下不同的利用方式, 这时候就可以这么写:

1name: poc-yaml-groups-test
2groups:
3  win:
4    - method: GET
5      path: "/getfile=../../../../windows/win.ini"
6      expression: |
7        response.status == 200
8  linux:
9    - method: GET
10      path: "/getfile=../../../../etc/passwd"
11      expression: |
12        response.status == 200

groups 的定义是 map[string]rules,这里执行逻辑上相当于 win | linux, 即只要有一组规则执行成功,该漏洞就认为存在。

注意:groupsrules 应当只存在一个。

内部变量与函数速查

通过类型注入技术,我们实现了四种自定义的数据类型

其中 request 包含的字段如下:

变量名类型说明
request.urlurlType自定义类型 urlType, 请查看下方 urlType 的说明
request.methodstring原始请求的方法
request.headersmap[string]string原始请求的HTTP头,是一个键值对(均为小写),我们可以通过headers['server']来获取值。如果键不存在,则获取到的值是空字符串。注意,该空字符串不能用于 == 以外的操作,否则不存在的时候将报错,需要先 in 判断下。详情参考下文常用函数章节。
request.content_typestring原始请求的 content-type 头的值, 等于request.headers["Content-Type"]
request.body[]byte原始请求的 body,需要使用字节流相关方法来判断。如果是 GET, body 为空。

response 包含的字段如下:

变量名类型说明
response.urlurlType自定义类型 urlType, 请查看下方 urlType 的说明
response.statusint返回包的status code
response.body[]byte返回包的Body,因为是一个字节流(bytes)而非字符串,后面判断的时候需要使用字节流相关的方法
response.headersmap[string]string返回包的HTTP头,类似 request.headers
response.content_typestring返回包的content-type头的值
response.latencyint响应的延迟时间,可以用于 sql 时间盲注的判断,单位毫秒 (ms)

urlType 包含的字段如下, 以 http://example.com:8080/a?c=d#x=y 为例:

变量名类型说明
url.schemestringurl 的 scheme, 示例为 "http"
url.domainstringurl 的域名,示例例为 "example.com"
url.hoststringurl 的主机名,示例为 "example.com:8080"
url.portstringurl 的 port,注意这里也是字符串。 示例为 "8080"
url.pathstringurl 的 path, 示例为 "/a"
url.querystringurl 的 query, 示例为 "c=d"
url.fragmentstringurl 的锚点,示例为 "x=y"

reverse 包含字段如下。(需要先使用 newReverse() 生成实例,假设实例名为 reverse)

变量名/函数名类型说明
newReverse()func() reverseType返回一个 reverse 实例
reverse.urlstring反连平台的 url
reverse.domainstring反连平台的域名
reverse.ipstring反连平台的 ip 地址
reverse.is_domain_name_serverbool反连平台的 domain 是否同时是 nameserver
reverse.wait(timeout)func (timeout int) bool等待 timeout 秒,并返回是否在改时间内获得了信息

常用函数一览

函数名函数原型说明
containsfunc (s1 string) contains(s2 string) bool判断s1是否包含s2,返回bool类型结果
bcontainsfunc (b1 bytes) bcontains(b2 bytes) bool判断一个b1是否包含b2,返回bool类型结果。与contains不同的是,bcontains是字节流(bytes)的查找
matchesfunc (s1 string) matches(s2 string) bool使用正则表达式s1来匹配s2,返回bool类型匹配结果
bmatchesfunc (s1 string) bmatches(b1 bytes) bool使用正则表达式s1来匹配b1,返回bool类型匹配结果。与matches不同的是,bmatches匹配的是字节流(bytes)
startsWithfunc (s1 string) startsWith(s2 string) bool判断s1是否由s2开头
endsWithfunc (s1 string) endsWith(s2 string) bool判断s1是否由s2结尾
instring in mapmap 中是否包含某个 key,目前只有 headers 是 map 类型
md5func md5(string) string字符串的 md5 (以下都是 0.13.0 版本新增)
randomIntfunc randomInt(from, to int) int两个范围内的随机数
randomLowercasefunc randomLowercase(n length) string指定长度的小写字母组成的随机字符串
base64func base64(string/bytes) string将字符串或 bytes 进行 base64 编码
base64Decodefunc base64Decode(string/bytes) string将字符串或 bytes 进行 base64 解码
urlencodefunc urlencode(string/bytes) string将字符串或 bytes 进行 urlencode 编码
urldecodefunc urldecode(string/bytes) string将字符串或 bytes 进行 urldecode 解码
substrfunc substr(string, start, length) string截取字符串
sleepfunc sleep(int) bool暂停执行等待指定的秒数
相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2