【VULNERABLITY】DOM XSS

  1. 1. 引言
  2. 2. DOM渲染
  3. 3. DOM fuzzing
  4. 4. DOM XSS 挖掘
    1. 4.0.0.1. 思路一
    2. 4.0.0.2. 思路二
  • 附录
  • 引言

      这几天阅读了《Web前端黑客技术揭秘》 ,对DOM型的XSS进行一个总结,内容主要为书中提到的知识点,整理整理作以后复习所用。
      DOM类型的XSS与反射型、存储型XSS都不同,DOM型XSS不用服务器端解析响应的参与,触发DOM型XSS可以说主要依靠浏览器客户端的解析。常见的输出点见0x04附录

    DOM渲染

      首先我们来理解一下HTML与Javascript自解码机制,查看以下三个例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    1. <input type="button" id=exec_btn" value="exec" onclick="document.write('<img src=@ onerror=alert(123) />')" />
    2. <input type="button" id=exec_btn" value="exec" onclick="document.write(HtmlEncode('<img src=@ onerror=alert(123) />'))" />
    <script>
    function HtmlEncode(str) {
    var s="";
    if(str.length==0) return "";
    s=str.replace(/&/g, "&amp;");
    s=str.replace(/</g, "&lt;");
    s=str.replace(/>/g, "&gt;");
    s=str.replace(/\"/g, "&quot;");
    return s;
    }
    3. <input type="button" id=exec_btn" value="exec" onclick="document.write('&lt;img src=@ onerror=alert(123) /&gt;')" />

      对于第一种情况,很清楚,点击这个按钮后,会将<img src=@ onerror=alert(123) />写入DOM中,并触发alert(123)。而第二种与第三种,document.write的内容都变成了&lt;img src=@ onerror=alert(123) /&gt;,区别在于一个是在HTML标签中,一个是通过Javascript处理后才变成这个样子的,那么这2种情况都会触发弹窗吗?答案是第二种不会,而第三种会触发。
      形成这样的原因就是因为HTML与Javascript自解码机制,在HTML标签中的javascript可以进行HTML形式的编码。在HTML标签中的javascript代码会先被HTML形式的编码进行解码,即第三种情况中&lt;img src=@ onerror=alert(123) /&gt;在javascript运行前已经解码为<img src=@ onerror=alert(123) />,而第二种情况为javascript运行中进行的HTML形式的编码,所以写到DOM中时直接显示在页面上。
    HTML中的编码:

    • 进制编码:&#xH;(十六进制格式)、&#D;(十进制格式),最后的分号可以不要
    • HTML实体编码:即上面的那个HtmlEncode

      那么同样的,在javascript上下文环境中,将内容改为javascript的编码,同样会自解码,我们来看下一个例子

    1
    2
    3
    4
    5
    6
    7
    8
    <input type="button" id="exec_btn" value="exec" />
    <script>
    function $(id){return document.getElementById(id);};
    $('exec_btn').onclick=function(){
    document.write('<img src=@ onerror=alert(123) />');
    document.write('\u003c\u0069\u006d\u0067\u0020\u0073\u0072\u0063\u003d\u0040\u0020\u006f\u006e\u0065\u0072\u0072\u006f\u0072\u003d\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0032\u0033\u0029\u0020\u002f\u003e');

    }

      上面2中write出现的结果是相同的,同样的道理,由于上述编码为javascript的编码形式,并且在javascript的上下文环境中,会先进行解码,再运行javascript。
    JavaScript中的编码:

    • Unicode形式:\uH
    • 普通十六进制:\xH
    • 纯转义:\’、\”、\<、>这样在特殊字符前加\进行转义

      通过上面几个例子,我们可以知道在HTML中与在Javascript中自动解码的差异,如果防御没有区分这样的场景,就会出现问题。
      理解了上述的自解码机制,在不同的标签下会有不同的结果,比如一下几个标签会自带HtmlEncode功能

    1
    2
    3
    4
    5
    6
    7
    8
    <title></title>
    <iframe></iframe>
    <noscript></noscript>
    <noframes></noframes>
    <textarea></textarea>

    <xmp></xmp>
    <plaintext></plaintext>

      <xmp>没有HtmlEncode功能,<plaintext>在Firefox下会进行HtmlEncode编码,在chrome下不会。

    DOM fuzzing

      直接看代码把 下面的程序用python编写

    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
    def get_template(template_file):
    """获取fuzzing的模板文件内容"""
    content=''
    with open(template_file) as f:
    content=f.read()
    return content

    def set_result(result_file,result):
    """生成fuzzing结果文件"""
    with open(result_file,'w') as f:
    f.write(result)
    def fuzzing(fuzz_file,result_file):
    template=get_template(fuzz_file)
    fuzz_area_0=template.find('<fuzz>')
    fuzz_area_1=template.find('</fuzz>')
    fuzz_area=template[fuzz_area_0+6:fuzz_area_1].strip()
    # chars=[]
    chars=[]
    for i in xrange(255): # ASCII玛转换为字符
    if i!=62:
    chars.append(chr(i))

    fuzz_area_result=''
    for c in chars: #遍历这些字符 逐一生成fuzzing内容
    fuzz_area_r=fuzz_area.replace('{{char}}',c)
    fuzz_area_r=fuzz_area_r.replace('{{id}}',str(ord(c)))
    fuzz_area_result+=fuzz_area_r+'\n'
    print fuzz_area_r
    result=template.replace(fuzz_area,fuzz_area_result)
    set_result(result_file,result)

    if __name__=='__main__':
    fuzzing("fuzz_xss_0.html","res.html")

      下面为fuzz_xss_0.html的内容

    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
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Fuzz xss 0</title>
    <script>
    function $(x){return document.getElementById(x);}
    function f(id){
    $('result').innerHTML+=id+'<br/>';
    }
    </script>
    </head>
    <body>
    <h3>Fuzzing Result:</h3>
    <code>
    {{id}}: <{{char}}script>f("{{id}}")</script>
    </code>
    <div id="result"></div>
    <br/>
    <h3>Fuzzing...</h3>
    <fuzz>
    {{id}}: <{{char}}script>f("{{id}}")</script><br/>
    </fuzz>
    </body>
    </html>

      通过上面的fuzzing技巧,可以自行扩展

    DOM XSS 挖掘

    1. 静态方法
        静态方法查找危险关键字,可以使用下列正则表达式来匹配。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      Finding Sources

      The following regular expression attempts to match most common DOMXSS sources (BETA):

      /(location\s*[\[.])|([.\[]\s*["']?\s*(arguments|dialogArguments|innerHTML|write(ln)?|open(Dialog)?|showModalDialog|cookie|URL|documentURI|baseURI|referrer|name|opener|parent|top|content|self|frames)\W)|(localStorage|sessionStorage|Database)/

      Finding Sinks

      The following regular expression attempts to match most common DOMXSS sinks (BETA):

      /((src|href|data|location|code|value|action)\s*["'\]]*\s*\+?\s*=)|((replace|assign|navigate|getResponseHeader|open(Dialog)?|showModalDialog|eval|evaluate|execCommand|execScript|setTimeout|setInterval)\s*["'\]]*\s*\()/

      This regular expression finds sinks based on jQuery, it also finds the $ function, which is not always insecure:

      /after\(|\.append\(|\.before\(|\.html\(|\.prepend\(|\.replaceWith\(|\.wrap\(|\.wrapAll\(|\$\(|\.globalEval\(|\.add\(|jQUery\(|\$\(|\.parseHTML\(/

    详情可以看https://code.google.com/archive/p/domxsswiki/wikis/FindingDOMXSS.wiki
    一旦发现页面存在可疑特征,就进行人工分析,这是静态方法的代价,对人工参与要求很高

    1. 动态方法
       动态方法相当于一次Javascript源码动态审计的过程。书中提到了两种思路
      1
      2
      3
      <script>
      eval(location.hash.substr(1));
      </script>

     就拿上面的例子来说,如何检测上面的DOM XSS

    思路一

      借用浏览器自身的动态性,可以写Firefox插件,批量对目标地址发起请求(一个模糊测试的过程),请求的形式为:在目标地址后加上#fuzzing内容,就当前这个例子来说。比如当前fuzzing内容为:var x='d0mx55'
      并且对常见的输出点函数进行劫持,如

    1
    2
    3
    4
    5
    6
    var _eval=eval;
    eval=function(x){
    if(typeof(x)=='undefined'){return;}
    if(x.indexOf('d0mx55')!=-1){alert('found dom xss');
    _eval(x);
    };

      在javascript层面劫持innerHTML这样的属性已经没那么容易了,常用的属性劫持可以针对具体的对象设置__defineSetter__,比如下面的代码

    1
    2
    window.__defineSetter__('x',function(){alert('hijack x')});
    window.x='xxxxxYYYYYYYY';

    当x赋值的时候,就会触发事先定义好的Setter方法。innerHTML属性属于那些节点对象,想劫持具体节点对象的innerHTML,需要事先知道这个具体节点的对象,然后设置__defineSetter__,这样如果要检测DOM XSS,就要劫持所有的输出点,比较麻烦,那么思路二可能会比较简单一点

    思路二

      仍然借用浏览器动态执行的优势,写一个Firefox插件,我们完全以黑盒的方式进行模糊测试输入点,然后判断渲染后的DOM树中是否有我们期待的值,比如,模糊测试的内容都有如下一段代码document.write('d0m'+'x55')如果这段代码顺利执行了就会存在d0mx55文本节点,后续的检测工作只要判断是否存在这个文本节点就可以了

    1
    2
    3
    if(document.documentElement.innerHTML.indexOf('d0mx55')!=-1){
    alert('found dom xss');
    }

    这个思路以DOM树的改变为判断依据,简单准确,但是同样无法避免那些逻辑判断上导致的漏报。

    附录

    输出点 javascript code
    直接输出HTML内容 document.write(…)
    document.writeln(…)
    document.body.innerHtml=…
    直接修改DOM树(包括DHTML事件) document.forms[0].action=…
    document.attachEvent(…)
    document.create…(…)
    document.execCommand(…)
    document.body. …
    widow.attachEvent(…)
    替换document url document.location=…(以及直接赋值给location的href,host,hostname属性)
    document.location.hostname=…
    document.location.replace(…)
    document.location.assign(…)
    documnent.URL=…
    window.navigate(…)
    打开或修改新窗口 document.open(…)
    window.open(…)
    window.location.href=…(以及直接赋值给location的href,host,hostname属性)
    直接执行脚本 eval(…)
    window.execScript(…)
    window.setInterval(…)
    window.setTimeout(…)