浏览器解析与编码顺序及xss挖掘绕过全汇总
- 2019 年 10 月 10 日
- 筆記

在以往的培训和渗透过程中,发现很多渗透人员尤其是初学者在挖掘xss漏洞时,很容易混淆浏览器解析顺序和解码顺序,对于html和js编码、解码和浏览器解析顺序、哪些元素可以解码、是否可以借助编码绕过等情况也基本处于混沌的状态,导致最终只能扔一堆payload上去碰碰运气。这篇文章就把浏览器解析顺序、编码解码的类型、各种解码的有效作用域以及在xss里的实战利用技巧做一个系统总结,让你深度掌握xss挖掘和绕过。
文章结构如下:

1、前提:编码相关
1.1 URL编码
标准的url结构我们都清楚,像这样:
scheme://login:password@address:port/path?query_string#fragment
比如:
http://example.com/test.php?uid=27&content=on#main
可以看到,像:?/=这些字符是浏览器用来解析URL用于语义分隔的保留字符,那么问题来了,如果URL中某个部分的名称用到了这些字符,就会破坏语法,影响正常解析,于是就有了url编码,它以一个百分号%和该字符的ASCII对应的2位十六进制数去替换这些字符,如常见的空格编码为%20,百分号%编码为%20,等于号=编码为%3d,等等。
1.2 HTML编码
跟url的问题类似,一些字符在 HTML 中也是是预留的,像<这样的对于HTML来说有特殊意义的字符,在浏览器中会被解析成各种标签,如果要作为纯文本输出这个字符,就需要用到字符实体。
HTML编码类型:
说到编码的时候,大多数初学者看到&#x;这些字符通常会一头雾水,其实我们见到的所谓“编码”有两种,共同点是以连接符&开头以分号;结尾,中间字符有:
- 英文字符;
- #后接十进制数或#x后接十六进制数。
如<、<和<都可以被解码成常见的尖括号<:

再具体一点,lt叫做实体名称,60和x3c叫做实体编号,效果其实是一样的,只是实体名称更容易记忆,但就浏览器的支持性来说实体编码要好一些。常见的实体如下:

(注:在<被过滤的场景下很多人会尝试使用<来绕过,这样输出的尖括号会被解析成文本格式,而不是作为标签执行,很少的情况下可以利用成功,下面会有案例讲到。)
1.3 JS编码
道理同上,js常见的反斜杠方式编码处理
- b退格符,t制表符,v垂直制表符等;
- 三位数字,不足位数用0补充,按8位原字符八进制字符编码;
- 两位数字,不足位数用0补充,按8位原字符16进制字符编码,前缀 x
- 四位数字,不足为数用0补充,按16位原字符16进制Unicode数值编码,前缀 u 。
如145、x65和u0065都代表字符e。
2、浏览器解析顺序:
2.1 数据包处理过程:
首先要了解我们在构造xss包的时候发生了什么:
1、在浏览器的地址栏中输入url,发送http请求头和数据;
2、数据包通过网络传输到达远程web服务器,服务器接收到url,分析请求头,根据它找到对应资源,经过后端代码进行处理(过滤,校验),然后给前端返回响应头和数据;
3、浏览器接收到响应的数据后,对数据进行解析(下面要说的事)
2.2 浏览器解析顺序
主要分为两个过程:
1、 浏览器接收到响应数据后,解析器先对HTML之类的文档进行解析,构建成DOM节点树,同时,CSS会被CSS解析器解析生成样式表。
2、 解析html标签过程中遇到<script>标签,则暂停HTML标签解析,控制权转交给JavaScript引擎,执行完后继续解析html,js可以对DOM进行修改。

因此js所处的位置会影响DOM的操作顺序,js若在header中会立即执行,若放在body的最尾部则可以在DOM生成后对DOM进行处理,若在DOM结构生成之前就调用DOM,JavaScript会报错。但使用defer属性也可以让浏览器在DOM加载完成后,再执行指定脚本。
2.3 实例解析
例1:纯html代码:
<html>
<h1>Main Title</h1>
<div>
<h2>Second Title</h2>
<p>Content</p>
</div>
</html>
通过解析器的解析后生成DOM树:

如果在这个时候,如果修改一下,比如说把<h1>Main Title</h1>用html编码成:
<h1>Main Title</h1>
<h1>MainTitle</h1>
前者对标签名编码,后者对标签内容编码,结果会发现前者没有了html标签的作用,而后者正常显示。由此可以明白HTML解码的时机:它是在浏览器构建完DOM树以后才进行解码的,当解析器对前者进行解析时,无法识别为html标签,所以构建不了DOM节点,后者在顺利构建完DOM树之后对节点内容进行解码。
例2:包含js和html的代码:
<div id="content"></div>
<img src="y.com" onerror="alert(2)"/>
<p>Hello Parser!</p>
<script>
var content = document.getElementById("content");
var img = document.createElement("img");
img.src = "x.com";
img.setAttribute("onerror","alert(1)");
content.appendChild(img);
</script>
尾部的script脚本中改变了DOM节点树,通过对<div>操作新增了一个<img>,所以通过调换<script>和img的先后顺序,会使得弹框的顺序不同。

3、浏览器解码顺序:
3.1 URL解码:
url解码过程较为简单,服务器对接收到用户传输过来的URL进行解析,遇到%便自动进行解码。
3.2 HTML解码:
首先了解一下HTML解析器的工作原理:
HTML解析器其实是一个状态机,在对HTML资源从上而下进行解析时遇到一个‘<‘符号就会进入标签开始状态(Tag Open State),然后搜寻标签,img可以被识别为正确的标签,img1则不会识别,最后在读到最近的一个‘>’时,结束标签状态进入数据状态(Data State)。
哪些HTML字符实体会被解析?
一般来说,HTML编码要在Data state(标签外部和标签的text段),标签内的属性值的位置才能被解析。可以对各个部分进行测试,是否可以使用实体替换以及执行效果如何:

