CT Stack
30
2022-05-25
配置见图
一个最基础的 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 就是我们 POC 的灵魂,在 YAML 中一个 Rule 是一个键值对,其包含如下键:
method: string
请求方法path: string
请求的完整 Path,包括 querystring 等headers: map[string]string
请求 HTTP 头,Rule 中指定的值会被覆盖到原始数据包的 HTTP 头中body: string
请求的Bodyfollow_redirects: bool
是否允许跟随300跳转expression: string
search: string
根据这些键的作用,我们将其分为三类:
method
、path
、headers
、body
、follow_redirects
的作用是生成检测漏洞的数据包expression
的作用是判断该条 Rule 的结果search
的作用是从返回包中提取信息对于第一部分的内容,用来将原始的扫描请求进行变形, 比如原请求是 GET
,但这里制定了 POST
, 那么发送的时候就会使用 POST
,其他项类似,不在赘述,我们从第二部分开始介绍。
整体执行流程可以参照上述的生命周期。
如果说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.url
、response.url
和 reverse.url
调用其中可以在 rule 的 expression 使用的类型有: request
、response
和自定义的变量。
关于这些类型的详细属性,参照后续的清单。
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')
response.body.bcontains(bytes(r1+'some value'+r2))
response.content_type.contains('application/octet-stream') && response.body.bcontains(b'\x00\x01\x02')
response.content_type.contains('zip') && r'^PK\x03\x04'.bmatches(response.body)
response.status >= 300 && response.status < 400
(response.status >= 500 && response.status != 502) || r'<input value="(.+?)"'.bmatches(response.body)
response.headers['location']=="https://www.example.com"
Location
等于指定值,如果 Location
不存在,该表达式返回 false'docker-distribution-api-version' in response.headers && response.headers['docker-distribution-api-version'].contains('registry/2.0')
docker-distribution-api-version
并且 value 包含指定字符串,如果不判断 in
,后续的 contains 会出错。response.body.bcontains(bytes(response.url.path))
expression表达式返回的必须是一个bool类型的结果,这个结果作为整个Rule的值,而rules由多个Rule组成。值为true的Rule,如果后面还有其他Rule,则继续执行后续Rule,如果后续没有其他Rule,则表示该POC的结果是true;如果一个Rule的expression返回false,则不再执行后续Rule,也表示本POC的返回结果是false。
也就是说,一个POC的rules中,最后一个Rule的值,决定是否存在漏洞。
一个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 这个字符等。
上面的范例包含了自定义变量的一些规律和约束:
request
和 reverse
(下面讲到) 变量{{}}
包裹变量,因为其他位置不是表达式(类似 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
, 即只要有一组规则执行成功,该漏洞就认为存在。
注意:groups
与 rules
应当只存在一个。
通过类型注入技术,我们实现了四种自定义的数据类型
其中 request 包含的字段如下:
变量名 | 类型 | 说明 |
---|---|---|
request.url | urlType | 自定义类型 urlType, 请查看下方 urlType 的说明 |
request.method | string | 原始请求的方法 |
request.headers | map[string]string | 原始请求的HTTP头,是一个键值对(均为小写),我们可以通过headers['server'] 来获取值。如果键不存在,则获取到的值是空字符串。注意,该空字符串不能用于 == 以外的操作,否则不存在的时候将报错,需要先 in 判断下。详情参考下文常用函数章节。 |
request.content_type | string | 原始请求的 content-type 头的值, 等于request.headers["Content-Type"] |
request.body | []byte | 原始请求的 body,需要使用字节流相关方法来判断。如果是 GET, body 为空。 |
response 包含的字段如下:
变量名 | 类型 | 说明 |
---|---|---|
response.url | urlType | 自定义类型 urlType, 请查看下方 urlType 的说明 |
response.status | int | 返回包的status code |
response.body | []byte | 返回包的Body,因为是一个字节流(bytes)而非字符串,后面判断的时候需要使用字节流相关的方法 |
response.headers | map[string]string | 返回包的HTTP头,类似 request.headers 。 |
response.content_type | string | 返回包的content-type头的值 |
response.latency | int | 响应的延迟时间,可以用于 sql 时间盲注的判断,单位毫秒 (ms) |
urlType 包含的字段如下, 以 http://example.com:8080/a?c=d#x=y
为例:
变量名 | 类型 | 说明 |
---|---|---|
url.scheme | string | url 的 scheme, 示例为 "http" |
url.domain | string | url 的域名,示例例为 "example.com" |
url.host | string | url 的主机名,示例为 "example.com:8080" |
url.port | string | url 的 port,注意这里也是字符串。 示例为 "8080" |
url.path | string | url 的 path, 示例为 "/a" |
url.query | string | url 的 query, 示例为 "c=d" |
url.fragment | string | url 的锚点,示例为 "x=y" |
reverse 包含字段如下。(需要先使用 newReverse() 生成实例,假设实例名为 reverse
)
变量名/函数名 | 类型 | 说明 |
---|---|---|
newReverse() | func() reverseType | 返回一个 reverse 实例 |
reverse.url | string | 反连平台的 url |
reverse.domain | string | 反连平台的域名 |
reverse.ip | string | 反连平台的 ip 地址 |
reverse.is_domain_name_server | bool | 反连平台的 domain 是否同时是 nameserver |
reverse.wait(timeout) | func (timeout int) bool | 等待 timeout 秒,并返回是否在改时间内获得了信息 |
常用函数一览
函数名 | 函数原型 | 说明 |
---|---|---|
contains | func (s1 string) contains(s2 string) bool | 判断s1是否包含s2,返回bool类型结果 |
bcontains | func (b1 bytes) bcontains(b2 bytes) bool | 判断一个b1是否包含b2,返回bool类型结果。与contains不同的是,bcontains是字节流(bytes)的查找 |
matches | func (s1 string) matches(s2 string) bool | 使用正则表达式s1来匹配s2,返回bool类型匹配结果 |
bmatches | func (s1 string) bmatches(b1 bytes) bool | 使用正则表达式s1来匹配b1,返回bool类型匹配结果。与matches不同的是,bmatches匹配的是字节流(bytes) |
startsWith | func (s1 string) startsWith(s2 string) bool | 判断s1是否由s2开头 |
endsWith | func (s1 string) endsWith(s2 string) bool | 判断s1是否由s2结尾 |
in | string in map | map 中是否包含某个 key,目前只有 headers 是 map 类型 |
md5 | func md5(string) string | 字符串的 md5 (以下都是 0.13.0 版本新增) |
randomInt | func randomInt(from, to int) int | 两个范围内的随机数 |
randomLowercase | func randomLowercase(n length) string | 指定长度的小写字母组成的随机字符串 |
base64 | func base64(string/bytes) string | 将字符串或 bytes 进行 base64 编码 |
base64Decode | func base64Decode(string/bytes) string | 将字符串或 bytes 进行 base64 解码 |
urlencode | func urlencode(string/bytes) string | 将字符串或 bytes 进行 urlencode 编码 |
urldecode | func urldecode(string/bytes) string | 将字符串或 bytes 进行 urldecode 解码 |
substr | func substr(string, start, length) string | 截取字符串 |
sleep | func sleep(int) bool | 暂停执行等待指定的秒数 |