跳到主要内容

漏洞分析:XSS 启发式检测基础设施

· 阅读需 10 分钟
Yak Project
网络安全垂直语言团队

背景:

XSS 漏洞检测是所有漏洞检测算法很难绕开的一个难点,传统的漏洞检测算法采用的方案其实很容易想到:

1.“构造很多 Payload”,把响应放到浏览器中,检测浏览器中的 “alert / prompt 是否被调用”

2.喷洒各种各样的 Payload,在 XSS 平台上看 “是否有目标上线”。

但是实际上,如果仅仅为了检测漏洞,或者提示 “潜在 XSS 风险”,一些发送大量数据包的破坏性的检测手段并不是一个好的办法。

在 Yaklang 中,我们要解决这个问题,并不应该直接提供给大家一个 “XSS” 检测算法,而是提供可以辅助编写算法的 “基础设施”。

有没有一种可能,全自动扫描漏洞其实并不重要,能提出高级的风险点,用户可以自己去挖掘一些非常 “深入” 的 XSS 更合适?

为此我们实现了两个很有意思的内置库以及函数:

1.yak.xhtml 提供HTML 相关的解析与定位辅助函数加速 XSS 漏洞检测

2.yak.js.ASTWalk 提供针对 javascript 的词法解析与 AST 遍历服务,把遍历到的字面量 / 符号 / 错误信息作为结果返回。

核心基础设施函数

yak.xhtml

yak.js.ASTWalk

*javascript.ASTWalkerResult 描述

这个结构是 ASTWalk 的结果,会把解析的 JS 的内容遍历 AST,汇总出 JS 代码整体的一些资料,用来判断用户构造的 Payload 在其中生效与否。

type palm/common/javascript.(ASTWalkerResult) struct {
Fields(可用字段):
// 所有字符串字面量:所有的字符串字面量和字符串定义都会在这里被找到并总结
StringLiteral: []string
// 整数字面量
Int64Literal: []int64
// 浮点数字面量
Float64Literal: []float64

// ID: 被认为是符号的内容(被当作调用对象,或者 field)
Identifies: []string
// 出现语法错误的位置
BadSyntax: []string
}

如何编写启发式检测 XSS?

0. 思路总集:Mind in a Graph

其中几个关键技术可以有效减少无效payload数量

1.通过参数回显位置生成相应payload:例如在div标签内、在script标签内、在标签属性中等等,不同属性对应生成 Payload 方法也不一样,例如 href 与 onerror / onclick 等。

2.根据位置探测过滤内容:把所有payload中的可疑字符拼接发送,根据响应包分析字符被过滤、转义、被转义为了什么,再进一步对受影响的payload进行变形或舍弃。

下面在分别对流程图中每一步进行分析介绍。

1.参数检测

参数位置

在页面回显的参数可能出现在:

1.post请求(form、json或xml)

2.get请求

3.Cookie

4.url path(例如form的action使用当前页面的url)

yak的fuzz库提供了方法可以轻松获取上述所有参数。

req = "<raw request>"
freq, err = fuzz.HTTPRequest(req)
if err!= nil{
println("new HTTPRequest error: %v",err)
return
}
params = freq.GetCommonParams() // 包含post json、post form、get参数、cookie参数(会自动过滤PHPSESSID、_ga、_gid等参数)
for _, param = range params {
printf("key: %s, value: %s, postion: %s\n", param.Name(),param.Value(),param.PositionVerbose())
}

多参数如何测试

同时对多个参数做测试可靠度低,没必要。只分别针对每个参数做测试(需要过滤一些没必要的参数,如form请求的submit或cookie参数中的PHPSESSID、JSESSIONID等)。

2.检测回显位置

通过生成一个随机的,不会被过滤的安全字符串测试请求,检测字符串的回显位置。

回显位置无非是:

1.标签内文本

2.属性

3.注释

4.script标签内(也属于标签内文本)

str = str.RandStr(5) // RandStr生成的随机字符集是大小写字母
resp, err = param.Fuzz(randStr).Exec() // param 是freq.GetCommonParams()获取的参数对象,可以直接对某个参数fuzz
if err != nil {
println("Fuzz param %s error: %v",param.Name(), err)
return
}
rspo = <-resp
body, err = str.ExtractBodyFromHTTPResponseRaw(rspo.ResponseRaw)
if err != nil {
println("Get response body error: %v", err)
return
}
matchNodes = xhtml.FindNodeFromHtml(body, randStr)

xhtml.``FindNodeFromHtml 方法可以从 body 中查找回显出现的位置信息。

3.生成 Paylaod

接着上一节,通过 xhtml.``FindNodeFromHtml 查找出回显位置,生成相应 Payload,如:

1.标签内文本:构造标签闭合

