DSXS源码分析

 

这是一个仅有一百行的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 小结

审计这个扫描器我们学习了什么:

  1. dom xss的检测方式:通过正则来查找有可能造成漏洞的函数如果在js中存在这样的函数,并且参数可控就返回,主要学习的是这个规则
  2. 参数传递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再跑一遍

(完)