MYBLOG

XSS漏洞扫描器的设计思路记录

2021-08-19 14:00:21ans

前言

之前写知识库系列,一直想弄一个自用的扫描器来实现知识库里的一些bypass和骚操作,来减少手工测试的时间。相比于SQL注入之类的,我一直觉得XSS的扫描原理似乎更复杂、更难以理解一点。因此选择做XSS扫描器作为开始。

目标除了和其他扫描器一样检测可能存在的XSS漏洞点,还要对漏洞点所过滤的关键词进行检测,并给出能够bypass的payload。

目前实现了部分功能,能够发现可能的XSS漏洞,检测出部分关键词,并选择部分能够bypass的payload,但无法处理CSP,无法应对http-only,离完全脱离手工测试还差的太远。

但是希望能把设计的思路写下来,也给之后的开发提供一个样本。

话不多说,下面就开始正题。

要实现上面的功能大体要做四个部分:爬虫、html解析、过滤词检测、payload生成。

首先通过爬虫爬取链接,找到所有可能的注入点,然后对这些注入点一一探测它们过滤了哪些关键词,再之后生成可用的payload,对每个注入点进行测试,若发现payload成功造成了XSS,则记录下来并返回。

难点主要有4个:

1.获取注入点时,页面内容中的URL部分通过正则比较容易获取,但POST类的请求例如表单等怎么爬取

对于爬虫我没有什么经验,于是搜了一下现有的爬虫框架,在github上恰好发现了一个专门给安全人员开发的crawlergo项目,于是就用了这个。

crawlergo最好的一点是它的输出格式为json,包含了url、method、headers、params等信息,后续发送请求比较方便。

2.过滤情况探测中,如何判断某个关键词是否被过滤

首先过滤方式我分成了ban和filter两种,前者是发现危险关键词直接禁止访问页面,后者是将危险关键词替换为空。尽管后者这种处理方式已经基本看不到了,还是以防万一加上了。

在看了一些扫描器的设计原理之后,它们进行探测的核心思想是构造一些页面没有的随机字符串作为payload发送,如果这个字符串存在于返回的报文中,则这个param可能存在XSS。

同样我们可以用这个思想去判断关键词的过滤情况。

对于script、on、alert等关键词,可以通过双写scscriptript、oonn、alalertert来去探测,这样的好处是:1.一般情况下页面里不会出现双写的字符串,误判的概率会降低,如果加上随机大小写那概率会进一步降低;2.如果关键词被ban了,页面必然不会出现这些双写payload,如果关键词被替换为空了,剩下的就是script、on、alert这些关键词,这样可以一次请求区分两种过滤方式。

那么对于单个符号的关键词,例如<,>,(,),’,”之类怎么去判断?

可以用“放大”的方式去处理,对于单个符号的,可以输入连续多个,去对应位置寻找是否有该字符串,例如<<<<<,’’’’’,”””””,>>>>>

3.根据过滤的关键词列表,如何选择合适的payload类型,如何生成payload

这部分我一开始想用机器学习的方式来做,后来想了想还是不太靠谱。所以还是用了最最原始的方法:首先写一堆payload,然后挑出没有过滤关键词的payload。只要有一个,就有很大可能成功。

4.如何判断发送的payload是否成功造成了XSS

关于这部分我想了一些思路,还是放到后面写吧。

HTML解析

这部分用于判断注入点位置以及验证漏洞是否存在。

XSS的注入点情况非常复杂,它可能在双引号里,可能在单引号里,可能在注释里,可能在标签属性里,可能在标签的内容里,每一种情况XSS的payload的前后缀都不同,因此必须有一个能够解析返回的HTML文本的工具。

一开始我用的是HTML_Parser,但是这个类它的方法基本都是“遇到xxx的时候所做的处理”。这样可以用来做XSS的探测,比如发送一个随机的payload,遇到某个标签时如果它的text就是这个payload就可以锁定XSS注入点的位置。但我除了要做位置判断,还要判断过滤的关键词等,写到后面时就觉得不太适合了。于是后来换成了beautifulsoup4,相对来说感觉功能比较全面。

