漏洞分析:XSS 启发式检测基础设施
背景:
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
-
优化 HTTP History 上滚动交互:会在恰当的时候自动加载数据,不需要每次都手动线上滚动~
-
修复了某些时候 “上 / 下” 方向键切换不了 HTTPFlow 的 BUG
-
MITM 过滤器自动保存
-
MITM 交互界面的表格新增 “ID / Body Size” 字段
-
右键 HTTP History 新增 “删除” 按钮
-
数据包编辑器 / monaco editor 新增 “自动解码”,可恢复 (\u0000) 格式的中文字符 @南风
-
Web Fuzzer 新增简易版本的 “导出” 功能,支持 CSV / JSON 两种格式。
yaklang 引擎 v1.0.15-sp13
-
增加 gzip 支持:支持快捷解压/压缩接口
-
增加 fuzz.UrlToHTTPRequest,可以使用 method + url 快速构建 fuzz.HTTPRequest
-
增加 js.ASTWalk 遍历 javascript 中的符号和字面量
-
增加通过 poc.UrlToHTTPRequest 快速构建一个原始数据包
-
增加 codec.AutoDecode 以及 grpc autoDecode 支持,以便 yakit 今后提供智能解析的规则
-
支持 fuzz {{int(1-9990|4)}} 语法来支持 fuzz zero padding
VSCode 插件:
- 支持最新 v1.0.15-sp13
本文首发于 Yak Project 公众号,阅读原文。
