这是一个仅有一百行的xss检测脚本,现在来分析一下他的源码
项目地址:https://github.com/stamparm/DSXS
0x01 扫描思路
支持dom型、存储型、反射型xss的扫描,存储和反射的扫描思路一致后面再进行分析,先来看一下dom型的xss是如何被检测的
整体的思路就是
首先根据DOM_FILTER_REGEX去掉响应包中可能会影响检测的内容,然后在响应中查找dom_patterns,如果存在,则提示可能存在dom-xss
所有的扫描都是先进行dom检测 然后再进行一次其他xss的检测
在代码中有一个核心的REGULAR_PATTERNS匹配元组
REGULAR_PATTERNS = ( # each (regular pattern) item consists of (r"context regex", (prerequisite unfiltered characters), "info text", r"content removal regex")
(r"\A[^<>]*%(chars)s[^<>]*\Z", ('<', '>'), "\".xss.\", pure text response, %(filtering)s filtering", None),
(r"<!--[^>]*%(chars)s|%(chars)s[^<]*-->", ('<', '>'), "\"<!--.'.xss.'.-->\", inside the comment, %(filtering)s filtering", None),
(r"(?s)<script[^>]*>[^<]*?'[^<']*%(chars)s|%(chars)s[^<']*'[^<]*</script>", ('\'', ';'), "\"<script>.'.xss.'.</script>\", enclosed by <script> tags, inside single-quotes, %(filtering)s filtering", r"\\'|{[^\n]+}"),
(r'(?s)<script[^>]*>[^<]*?"[^<"]*%(chars)s|%(chars)s[^<"]*"[^<]*</script>', ('"', ';'), "'<script>.\".xss.\".</script>', enclosed by <script> tags, inside double-quotes, %(filtering)s filtering", r'\\"|{[^\n]+}'),
(r"(?s)<script[^>]*>[^<]*?%(chars)s|%(chars)s[^<]*</script>", (';',), "\"<script>.xss.</script>\", enclosed by <script> tags, %(filtering)s filtering", r"&(#\d+|[a-z]+);|'[^'\s]+'|\"[^\"\s]+\"|{[^\n]+}"),
(r">[^<]*%(chars)s[^<]*(<|\Z)", ('<', '>'), "\">.xss.<\", outside of tags, %(filtering)s filtering", r"(?s)<script.+?</script>|<!--.*?-->"),
(r"<[^>]*=\s*'[^>']*%(chars)s[^>']*'[^>]*>", ('\'',), "\"<.'.xss.'.>\", inside the tag, inside single-quotes, %(filtering)s filtering", r"(?s)<script.+?</script>|<!--.*?-->|\\"),
(r'<[^>]*=\s*"[^>"]*%(chars)s[^>"]*"[^>]*>', ('"',), "'<.\".xss.\".>', inside the tag, inside double-quotes, %(filtering)s filtering", r"(?s)<script.+?</script>|<!--.*?-->|\\"),
(r"<[^>]*%(chars)s[^>]*>", (), "\"<.xss.>\", inside the tag, outside of quotes, %(filtering)s filtering", r"(?s)<script.+?</script>|<!--.*?-->|=\s*'[^']*'|=\s*\"[^\"]*\""),
)
通过查看这些元组的内容可以发现这是在匹配所有可以进行xss的位置(需要恶补正则了)
一共四个元素,第一个是正则表达式,第二个是 condition状况 第三个是info 第四个content removal regex
规则大致解析如下表
上下文大致正则表达式 | 必备未过滤的字符 | 信息文本 | 需要去除的内容 | 利用方式 |
---|---|---|---|---|
.xss. |
< > | 纯文本响应 | 无 | 使用标签 |
<!--.xss.--> |
< > | 注释内 | 无 | 闭合注释 |
<script>.'.xss.'.</script> |
单引号,分号不过滤 | 在script标签单引号包裹 | 去掉’ | 单引号逃逸然后自定义js语句 |
<script>.".xss.".</script> |
双引号分号 | 在script标签双引号包裹 | 去掉” | 双引号逃逸然后自定义js语句 |
<script>.xss.</script> |
分号不过滤 | 在script标签内 | 无 | 自定义js语句 |
>.xss.< |
<> 不过滤 | 在标签外 | 去掉注释和script防止payload重合 | 自定义标签 |
<.'.xss.'.> |
‘不过滤 | 标签内 被单引号包裹 | 去掉注释和script和\ | 单引号逃逸然后利用标签属性 |
<.".xss.".> |
“不过滤 | 标签内 被双引号包裹 | 去掉注释和script和\ | 双引号逃逸然后利用标签属性 |
<.xss.> |
无 | 在标签内 | 去掉注释和script和=’…’和=”…” | 利用标签属性 |
在规则之后来查看代码
0x02 代码分析
代码很短,仅有四个主要函数
_retrieve_content
就是抓包返回结果的包装函数
_contains
确定“必备未过滤的字符”是否有被过滤 原理是判断是否在字符前面存在\
init_options
的作用是获取我们传入的参数当作全局变量存储
scan_page
是最核心的函数
代码第47行:
url, data = re.sub(r"=(&|\Z)", "=1\g<1>", url) if url else url, re.sub(r"=(&|\Z)", "=1\g<1>", data) if data else data
这句话的用途是如果我们传入的参数中存在空,则替换为1,比如&a=&b=1替换后就变成&a=1&b=1
这个功能在awvs的规则中同样存在
original = re.sub(DOM_FILTER_REGEX, "", _retrieve_content(url, data)) #将响应中的DOM_FILTER_REGEX去掉,包括单引号 双引号中的 注释
dom = next(filter(None, (re.search(_, original) for _ in DOM_PATTERNS)), None
# DOM_PATTERNS 从响应体中正则判断是否存在 dom xss 可能性
# 因为dom型xss是js直接操作节点来生成的所以js源码有能正则匹配的document\.write\(|\.innerHTML location setTimeout等
如果存在就输出位置
后面是参数传递xss的检测
try:
for phase in (GET, POST): #判断是什么形式的请求方式
current = url if phase is GET else (data or "")
for match in re.finditer(r"((\A|[?&])(?P<parameter>[\w\[\]]+)=)(?P<value>[^&#]*)", current):
#如果能匹配到 就是对的请求方式 设置usable为true
found, usable = False, True
print("* scanning %s parameter '%s'" % (phase, match.group("parameter")))
#生成随机的五位长度字符串
prefix, suffix = ("".join(random.sample(string.ascii_lowercase, PREFIX_SUFFIX_LENGTH)) for i in range(2))
后面的代码看起来很长,一句一句分析
for pool in (LARGER_CHAR_POOL, SMALLER_CHAR_POOL):
为什么这里需要设置两个参数集,查看作者的注释,“用于XSS篡改参数值的字符(较小的字符集-避免可能的SQLi错误)” 这是SMALLER_CHAR_POOL的,所以说小字符集的作用就是防止sql注入误报
tampered = current.replace(match.group(0), "%s%s" % (match.group(0), urllib.parse.quote("%s%s%s%s" % ("'" if pool == LARGER_CHAR_POOL else "", prefix, "".join(random.sample(pool, len(pool))), suffix))))
这句是生成payload,代码有点长 可以直接看生成的内容来判断规则
1'hjref>;'"<zyftg
例如这样的格式,可以比较明显发现实际就是LARGER_CHAR_POOL = ('\'', '"', '>', '<', ';')
和上面随机产生的prefix, suffix组合
这里的实现方法其实是原本的参数替换成原本参数拼接上四个成分:如果for循环中的pool是LARGER_CHAR_POOL
,那就添加'
如果不是那就不需要加,也是为了防止误报,但是这个重复添加的'
的原因可能用于制造sql注入,在报错信息中查找注入点
因为在这里添加了一个'
所以后面在content返回内容中也进行了替换删除
然后把参数替换上我们的paylaod发起请求获取返回内容
for regex, condition, info, content_removal_regex in REGULAR_PATTERNS:
filtered = re.sub(content_removal_regex or "", "", content)
这里的参数对应上一部分我写的表,可以回去看
替换返回包中的需要去除的内容防止误判
for sample in re.finditer("%s([^ ]+?)%s" % (prefix, suffix), filtered, re.I):
context = re.search(regex % {"chars": re.escape(sample.group(0))}, filtered, re.I)
用给定的正则匹配方式去匹配文本中存在的prefix和suffix看看是否插入成功 如果这一部分是插入成功的
那么后面就只需要看是否有关键词被转义即可
if context and not found and sample.group(1).strip():
if _contains(sample.group(1), condition):
print(" (i) %s parameter '%s' appears to be XSS vulnerable (%s)" % (phase, match.group("parameter"), info % dict((("filtering", "no" if all(char in sample.group(1) for char in LARGER_CHAR_POOL) else "some"),))))
found = retval = True
break
通过这一部分就说明可能存在xss,就返回结果
0x03 小结
审计这个扫描器我们学习了什么:
- dom xss的检测方式:通过正则来查找有可能造成漏洞的函数如果在js中存在这样的函数,并且参数可控就返回,主要学习的是这个规则
- 参数传递xss检测方式:通过判断每一个可以利用的点,以及对应的必备的字符,如果这个必备字符没有被过滤就证明存在技巧:使用两个随机字符串 再加上必备未过滤字符,进行正则匹配
在使用过程中发现存在问题:会产生sql注入的误报,比如
这是属于一个sql注入漏洞的误报,最终因为返回内容只过滤了尖括号导致判断存在xss,
所以可以添加解决方案
SQLI_PATTERNS=(
r"You have an error in your SQL syntax"
)
if re.search(SQLI_PATTERNS,content):
print(" (i) %s parameter '%s' appears to be SQL-Injection vulnerable" % (phase, match.group("parameter")))
再显示一下 或者 直接接入到sqlmap再跑一遍