2.属性:构造标签闭合或伪协议

3.注释:构造标签闭合

4.script 标签内:构造标签闭合或dom型

for _, matchNode = range matchNodes {
printf("Echo xpath postion: %s", matchNode.Xpath)
if matchNode.IsText() {
if matchNode.TagName == "script" {
//例:<script>a = '<参数>';</script>
payload = sprintf("';alert('Hello');'", matchNode.TagName)
payloads = append(payloads, payload)
} else {
//例:<div><参数></div>
payload = sprintf("</%s>Hello<%s>", matchNode.TagName)
payloads = append(payloads, payload)
}
} else if matchNode.IsAttr() {
//例:<div id="<参数>"></div>
payload = sprintf("\"></%s>Hello<%s %s=\"%s", matchNode.TagName, matchNode.TagName, matchNode.Key, matchNode.Value)
payloads = append(payloads, payload)
} else if matchNode.IsCOMMENT() {
//例:<!-- <参数> -->
payload = sprintf("-->Hello<!--")
payloads = append(payloads, payload)
}
}

4.如何检测过滤字符?

根据回显的位置,生成相应payload,提取payload中存在可能被过滤的字符

生成一个随机字符串,用随机字符串作为分隔符,拼接这些可能被过滤的字符串

例:随机字符串agsAKdhjkTUI,可疑字符:<,',",/

生成:

agsAKdhjkTUI<agsAKdhjkTUI'agsAKdhjkTUI"agsAKdhjkTUI/agsAKdhjkTUI

对比响应包,可以找出哪些字符被过滤,或被转义为什么字符,

再根据过滤情况,对payload做一遍过滤,可以有效减少无效payload数量。

提取出payload中所有的可疑字符:

dangerousChars = ["<", ">", "/", "\\", "'", "\""]
detectChars = []
for _, payload = range payloads {
for _, dangerousChar = range dangerousChars {
if utils.MatchAllOfGlob(payload, sprintf("*%s*", dangerousChar)) {
detectChars = append(detectChars, dangerousChar)
}
}
}

检测被过滤的字符:

detectStr = randStr + strings.Join(detectChars, randStr) + randStr
resp, err = param.Fuzz(detectStr).Exec()
if err != nil {
println("Fuzz param %s error: %v", param.Name(), err)
return
}
rspo = <-resp
body, err = str.ExtractBodyFromHTTPResponseRaw(rspo.ResponseRaw)
if err != nil {
println("Get response body error: %v", err)
return
}
randStrFromIndex = body
passChars = []
i = 0
for {
n, btChar = xhtml.MatchBetween(randStrFromIndex, randStr, randStr, 50)
if n == -1 {
break
}
if i >= len(dangerousChars) {
break
}
if dangerousChars[i] == btChar {
passChars = append(passChars, btChar)
} else {
printf("Found characters to be filtered: %s->%s", dangerousChars[i], btChar)
}
randStrFromIndex = randStrFromIndex[n+len(randStr):]
i += 1
}

再通过筛选出不含有过滤字符的payload

newPayloads = []
for _, payload = range payloads {
for _, filterChar = range passChars {
if str.MatchAllOfGlob(payload, sprintf("*%s*", filterChar)) {
newPayloads = append(newPayloads, payload)
}
}
}

5.判断成功

根据payload类型判断,大概有以下四种类型:

1.标签内文本:构造标签闭合

2.属性:构造标签闭合或伪协议

3.注释:构造标签闭合

4.script标签内:构造标签闭合或dom型

对于构造标签闭合的payload,可以通过对比dom树的方式检测。

手工判断

可以在payload中加入随机字符串做定位,再通过xhtml.Find查询回显位置,对比原请求的回显位置,针对每种payload对html的影响做相应检测。

例如:

// <div><参数></div>
payload = "<script>asgdhFFASDljl</script>"
resp,err = param.Fuzz(sprintf("</div>%s<div>",payload)).Exec()
if err != nil {
println("Fuzz param %s error: %v", param.Name(), err)
return
}
rspo = <-resp
body, err = str.ExtractBodyFromHTTPResponseRaw(rspo.ResponseRaw)
if err != nil {
println("Get response body error: %v", err)
return
}
matchNodes = FindNodeFromHtml(body, "asgdhFFASDljl")
for _, matchNode = range matchNodes {
if matchNode.MatchText == "" && matchNode.TagName == "script"{
println("Found xss, payload: %s",payload)
}
}

或者在属性中

