一个最基础的 POC 如下:
1name: poc-yaml-example-com
2# 脚本部分
3transport: http
4rules:
5 r1:
6 request:
7 method: GET
8 path: "/"
9 expression: |
10 response.status==200 && response.body.bcontains(b'Example Domain')
11expression:
12 r1()
13# 信息部分
14detail:
15 author: name(link)
16 links:
17 - http://example.com
整个 POC 大致可以分为 3 部分:
接下来分别介绍一个各个部分的格式和内容。
这个部分包含 4 部分内容,分别为:
该字段用于指定发送数据包的协议。 transport: string
形如:
1transport: http
目前 transport 的取值可以为以下 3 种之一:
相比于 v1 版本,v2 版本做出的一个重要变更是允许发送多种多样的数据包了,不再仅局限于 http 请求,我们新增 tcp 和 udp 的支持。
目前不允许一个脚本中发送不同种 transport 的请求,因为通常我们的输入是一个稳定的协议, 比如:
如果后续有其它协议的需求,这个字段的取值是可以扩展的。
该字段用于定义全局变量。比如随机数,反连平台等。 set: map[string]interface{}
形如:
1set:
2 a: 1
该字段用于定义多个 payload,来实现发送不同 payload 的效果。 该字段结构如下
变量名/函数名 | 类型 | 说明 |
---|---|---|
continue | bool | 命中一个之后是否继续 |
payloads | map[string]Set | 和 set 一样的结构和语法 |
形如:
1payloads:
2 continue: false
3 payloads:
4 ping:
5 cmd: r"ping test.com"
6 curl:
7 cmd: r"curl test.com"
注:
payload
, 目前不考虑支持文件或者复杂排列组合等情况,每个 payload
中的 key
必须严格一致1set:
2 a: 1
3payloads:
4 - b: a
5 - b: b
实际运行结果相当于,运行两遍
第一遍:
1set:
2 a: 1
3 b: a
第二遍:
1set:
2 a: 1
3 b: b
该字段定义了一些具名的 rule。 rules: map[string]Rule
形如:
1rules:
2 r1:
3 # 此处为一个 http request 的例子
4 request:
5 method: GET
6 path: "/"
7 expression: |
8 response.status==200 && response.body.bcontains(b'Example Domain')
9 # 相比于 V1 版本新增
10 output:
11 search: |
12 r'(?P<version>1.1.1)'.bsubmatch(response.raw)'
13 version: search['version']
其中 rule 的结构又可以分为 3 部分:
该字段应该与前文说到的 transport
是对应的。对应于不同的 transport 有不同的请求参数设置。
结构如下:
1cache: true
2content: "request"
3read_timeout: "10"
4connection_id: xxx
cache: bool
是否使用缓存的请求,如果该选项为 true,那么如果在一次探测中其它脚本对相同目标发送过相同请求,那么便使用之前缓存的响应,而不发新的数据包content:string
请求内容read_timeout: string
发送请求之后的读取超时时间(注 实际是一个 int
, 但是为了能够变量渲染,设置为 string
)connection_id: string
连接 id ,同一个连接 id 复用连接(注 不允许用0; cache
为 true 的时候可能会导致请求不会发送,所以如果有特殊需求记得 cache: false
)结构如下:
1cache: true
2content: "request"
3read_timeout: "10"
4connection_id: xxx
cache: bool
是否使用缓存的请求,如果该选项为 true,那么如果在一次探测中其它脚本对相同目标发送过相同请求,那么便使用之前缓存的响应,而不发新的数据包content:string
请求内容read_timeout: string
发送请求之后的读取超时时间(注 实际是一个 int
, 但是为了能够变量渲染,设置为 string
)connection_id: string
连接 id ,同一个连接 id 复用连接(注 不允许用0; cache
为 true 的时候可能会导致请求不会发送,所以如果有特殊需求记得 cache: false
)结构如下:
1cache: true
2method: GET
3path: /
4headers:
5 Content-Type: application/xml
6body: aaaa
7follow_redirects: true
cache: bool
是否使用缓存的请求,如果该选项为 true,那么如果在一次探测中其它脚本对相同目标发送过相同请求,那么便使用之前缓存的响应,而不发新的数据包method: string
请求方法path: string
请求的完整 Path,包括 querystring 等 (详情见: HTTP PATH 的使用)
/
开头的, 取 dir 路径拼接^
开头的, uri 直接取该路径headers: map[string]string
请求 HTTP 头,Rule 中指定的值会被覆盖到原始数据包的 HTTP 头中body: string
请求的Bodyfollow_redirects: bool
是否允许跟随300跳转该字段定义了这条 rule 最终执行的一个结果. expression: string
形如:
1expression: |
2 response.status==200 && response.body.bcontains(b'Example Domain')
具体 expression 应该如何编写,请看 expression 编写
该字段定义了这条 rule 运行完成之后的一些变量,该字段定义的变量会被添加到全局变量中,类似于 set
。output: []string
形如:
1output:
2 search: |
3 r'(?P<version>1.1.1)'.bsubmatch(response.raw)'
4 version: search['version']
该字段定义了整个脚本最终执行的一个结果. expression: string
形如:
1expression: |
2 r1() || r2()
具体 expression 应该如何编写,请看 expression 编写, 尤其注意脚本 expression 编写部分
该字段用于定义一些和脚本相关的信息。内容都为非必填内容。
1detail:
2 author: name(link)
3 links:
4 - http://example.com
5 # 指纹信息
6 fingerprint:
7 infos:
8 - id: "长亭指纹库 id"
9 name: "SSH"
10 version: {{version}}
11 type: "system_bin"
12 confidence: 70
13 host_info:
14 hostname: "test"
15 # 漏洞信息
16 vulnerability:
17 id: "长亭漏洞库 id"
18 match: "证明漏洞存在的信息"
19 # 其它字段
20 cve: "CVE-2020-1234"
21 # 其它未明确定义的字段
22 summary: "test"
目前主要定义了一下几个部分:
注:其中支持变量渲染,形如 {{variable}}
, 其中变量为 set 或者 rule output 中定义的变量
author: string
作者links: []string
相关链接fingerprint
指纹信息
infos: []Info
指纹信息
id: string
长亭指纹库 IDname: string
名称version: string
版本号type: string
指纹类型,有以下可选值: operating_system
, hardware
, system_bin
, web_application
, dependency
confidence: int
取值范围(1-100)host_info
主机信息
hostname: string
主机名vulnerability
漏洞信息
id: string
长亭漏洞库 IDmatch: string
证明漏洞存在的一些信息正如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”。
expression表达式上下文还包含有一些常用的函数。比如上述 bcontains
用来匹配 bytes 是否包含,类似的,如果要匹配 string 的包含,可以使用 contains
, 如:
response.content_type.contains("json")
值得注意的是,类似于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 结构中的 expression, 这个结果作为整个Rule调用的值
对于脚本层级的 expression,这个结果作为最后脚本是否匹配成功的值,通常脚本层级的 expression 是 rule 结果的一个组合。 比如一个脚本包含 r1
, r2
, r3
,r4
4 条规则, 作为脚本层级的 expression,其全局变量将会定义 r1
, r2
, r3
, r4
4 个函数,调用这个 4 个函数即可获得它对应 rule 的结果。
1expression: r1() && r2() && r3() && r4()
注:
相比于 V1 版本, rule 如何运行这件事情发生了较大的改变。假设我们有 r1
, r2
, r3
,r4
4 条规则
最开始时, V1 版本添加了 rules: []Rule
来定义 rule 及其执行顺序。其逻辑为顺序执行 rule, 且 r1
, r2
, r3
,r4
都为 true 时, 脚本执行成功
1rules:
2 r1: ...
3 r2: ...
4 r3: ...
5 r4: ...
然后添加了 group: map[string][]Rule
拓展了一下 ruel 的执行方式。支持了 r1,r2 同时为 true 或者 r3, r4 同时为 true 时, 脚本执行成功
1group:
2 g1: [r1, r2]
3 g2: [r3, r4]
在 V2 版本下, 我们使用 expression 来组织 rule 的执行逻辑。
对于方式1,其对应的形式为:
1rules:
2 r1: ...
3 r2: ...
4 r3: ...
5 r4: ...
6expression: r1() && r2() && r3() && r4()
对于方式2, 其对应的形式为:
1rules:
2 r1: ...
3 r2: ...
4 r3: ...
5 r4: ...
6expression: (r1() && r2()) || (r3() && r4())
甚至我们还能支持:
1rules:
2 r1: ...
3 r2: ...
4 r3: ...
5 r4: ...
6expression: (r1() || r2() || r3()) && r4()
这里有几点需要说明一下:
短路求值
, 即 r1() || r2()
, 如果 r1()
的结果为 true 那么 r2 是不会执行的xray 支持所有CEL文档中的类型,同时还注入了几种特殊的类型,包含:
addrType
连接地址信息connInfoType
连接信息,包含源地址和目的地址, 可以通过 response.conn
urlType
url 类型,可以 request.url
、response.url
和 reverse.url
调用reverseType
反连平台类型request
扫描请求response
请求的响应,通用属性包含:raw
其中注入的 request 和 response 类型是随着 transport
对应的值进行改变的。
addrType 类型包含字段如下, 设变量名为 addr
变量名 | 类型 | 说明 | 适用版本 |
---|---|---|---|
addr.transport | string | tranport | Xray ≥ 1.8.4 |
addr.addr | string | 目的地址, 获取失败时返回空字符串,形如: "192.0.2.1:25", "[2001:2001::1]:80" | Xray ≥ 1.8.4 |
addr.port | string | 端口号, 获取失败时返回 "" | Xray ≥ 1.8.4 |
connInfoType 类型包含字段如下, 设变量名为 conn
变量名 | 类型 | 说明 | 适用版本 |
---|---|---|---|
conn.source | addrType | 源地址信息 | Xray ≥ 1.8.4 |
conn.destination | addrType | 目的地址信息 | Xray ≥ 1.8.4 |
urlType 类型包含的字段如下, 设变量名为 url
, 以 http://example.com:8080/a?c=d#x=y
为例:
变量名 | 类型 | 说明 | 适用版本 |
---|---|---|---|
url.scheme | string | url 的 scheme, 示例为 "http" | Xray ≥ 1.8.4 |
url.domain | string | url 的域名,示例例为 "example.com" | Xray ≥ 1.8.4 |
url.host | string | url 的主机名,示例为 "example.com:8080" | Xray ≥ 1.8.4 |
url.port | string | url 的 port,注意这里也是字符串。 示例为 "8080" | Xray ≥ 1.8.4 |
url.path | string | url 的 path, 示例为 "/a" | Xray ≥ 1.8.4 |
url.query | string | url 的 query, 示例为 "c=d" | Xray ≥ 1.8.4 |
url.fragment | string | url 的锚点,示例为 "x=y" | Xray ≥ 1.8.4 |
reverseType 类型包含字段如下, 设变量名为 reverse
(需要先使用 newReverse()
生成实例)
变量名 | 类型 | 说明 | 适用版本 |
---|---|---|---|
reverse.url | urlType | 反连平台的 url | Xray ≥ 1.8.4 |
reverse.domain | string | 反连平台的域名 | Xray ≥ 1.8.4 |
reverse.ip | string | 反连平台的 ip 地址 | Xray ≥ 1.8.4 |
reverse.is_domain_name_server | bool | 反连平台的 domain 是否同时是 nameserver | Xray ≥ 1.8.4 |
reverse.wait(timeout) | func (timeout int) bool | 等待 timeout 秒,并返回是否在改时间内获得了信息 | Xray ≥ 1.8.4 |
其中 request 包含的字段如下:
变量名 | 类型 | 说明 | 适用版本 |
---|---|---|---|
request.raw | []byte | 原始请求 | Xray ≥ 1.8.4 |
response 包含的字段如下:
变量名 | 类型 | 说明 | 适用版本 |
---|---|---|---|
response.conn | connInfoType | 连接相关信息 | Xray ≥ 1.8.4 |
response.raw | []byte | 原始响应 | Xray ≥ 1.8.4 |
其中 request 包含的字段如下:
变量名 | 类型 | 说明 | 适用版本 |
---|---|---|---|
request.raw | []byte | 原始请求 | Xray ≥ 1.8.4 |
response 包含的字段如下:
变量名 | 类型 | 说明 | 适用版本 |
---|---|---|---|
response.conn | connInfoType | 连接相关信息 | Xray ≥ 1.8.4 |
response.raw | []byte | 原始响应 | Xray ≥ 1.8.4 |
其中 request 包含的字段如下:
变量名 | 类型 | 说明 | 适用版本 |
---|---|---|---|
request.raw | []byte | 原始请求 | Xray ≥ 1.8.4 |
request.url | urlType | 自定义类型 urlType, 请查看下方 urlType 的说明 | Xray ≥ 1.8.4 |
request.method | string | 原始请求的方法 | Xray ≥ 1.8.4 |
request.headers | map[string]string | 原始请求的HTTP头,是一个键值对(均为小写),我们可以通过headers['server'] 来获取值。如果键不存在,则获取到的值是空字符串。注意,该空字符串不能用于 == 以外的操作,否则不存在的时候将报错,需要先 in 判断下。详情参考下文常用函数章节。 | Xray ≥ 1.8.4 |
request.content_type | string | 原始请求的 content-type 头的值, 等于request.headers["Content-Type"] | Xray ≥ 1.8.4 |
request.raw_header | []byte | 原始的 header 部分,需要使用字节流相关方法来判断。 | Xray ≥ 1.8.4 |
request.body | []byte | 原始请求的 body,需要使用字节流相关方法来判断。如果是 GET, body 为空。 | Xray ≥ 1.8.4 |
response 包含的字段如下:
变量名 | 类型 | 说明 | 适用版本 |
---|---|---|---|
response.raw | []byte | 原始响应 | Xray ≥ 1.8.4 |
response.url | urlType | 自定义类型 urlType, 请查看下方 urlType 的说明 | Xray ≥ 1.8.4 |
response.status | int | 返回包的status code | Xray ≥ 1.8.4 |
response.raw_header | []byte | 原始的 header 部分,需要使用字节流相关方法来判断。 | Xray ≥ 1.8.4 |
response.body | []byte | 返回包的Body,因为是一个字节流(bytes)而非字符串,后面判断的时候需要使用字节流相关的方法 | Xray ≥ 1.8.4 |
response.headers | map[string]string | 返回包的HTTP头,类似 request.headers 。 | Xray ≥ 1.8.4 |
response.content_type | string | 返回包的content-type头的值 | Xray ≥ 1.8.4 |
response.latency | int | 响应的延迟时间,可以用于 sql 时间盲注的判断,单位毫秒 (ms) | Xray ≥ 1.8.4 |
xray 支持所有CEL文档中的函数,同时还新增了一些函数支持,下面列举一下常用的函数:
注:gocel 原生的正则支持使用的 golang 自带的 regex 库, 在脚本中我们使用 regex2 替换了自带的 regex 库
注:只有脚本层级的 expression 才能使用这些函数
函数名 | 函数原型 | 说明 | 适用版本 |
---|---|---|---|
rule name | func <rule name>() bool | 返回这条 rule expression 执行的结果 | Xray ≥ 1.8.4 |
函数名 | 函数原型 | 说明 | 适用版本 |
---|---|---|---|
contains | func (s1 string) contains(s2 string) bool | 判断s1是否包含s2,返回bool类型结果 | Xray ≥ 1.8.4 |
icontains | func (s1 string) icontains(s2 string) bool | 判断s1是否包含s2,返回bool类型结果, 与contains不同的是,icontains 忽略大小写 | Xray ≥ 1.8.4 |
substr | func substr(string, start int, length int) string | 截取字符串 | Xray ≥ 1.8.4 |
replaceAll | func replaceAll(string, old string, new string) string | 将 string 中的 old 替换为 new,返回替换后的 string | Xray ≥ 1.8.4 |
printable | func printable(string) string | 将 string 中的非 unicode 编码字符去掉 | Xray ≥ 1.8.4 |
toUintString | func toUintString(s1 string, direction string) string | direction 取值为 > ,< 表示读取方向, 将 s1 按 direction 读取为一个整数,返回该整数的字符串形式 | Xray ≥ 1.8.4 |
startsWith | func (s1 string) startsWith(s2 string) bool | 判断s1是否由s2开头 | Xray ≥ 1.8.4 |
endsWith | func (s1 string) endsWith(s2 string) bool | 判断s1是否由s2结尾 | Xray ≥ 1.8.4 |
函数名 | 函数原型 | 说明 | 适用版本 |
---|---|---|---|
bcontains | func (b1 bytes) bcontains(b2 bytes) bool | 判断一个b1是否包含b2,返回bool类型结果。与contains不同的是,bcontains是字节流(bytes)的查找 | Xray ≥ 1.8.4 |
ibcontains | func (b1 bytes) ibcontains(b2 bytes) bool | 判断b1是否包含b2,返回bool类型结果, 与contains不同的是,ibcontains 是字节流(bytes)的查找, 且忽略大小写 | Xray ≥ 1.8.4 |
bstartsWith | func (b1 bytes) bstartsWith(b2 bytes) bool | 判断一个b1是否由b2开头,返回bool类型结果。与startsWith不同的是,bcontains是字节流(bytes)的查找 | Xray ≥ 1.8.4 |
bformat | func bformat(bytes, int, int, string, int)string | 将bytes进行进制转换编码,可以根据输入的参数转换成对应的进制编码,并可以自定义转换格式 | Xray ≥ 1.8.5 |
函数介绍:
\x
等;示例1:bformat(b'asdfghj',16,0,'',0)
输出1:6173646667686a
示例2:bformat(b'asdfghj',16,4,'',0)
输出2:006100730064006600670068006a
示例3:bformat(b'asdfghj',16,0,'\x',1)
输出3:\x61\x73\x64\x66\x67\x68\x6a
函数名 | 函数原型 | 说明 | 适用版本 |
---|---|---|---|
md5 | func md5(string) string | 字符串的 md5 | Xray ≥ 1.8.4 |
base64 | func base64(string/bytes) string | 将字符串或 bytes 进行 base64 编码 | Xray ≥ 1.8.4 |
base64Decode | func base64Decode(string/bytes) string | 将字符串或 bytes 进行 base64 解码 | Xray ≥ 1.8.4 |
urlencode | func urlencode(string/bytes) string | 将字符串或 bytes 进行 urlencode 编码 | Xray ≥ 1.8.4 |
urldecode | func urldecode(string/bytes) string | 将字符串或 bytes 进行 urldecode 解码 | Xray ≥ 1.8.4 |
faviconHash | func faviconHash(string/bytes) int | 将字符串或 bytes 进行 faviconHash 编码,参考:iconhash | Xray ≥ 1.8.4 |
sha | func sha(string/bytes, string)string | 该函数可以将指定字符串或bytes进行sha系列计算,第二个参数控制加密类型。例:sha('asd','sha1')。目前支持'sha1'、'sha224'、'sha256'、'sha384'、'sha512' | Xray ≥ 1.8.4 |
hmacSha | func hmacSha(string/bytes, string/bytes, string)string | 该函数可以将指定字符串或bytes进行hmac_sha系列计算,第一个参数为待加密明文,第二个参数为密钥,第三个参数控制加密类型。例:sha('asd','123','sha1')。目前支持'sha1'、'sha224'、'sha256'、'sha384'、'sha512' | Xray ≥ 1.8.4 |
函数名 | 函数原型 | 说明 | 适用版本 |
---|---|---|---|
randomInt | func randomInt(from, to int) int | 两个范围内的随机数 | Xray ≥ 1.8.4 |
randomLowercase | func randomLowercase(n length) string | 指定长度的小写字母组成的随机字符串 | Xray ≥ 1.8.4 |
函数名 | 函数原型 | 说明 | 适用版本 |
---|---|---|---|
matches | func (s1 string) matches(s2 string) bool | 使用正则表达式s1来匹配s2,返回bool类型匹配结果 | Xray ≥ 1.8.4 |
submatch | func (s1 string) matches(s2 string) map[string]string | 使用正则表达式s1来匹配s2,返回 map[string]string 类型结果,注:只返回具名的正则匹配结果 (?P…) 格式 | Xray ≥ 1.8.4 |
bmatches | func (s1 string) bmatches(b1 bytes) bool | 使用正则表达式s1来匹配b1,返回bool类型匹配结果。与matches不同的是,bmatches匹配的是字节流(bytes) | Xray ≥ 1.8.4 |
bsubmatch | func (s1 string) bmatches(b1 bytes) map[string]string | 使用正则表达式s1来匹配b1,返回 map[string]string 类型结果 注:只返回具名的正则匹配结果 (?P…) 格式。与matches不同的是,bmatches匹配的是字节流(bytes) | Xray ≥ 1.8.4 |
函数名 | 函数原型 | 说明 | 适用版本 |
---|---|---|---|
newReverse | func newReverse() reverseType | 返回一个 reverse 实例 | Xray ≥ 1.8.4 |
wait | func (reverse reverseType) wait(timeout int) bool | 等待 timeout 秒,并返回是否在改时间内获得了信息 | Xray ≥ 1.8.4 |
函数名 | 函数原型 | 说明 | 适用版本 |
---|---|---|---|
now | func now() Time | 使用该函数将返回当前时间,使用int(now()),将会获取当前时间的时间戳 | Xray ≥ 1.8.5 |
sleep | func sleep(int) bool | 暂停执行等待指定的秒数 | Xray ≥ 1.8.4 |
timeConvert | func timeConvert(int, string) string | 该函数可以将指定时间戳转换成自定义格式的字符串 | Xray ≥ 1.8.5 |
timeConvert本质上是使用golang的time包提供的Format方法,第一个参数传入时间戳,第二个参数传入想要函数输出的时间格式
以下是golang中time包对于时间的详细定义
代表 | 写法 |
---|---|
⽉份 | 1,01,Jan,January |
⽇ | 2,02,_2 |
时 | 3,03,15,PM,pm,AM,am |
分 | 4,04 |
秒 | 5,05 |
年 | 06,2006 |
时区 | -07,-0700,Z0700,Z07:00,-07:00,MST |
周⼏ | Mon,Monday |
⽐如⼩时的表⽰(原定义是下午3时,也就是15时)
3 ⽤12⼩时制表⽰,去掉前导0
03 ⽤12⼩时制表⽰,保留前导0
15 ⽤24⼩时制表⽰,保留前导0
03pm ⽤24⼩时制am/pm表⽰上下午表⽰,保留前导0
3pm ⽤24⼩时制am/pm表⽰上下午表⽰,去掉前导0
⼜⽐如⽉份
1 数字表⽰⽉份,去掉前导0
01 数字表⽰⽉份,保留前导0
Jan 缩写单词表⽰⽉份
January 全单词表⽰⽉份
示例:timeConvert(1560391089, '2006_01_02/3/4/05')
输出:2019_06_13/9/58/09
函数名 | 函数原型 | 说明 | 适用版本 |
---|---|---|---|
in | string in map | map 中是否包含某个 key,目前只有 headers 是 map 类型 | Xray ≥ 1.8.4 |