一直以来的一个想法,现在有新的思路的就写出来看看。

自动化Web漏洞扫描器是渗透测试亘古不变的话题。把当前一直重复的手工劳动和新的思路转化为自动化工具,利用技术和程序实现为自己节省时间是一件非常有趣的事情。然而,自动化的路坑却不少。其中误报就是一件比较蛋疼的事情,XSS的误报尤其蛋疼。当前众多扫描器的XSS插件都存在误报的情况,我们来看一下大部分扫描器的XSS检测思路,我写了一个简单的Python函数来描述它,当然实际的通用XSS检测插件肯定会比这个要复杂的多,我只截取了一部分来说明思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
headers={}

def xss_detect(method='GET', querylist=[], url=''):
params = {}
resp = None
# 这里有更多的payload,如onfocus=alert(2333)
payload = '-->\'"><script>alert(1);</script>'
if method == 'GET':
for query in querylist:
# url = '{url}?{query}={payload}'.format(url=url,query=query,payload=payload)
params[query] = payload
resp = requests.get(url, params=params, headers=headers)
# 此处省略POST
if resp:
# 这里有更多的正则匹配规则
if '<script>alert(1);</script>' in resp.content:
return True
else:
return False

再准备一个有XSS漏洞的php脚本:

1
2
3
4
<?php
$id = $_GET['id'];
echo $id;
?>

测试:

由脚本可知,当前的XSS自动化测试思路一般都是给GET或POST参数传入Payload,并正则匹配检测返回的响应中是否含有我们构造payload的完整的html标签(不一定是<script>标签,见过用<H1>的=.=),如果有则判断该页面含有XSS漏洞。

这里的误区在于html页面含有闭合的<script>含有payload的标签,是否就代表JS可执行呢?

我们来看下面这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$id = $_GET['id'];

// 基于黑名单的过滤
$black_list = array('"', "'");
foreach ($black_list as $key => $value) {
$id = str_replace($value, '', $id);
}

echo '<input type="text" name="" value="' .$id. '">';

?>

该php脚本过滤掉了双引号和单引号,并把GET请求中获得的参数添加的input的value属性中,这种情况依照原本xss检测的方法,会出现误报,即虽然从http返回中能检测到闭合完整的<script>标签,但实际该js是不执行的。

其余会产生类似误报的情况也很多,这是一直困扰我的一个问题。直至我在前天重新温习爬虫知识的时候,把爬虫的某项知识和这个联系在了一起,有了一个新的思路。

在SQL注入的自动化检测里面,我们会力求出数据或输出由SQL语句生成的字符串,我们的正则是去匹配sql生成字符串而非sql语句,这样能保证sql语句正确执行了。例如我们payload是select md5(0x2333333),正则匹配查找5e2e9b556d77c86ab48075a94740b6f7,这样就能大幅度减小误报率。那XSS是否也能用这样的思路呢?可惜的是,几乎所有的http请求库都不会返回js执行之后的结果而只会返回未执行的js代码:

1
2
3
4
In [1]: import requests
In [3]: r = requests.get('http://127.0.0.1:8080/html.html')
In [4]: r.content
Out[4]: '<!DOCTYPE html>\n<html>\n<head>\n <title>111</title>\n</head>\n<body>\n<script type="text/javascript">\n document.body.appendChild(document.createTextNode(String.fromCharCode(84,48,48,76,83)));\n</script>\n</body>\n</html>'

很明显requests库不会像浏览器一样去执行JavaScript。既然requests库不行,那么是不是有其它Python库可以实现模拟浏览器执行js,返回被js操作过的dom树的情况呢?python-qt4有可以模拟浏览器操作的Web工具库,只要是浏览器支持的操作它一般都能模拟。使用PyQt4.QtWebKit就可以执行js,返回js执行之后的结果。我们写一个小demo来验证一下该想法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sys  
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtWebKit import *
from lxml import html

class Render(QWebPage):
def __init__(self, url):
self.app = QApplication(sys.argv)
QWebPage.__init__(self)
self.loadFinished.connect(self._loadFinished)
self.mainFrame().load(QUrl(url))
self.app.exec_()

def _loadFinished(self, result):
self.frame = self.mainFrame()
self.app.quit()
1
2
3
4
5
In [3]: url = 'http://192.168.1.126:8080/html.html'
In [4]: r = Render(url)
In [5]: result = r.frame.toHtml()
In [6]: result
Out[6]: PyQt4.QtCore.QString(u'<!DOCTYPE html><html><head>\n <title>111</title>\n</head>\n<body>\n<script type="text/javascript">\n document.body.appendChild(document.createTextNode(String.fromCharCode(84,48,48,76,83)));\n</script>T00LS\n\n</body></html>')

可以发现和requests不同,Html内JavaScript已经执行并在html里面插入了T00LS这样的节点。这样我们就可以获取并匹配Js执行之后的内容T00LS,保证Xss测试用的Payload在浏览器是可以被执行的,减少误报。

修改后的xss检测插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import sys  
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtWebKit import *
from lxml import html

class Render(QWebPage):
def __init__(self, url):
self.app = QApplication(sys.argv)
QWebPage.__init__(self)
self.loadFinished.connect(self._loadFinished)
self.mainFrame().load(QUrl(url))
self.app.exec_()

def _loadFinished(self, result):
self.frame = self.mainFrame()
self.app.quit()

def xss_detect(method='GET', querylist=[], url=''):
params = {}
resp = None
payload = '-->\'"><script>document.body.appendChild(document.createTextNode(String.fromCharCode(84,48,48,76,83)));</script>'
if method == 'GET':
for query in querylist:
url = '{url}?{query}={payload}'.format(url=url,query=query,payload=payload)
r = Render(url)
result = r.frame.toHtml()
resp = str(result.toAscii())
print resp
if resp:
if 'T00LS' in resp:
return True
else:
return False

print xss_detect(querylist=['id',], url='http://192.168.1.126/xss.php')

分别对上面构造的两个php脚本进行检测,可以发现不会出现相同的误报了。

这种方法保证了我们所插入的Js代码确实在浏览器能执行,实际的环境中,我们应该让Js生成比T00LS更加复杂得多的字符,这样就可以避免误报了。

本文URL: https://blog.neargle.com/2016/07/23/a-think-of-automatic-xss-detection-used-python-qt4/index.html