3.3 Js解码:
Js解码就简单很多,js的脚本处理模型是按照源码处理-函数解析-代码执行这个执行流来的,不管是外部引用还是直接写在script标签里,又或者是在html标签的属性里,对于js编码的解码都是相同的,所以分别对函数编码:
<script>
u0061lert("HelloWorld");
</script>
对value值进行编码:
<script>
alert("u0048elloWorld");
</script>
效果都是一样的,xss挖掘中这样的编码适用于js代码环境中alert等函数被过滤的情况。
3.4 浏览器的解码顺序:
首先要强调是一点是:浏览器的解码顺序和解析顺序是两码事。浏览器一般的解码顺序是先进行html解码,再进行javascript解码,最后再进行url解码,需要注意的是这里的url解码和我们发送到服务器的url解码不同,那个过程是由服务器来完成的,而不是浏览器。
明白了这个顺序,我们就可以理解<script>alert('1')</script>是无法弹框的,因为script标签内无法解析HTML实体编码。
2个tips:
1、 在<textarea>和<title>的内容中不会创建标签,不会有脚本能够执行,结果是这样:

所以遇到输出在<textarea>之间的情况,如果不能使用</textarea>闭合,就要提早放弃。
2、 <svg>属于外部标签,是一种特殊的标签,它使用XML格式定义图像,支持XML解析。因为xml支持在标签内解析HTML实体字符,所以在XML中(会被解析成(,<svg><script>alert('1')</script>是可以被解析的。

前端技术繁多,还有很多其他小tips,在这里不一一列举。
3.5 实例解析
例1:
根据上面讲的浏览器解码顺序,我们可以提交payload:
<img src="1"onerror=\u0061\u006c\u0065\u0072\u0074('\u0031')>
第一步 在html中所以解析成:
<img src="1"onerror=u0061u006cu0065u0072u0074('u0031')>
第二步 因为事件触发函数后的字符串也是js代码,经过js解码成:
<img src="1" onerror=1>
例2:
当下大多数网站对xss的防御是对用户输入使用html实体编码,大多数情况下可以达到效果,但有些场景下并不能生效,一种经典的情况就是,服务器将用户输入的htmlencoded值直接动态输出到客户端javascript(事件处理器)中:
<input value=<%= HtmlEncode(input)%> id=textbox>
如果input处用户输入a onclick=alert(document.cookie),结果会输出为:
<input value=aonclick=alert(document.cookie) id=textbox>
很容易理解,根据上述讲到的浏览器解析顺序,用户输入——>后台代码编码——>浏览器(HTML解析器)解析——>传到js代码执行。
下面这个也是在现实渗透中发现的一个案例,核心也是在经过<input>解码为value值后传递给了innerHTML,将其二次解析成HTML格式内容。所以总结起来绕过这种编码的关键就是需要注意输入输出的上下文,看输入是否进行了二次传递、处理。

4、xss的挖掘和绕过思路
xss的挖掘思路的核心是:关注“输入”和“输出“”,也是大多数漏洞产生和挖掘的核心。基本的思路和流程如下:

简单来讲分为三步:
1、探测输出点
输入处使用容易辨识的特征值明确输出点,如“aaaaa”,“11111”等等都可以,确定是单点输出、多点输出,以及是否存在二次输出的情况。
2、根据HTML结构构造payload
这一步需使用第二章的内容,明确输出点的位置在HTML标签文本内、标签属性中、标签事件中、<script>标签内、function函数变量中等等。这里需要清楚DOM结构和标签的优先级,有时候输出可能在不同层级的标签内,就像下面的输出就既可以选择闭合内部的function()函数,也可以直接闭合更高一级的<script>。

通常构造payload流程也有三步:
- 闭合输入前的标签;
- 使弹框语句正常执行;
- 处理剩下的字符;
对应上面这个案例,对function函数进行处理:
- 闭合前面的成对字符串:123";}
- 输入弹框语句,因为在<script>内,所以输入confirm(/Jayway/);
- 遗留“;无法注释,可以复制之前的结构语句使其正确: function b(){a=",最终payload为123";}confirm(/Jayway/);function b(){a="z

最终构造成功的结构其实为<script>标签内并列的两个自定义function和一个confirm,当然这里直接对<script>进行处理更为简便。
3、检测过滤及绕过
如果系统对输入做了过滤,我们可以通过各种方法进行绕过,当然这篇文章说的编码绕过只是其中一种方法,过滤的情况也不尽相同,有对尖括号、圆括号、引号等字符的过滤,有对alert、script关键字的过滤,还有对于组合的正则过滤。
在黑盒测试的时候,可以通过intruder模块对于各种关键字进行fuzzing测试,确定后端的过滤机制,然后对于不同的过滤采取不同的绕过手段,但前提是要根据浏览器的解析和解码原理针对性地进行构造。
比如过滤了()可以用反引号“代替;过滤了script用img等标签;过滤了<>看是不是可以用事件触发,过滤了alert可以用confirm、throw、prompt。这部分涉及内容太多,有空再集中做个汇总。
对xss的精通程度取决于对浏览器机制和HTML、JS的了解程度,在理解本文的基础上对常见的标签和事件进行学习补充,在xss练习平台上加以练习,一般的xss都可以挖掘得到。