关于注入点的位置,我粗略地分了16种情况:

  1. # 注入点的位置判断:1.在script标签内(2.在//注释内在单引号内 3.在//注释内在双引号内 4.在//注释内不在引号内 5.在/**/注释内在单引号内 6.在/**/注释内在双引号内 7.在/**/注释内不在引号内 8.不在注释内在单引号内 9.不在注释内在双引号内 10.不在注释内不在引号内) 11.在其他标签text内 12.在标签的属性中(必在引号里) 13.在html注释中 14.在href、action、code属性中 15.在on属性中 16.在object的data属性里 17.在标签之外

针对这些情况,payload的前后缀如下:

2.'\n+payload

3."\n+payload

4.\n+payload

5.'*/+payload+/*'

6."*/+payload+/*"

7.*/+payload+/*

8.';+payload+//

9.";+payload+//

10.直接payload或\n+payload

11.</标签名>+payload

12."+on事件或>+payload

13.-->+payload

14.直接javascript:+payload

15.直接发送js代码

16.用data:text/html;base64,PHNjcmlwdD5hbGVydCgiSGVsbG8iKTs8L3NjcmlwdD4=类似的payload

17.直接payload

虽然写起来好像每种注入点位置都有对应的前后缀,加上就行了,但是实际上处理起来非常麻烦(也可能是我代码能力太辣鸡了),payload的类型不同,有带标签的,也有不带标签的,也有带javascript:的,有闭不闭合标签都行的,有多种前后缀的,几乎每种情况都要特殊处理。

过滤词检测

之前在前言里写了过滤的关键词检测的思路,在实际写的时候我还是加了一点东西。

假设已知某个参数用户可控,内容会打印在页面上,那么检测过滤词的时候就给每个检测payload前面都加一个随机字符串。例如dUzJmjRTPsCrScRiPtIpT,如果script被ban了那么这个随机字符串也不会出现在返回的报文里。如果script只是被替换为空,那么随机字符串会出现在返回的报文里。

这样过滤词检测的准确性就得到了进一步的提高。

payload生成

这部分要完成payload的生成和发送。

payload的类型

由于注入点位置情况太复杂,把payload基本分为四种类型。

第一类payload是完整的script标签payload。为啥把它单独列出来,是因为script标签是唯一一个无属性的包含完整标签的payload。

第二类payload就是有属性的包含完整标签的payload。

这两类其实都对应于同样的位置情况,即处在处在标签内容中或处在可以闭合标签的位置,对应于上面的11、12、13、16、17。

第三类是在标签非可执行js的属性内部的,例如srcclassid这类属性里,payload不需要完整标签,只需要闭合属性然后再加一个能执行js的属性即可,也可以选择闭合标签。这种情况对应于上面的12。

第四类是在js代码内部的,例如在<scirpt>标签里,在on事件中,payload可以直接设为js代码。这个对应于上面的2-10、14、15。

一个html元素的组成

接下来是生成payload的部分。

一个完整的html元素,包含标签名、属性、标签值以及子标签等。含有子标签的payload比较少见,这里就先忽略掉。

payload发送的目的是发现XSS漏洞,而XSS漏洞的发现是通过执行JS代码来反馈的,我们通常用alert、prompt、confirm、document.write、console.log等来给予我们反馈,证明这个地方确实存在XSS。alert这类测试函数,在此先称之为test_func。

我们的目的就是让test_func能执行,所以就两种情况:一是test_func在script标签内部,二是test_func在某些可以执行JS的属性里。根据这两种情况写个函数来构造payload还是不难的。值得注意的是在<a href、<form action、<applet code等属性里要执行JS需要加上javascript:为协议,这也是上面第14种情况单独列出来的原因。

所以,构造一个payload,最多就是前后缀+标签+属性+test_func。可以没有标签和属性,但是test_func是必需的。同时test_func也是bypass花样最多的部分。所以可以将各种bypass的骚操作加入一个test_func列表中,在后面针对每种位置情况和过滤情况选择不同的test_func。

生成payload

逻辑比较简单,对于每个注入点,首先根据位置产生对应类型的无test_func的payload,然后查看payload里是否有关键词被ban掉,如果有就换其他同类型的payload,直到没有关键词被ban掉,如果不存在这样的payload,直接判定为无法XSS。

如果找到了第一个没有关键词被ban掉的payload,就去找没被ban掉的test_func,直到找到合适的,组成新的payload,然后加上前后缀形成完整payload。

再查看有没有被替换为空的关键词,若有,进行双写处理,这就是最终的payload了。

漏洞验证

关于漏洞验证,一开始我是想通过bs4解析HTML,然后通过查找元素、属性等是否包含payload来确定,但是这样写情况比较复杂,误判的可能较大。

所以想看一下有没有比较简单又精准的方法。

一开始想用无头浏览器的方式,用selenium直接通过有没有弹窗来判断是否存在XSS。这样的好处是几乎不会误判,但是缺点就是效率太慢了。何况这个扫描器比一般的扫描器还要多很多请求,直接无法接受。

后来想,能不能在页面中产生随机字符串,只要发现这个随机字符串存在那就是执行了JS了。

一个思路是常用的alert改成document.write,写入一段随机字符串,查找字符串是否存在。另一个思路是用createElement创建一个标签,标签名是随机字符串,然后只要查找这个标签产生了没即可。还有一种思路是给标签加入id属性,通过查找id来判断JS是否执行。
但是这样也有一个问题,payload里的随机字符串可能本身就存在于返回的报文里,这样无论执行成功与否都会返回成功。这也不是什么大问题,写入的内容可以在payload中用多个字符串拼接,例如document.write(9999+9999)来找19998,就可以避免这个问题。

然而,最大的问题是,通过requests等用于web请求的库获得的都是原生的页面源码,就是右键查看源代码里面的,是未经JS渲染过的。如果想要获得JS渲染过后的页面源码,还是要靠浏览器……

但是后来我偶然在github发现requests库的作者又做了一个新的requests-html库,就是为了解决这个问题,我赶紧pip install了一个,然后试了一下,发现还是不行。

貌似这个库只能渲染页面中本来存在的JS,对于XSS这种像HTML文档里新插入的JS脚本依然得不到渲染后的页面源码。

最后只得放弃,还是靠写逻辑来判断吧。

根据之前的那四类payload来区分验证逻辑。

第一类payload就是完整的script标签,这种比较容易验证,遍历所有的script标签,只要有一个text值为test_func即可判断XSS成功。
第二类有属性的完整标签payload,就记录属性,遍历所有标签名,看其中该属性的值是否为test_func,有一个就成功。

第三类是在标签非可执行js的属性内部的,payload为闭合引号加属性和值的,这种就记录属性,遍历所有同名属性查看是否存在值为test_func的属性,存在就成功。

第四类是在js代码内部的,直接查找返回报文看有没有test_func即可。

如果完全按照这种逻辑来判断,那漏报率要爆炸。

之后经过了大量测试,附加了一些新的细分逻辑,才勉强能满足基本的XSS扫描。还需要后期不断地调整。

小结

目前这个扫描器能满足我自己的基本的扫描需求,但是问题也很多:

1.payload是大小写混搭的,对于标签或者属性,浏览器可以解析大小写混杂的内容,但是JS内容不行,所以扫描器给出的payload直接拿去试基本没有一个能用的,但只要稍微调整一下大小写和URL编码就可以

2.扫描器对于http-only和CSP完全没有任何处理,目前暂时也没想到怎样去应对这两种安全措施。

3.扫描器的过滤词检测还远远不够,test_func数量也不够,并且还有很多特殊情况没有加入到扫描范围中,需要改进和提升的地方还有太多。

本次算是第一次开发扫描器,经历了痛苦的debug后代码已经惨不忍睹。

不过也算是了解一点扫描器开发的知识(其实就是自己瞎鸡掰试罢了)。后续有时间有机会的话会继续做其他漏洞类型的扫描器。

参考文献

https://paper.seebug.org/1119/

https://github.com/Qianlitp/crawlergo/blob/master/README_zh-cn.md

https://github.com/timwhitez/crawlergo_x_XRAY

https://www.aqniu.com/vendor/42666.html

全部留言 0