// <div id="参数"></div>
resp,err = param.Fuzz(sprintf("\"></div>Hello<div id=\"asgdhFFASDljl")).Exec()
if err != nil {
println("Fuzz param %s error: %v", param.Name(), err)
return
}
rspo = <-resp
body, err = str.ExtractBodyFromHTTPResponseRaw(rspo.ResponseRaw)
if err != nil {
println("Get response body error: %v", err)
return
}
matchNodes = FindNodeFromHtml(body, "asgdhFFASDljl")
for _, matchNode = range matchNodes {
if matchNode.IsAttr() && matchNode.Key == "id"{
println("Found xss")
}
}

DOM树判断

深度遍历整棵树,比较标签名、属性key和value、文本、注释

检测标志:

标签名出现不同(可能有些页面广告是后端渲染的,每次请求生成的广告数量不同会影响检测结果)、属性key和数量(两次相同请求可能会有key不同的情况,例如是样式属性,也会影响检测结果)。

例如:

rawHtml = "<raw html>"
resp,err = param.Fuzz(sprintf("\"></div>Hello<div id=\"asgdhFFASDljl")).Exec()
if err != nil {
println("Fuzz param %s error: %v", param.Name(), err)
return
}
rspo = <-resp
body, err = str.ExtractBodyFromHTTPResponseRaw(rspo.ResponseRaw)
if err != nil {
println("Get response body error: %v", err)
return
}
diffs,err = xhtml.CompareHtml(rawHtml,body)
if err != nil {
println("CompareHtml func error: %v", err)
return
}
for _, diff = range diffs {
if diff.Type == Tag {
printf("Found xss, XpathPos: %s, Reason: %s", diff.XpathPos, diff.Reason)
}
}

JavaScript 的 AST 抽象语法树判断

除了闭合标签或属性,目标站点还可能通过后端动态生成javascript代码,如

<script>var name = "<参数>";</script>,如果payload不会产生新标签,那就需要对script标签内的代码进行分析,yak提供了js.ASTWalk方法,可以获取所有变量、Identifies,语法错误

如果变量数量变化、函数名和数量变化或原请求的AST无语法错误,恶意请求导致AST有语法错误,就可以判断payload对页面“产生了影响”。

例:

originJs = "console.log('<参数>');"
fuzzJs = "console.log(''); 2022-6-10; console.log('');"
result,err = js.ASTWalk(fuzzJs)
// result有五个数组类型成员:StringLiteral、Int64Literal、Float64Literal、Identifies、BadSyntax

// originJs的<参数>本应该是String类型,fuzzJs的payload是'); 2022-6-10 console.log(',
isContain = false
for _,s = range result.StringLiteral{
if str.StringContainsAnyOfSubString(s, "2022-6-10"){
isContain = true
break
}
}
if !isContain{
println("Found xss")
}

通过语法错误判断

originJs = "console.log('<参数>');"
fuzzJs = "console.log(''');"
result1,err = js.ASTWalk(originJs)
result2,err = js.ASTWalk(fuzzJs)
if len(result1.BadSyntax) == 0 && len(result2.BadSyntax) != 0{
println("可能存在xss漏洞,但payload不对")
}

小总结

虽然我们并没有给出一个针对 XSS 检测的最佳实践,但是在启发式检测中,我们已经完善了启发式检测全流程的 “思路”。在用户掌握上述步骤中使用到的基础设施的时候,我相信每个人都是有关于 “漏洞检测” 的想法,恰好这一套辅助函数可以尝试让自己的想法无障碍落地。

更新通知

Yakit 1.0.15-sp7 Feature

  1. 优化 HTTP History 上滚动交互:会在恰当的时候自动加载数据,不需要每次都手动线上滚动~

  2. 修复了某些时候 “上 / 下” 方向键切换不了 HTTPFlow 的 BUG

  3. MITM 过滤器自动保存

  4. MITM 交互界面的表格新增 “ID / Body Size” 字段

  5. 右键 HTTP History 新增 “删除” 按钮

  6. 数据包编辑器 / monaco editor 新增 “自动解码”,可恢复 (\u0000) 格式的中文字符 @南风

  7. Web Fuzzer 新增简易版本的 “导出” 功能,支持 CSV / JSON 两种格式。

yaklang 引擎 v1.0.15-sp13

  1. 增加 gzip 支持:支持快捷解压/压缩接口

  2. 增加 fuzz.UrlToHTTPRequest,可以使用 method + url 快速构建 fuzz.HTTPRequest

  3. 增加 js.ASTWalk 遍历 javascript 中的符号和字面量

  4. 增加通过 poc.UrlToHTTPRequest 快速构建一个原始数据包

  5. 增加 codec.AutoDecode 以及 grpc autoDecode 支持,以便 yakit 今后提供智能解析的规则

  6. 支持 fuzz {{int(1-9990|4)}} 语法来支持 fuzz zero padding

VSCode 插件:

  1. 支持最新 v1.0.15-sp13

本文首发于 Yak Project 公众号,阅读原文