XSS 启发式检测基础设施

本文介绍了Yaklang中如何通过yak.xhtml和yak.js.ASTWalk这两个内置库辅助实现启发式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)
      }
   }
}
  1. 判断成功

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

Yak官方资源

Yak 语言官方教程:
https://yaklang.com/docs/intro/
Yakit 视频教程:
https://space.bilibili.com/437503777
Github下载地址:
https://github.com/yaklang/yakit
Yakit官网下载地址:
https://yaklang.com/
Yakit安装文档:
https://yaklang.com/products/download_and_install
Yakit使用文档:
https://yaklang.com/products/intro/
常见问题速查:
https://yaklang.com/products/FAQ

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值