从XSS Payload学习浏览器解码
作为一个浏览器在解析一篇HTML文档时主要有三个处理过程,每个解析器负责解码和解析HTML文档中它所对应的部分,下面我将按照解码顺序依次讲解。
- HTMl解析
- URL解析
- JavaScript解析
HTML解析
一个HTML解析器作为一个状态机,它从输入流中获取字符并按照转换规则转换到另一种状态。在解析过程中,任何时候它只要遇到一个’<‘符号(后面没有跟’/'符号)就会进入“标签开始状态(Tag open state)”。然后转变到“标签名状态(Tag name state)”,“前属性名状态(before attribute name state)”…最后进入“数据状态(Data state)”并释放当前标签的token。当解析器处于“数据状态(Data state)”时,它会继续解析,每当发现一个完整的标签,就会释放出一个token。
用代码表示如下
存在<h1>hhhhhhhh</h1>
首先状态机吃下< 进入标签开始状态然后转变到标签名状态开始匹配标签
当吃进去/>标签的时候进入标签结束状态然后进入Data state状态
字符实体
字符实体是一个转义序列,它定义了一般无法在文本内容中输入的单个字符或符号。一个字符实体以一个&符号开始,后面跟着一个预定义的实体的名称,或是一个#符号以及字符的十进制数字。
HTML字符实体
在HTML中,某些字符是预留的。例如在HTML中不能使用“<”或“>”,这是因为浏览器可能误认为它们是标签的开始或结束。如果希望正确地显示预留字符,就需要在HTML中使用对应的字符实体。一个HTML字符实体描述如下:
字符引用
字符引用包括“字符值引用”和“字符实体引用”。在上述HTML例子中,‘<‘对应的字符值引用为’<’,对应的字符实体引用为‘<’。字符实体引用也被叫做“实体引用”或“实体”。)
现在你大概会明白为什么我们要转义“<”、“>”、“'” (单引号)和“"” (双引号)字符了。
这里要提一下RCDATA的概念。要了解什么是RCDATA,我们先要了解另一个概念。在HTML中有五类元素:
-
空元素(Void elements),如<area>,<br>,<base>等等
-
原始文本元素(Raw text elements),有<script>和<style>
-
RCDATA元素(RCDATA elements),有<textarea>和<title>
-
外部元素(Foreign elements),例如MathML命名空间或者SVG命名空间的元素
-
基本元素(Normal elements),即除了以上4种元素以外的元素
五类元素的区别如下:
-
空元素,不能容纳任何内容(因为它们没有闭合标签,没有内容能够放在开始标签和闭合标签中间)。
-
原始文本元素,可以容纳文本。
-
RCDATA元素,可以容纳文本和字符引用。
-
外部元素,可以容纳文本、字符引用、CDATA段、其他元素和注释
-
基本元素,可以容纳文本、字符引用、其他元素和注释
如果我们回头看HTML解析器的规则,其中有一种可以容纳字符引用的情况是“RCDATA状态中的字符引用”。这意味着在<textarea>和<title>标签中的字符引用会被HTML解析器解码。这里要再提醒一次,在解析这些字符引用的过程中不会进入“标签开始状态”。
,所以只能对HTML编码进行解码打印出文本,但不会执行解码后相应的字符引用。
另外,对RCDATA有个特殊的情况。在浏览器解析RCDATA元素的过程中,解析器会进入“RCDATA状态”。在这个状态中,如果遇到“<”字符,它会转换到“RCDATA小于号状态”。如果“<”字符后没有紧跟着“/”和对应的标签名,解析器会转换回“RCDATA状态”。这意味着在RCDATA元素标签的内容中(例如<textarea>或<title>的内容中),唯一能够被解析器认做是标签的就是“</textarea>”或者“</title>”。
因此,在“<textarea>”和“<title>”的内容中不会创建标签,就不会有脚本能够执行。
URL解析
URL的协议部分必须为ASCII字符,否则URL解析器的状态机将进入No Scheme状态
例如:
<a href="%6a%61%76%61%73%63%72%69%70%74:alert(1)"></a>
url编码部分为javascript,因为作为协议部分的"javascript"被编码,故不会触发JS
你不能对协议类型进行任何的编码操作,不然URL解析器会认为它无类型,该原则对协议后面的“:”(冒号)同样适用
<a href="javascript:%61%6c%65%72%74%28%32%29">
当HTML解析器工作完成后,URL解析器开始解析href属性值里的链接。
在这时,“javascript”协议已经被解码,它能够被URL解析器正确识别。
然后URL解析器继续解析链接剩下的部分。
由于是“javascript”协议,JavaScript解析器开始工作并执行这段代码。
这就是为什么代码能够被执行的原因。
其次,URL编码过程使用UTF-8编码类型来编码每一个字符。如果你尝试着将URL链接做了其他编码类型的编码,URL解析器就可能不会正确识别。
JavaScript解析
像\uXXXX一样的字符被称作Unicode转义序列。从上下文来看,你可以将转义序列放在3个部分:字符串中,标识符名称中和控制字符中。JavaScript能否解析这些字符来执行需要根据视情况而定。具体的说就是要看被编码的序列到底是哪部分。
字符串中:当Unicode转义序列存在于字符串中时,它只会被解释为正规字符,而不是单引号,双引号或者换行符这些能够打破字符串上下文的字符。因此,Unicode转义序列将永远不会破环字符串上下文,因为它们只能被解释成字符串常量。
例如:
<script>alert(“\u0031”);</script>
被编码的为1且是字符串,可以正常解码并触发执行
标识符名称中:当Unicode转义序列出现在标识符名称中时,它会被解码并解释为标识符名称的一部分,例如函数名,属性名等等。
例如:
<script>\u0061\u006c\u0065\u0072\u0074(1);</script>
被编码的部分为alert字符,是函数名,属于在标识符中的情况,因此会被正常解码并执行JS
控制字符:当用Unicode转义序列来表示一个控制字符时,例如单引号、双引号、圆括号等等,它们将不会被解释成控制字符,而仅仅被解码并解析为标识符名称或者字符串常量。如果你去看ECMAScript的语法,就会发现没有一处会用Unicode转义序列来当作控制字符。例如,如果解析器正在解析一个函数调用语句,圆括号部分必须为“(”和“)”,而不能是\u0028和\u0029。
例如:
<script>alert\u0028″xss”);</script>
\u0028会被解码为( 但是不会触发JS,因为是控制符
总的来说,Unicode转义序列只有在标识符名称里不被当作字符串,也只有在标识符名称里的编码字符能够被正常的解析。
案例解析
下面我将通过15个案例来复习上述所讲的内容
1.<a href="%6a%61%76%61%73%63%72%69%70%74:%61%6c%65%72%74%28%31%29">aaa</a>
无法弹窗,不能对协议类型进行任何的编码操作,不然URL解析器会认为它无类型(即被编码的javascript:
),所以导致解码失败,不会被执行。
2.<a href="javascript:%61%6c%65%72%74%28%32%29">
先HTML解码,得到
<a href="javascript:%61%6c%65%72%74%28%32%29">
href中为URL,URL模块可识别为javascript
协议,进行URL解码,得到
<a href="javascript:alert(2)">
由于是javascript协议,解码完给JS模块处理,于是被执行
3.<a href="javascript%3aalert(3)"></a>
和第一题一样url编码不能编码协议,所以导致解码失败,不会被执行。
4.<div><img src=x onerror=alert(4)></div>
<img src=x onerror=alert(4)>
从HTML解析机制看,在读取<div>
之后进入数据状态,<
会被HTML解码,但不会进入标签开始状态,只是将<文本打印出来,所以就不会创建img
元素,也就不会执行。
5.<textarea><script>alert(5)</script></textarea>
因为在RCDATA
元素中唯一能够被解析器认做是标签的就是“</textarea>”或者“</title>”。所有其他的元素都会将当成文本直接打印,不会执行。
6.<textarea><script>alert(6)</script></textarea>
和第五题一样所有其他的元素都会将当成文本直接打印,不会执行。
7.<button onclick="confirm('7');">Button</button>
这里onclick
中为标签的属性值(类比2中的href
),会被HTML解码,得到
<button onclick="confirm('7');">Button</button>
所以可以直接执行
8.<button onclick="confirm('8\u0027);">Button</button>
因为在JS中unicode解析只能解析字符串和标识符,而单引号双引号圆括号这些会影响字符串上下文的符号将不能被unicode编码。而这一题编码的是单引号,所以JS执行失败。
9.<script>alert(9);</script>
因为<script>
属于原始文本元素(Raw text elements),只可以容纳文本,注意没有字符引用,所以直接由JS处理,JS也认不出来,执行失败。
10.<script>\u0061\u006c\u0065\u0072\u0074(10);</script>
因为没有编码符号,函数名alert
属于标识符,所以直接将unicode编码直接解码,顺利被JS执行。
11.<script>\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0031\u0029</script>
由于编码了圆括号,所以导致JS执行失败。
12.<script>\u0061\u006c\u0065\u0072\u0074(\u0031\u0032)</script>
这里看似将没毛病,但是这里\u0031\u0032
在解码的时候会被解码为字符串12
,注意是字符串,不是数字,文字显然是需要引号的,JS执行失败
13.<script>alert('13\u0027)</script>
和第八题一样,JSunicode编码不能编码符号。
14.<script>alert('14\u000a')</script>
因为\u000a
在JavaScript里是换行,就是\n
,所以代码会直接执行,但不会打破字符串上下文,所以正常执行JS代码。
15.<a href="javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(15)"></a>
先HTML解码,得到
<a href="javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(15)"></a>
在href中由URL模块处理,解码得到
javascript:\u0061\u006c\u0065\u0072\u0074(15)
识别JS协议,然后由JS模块处理,解码得到
javascript:alert(15)
最后被正常执行
总结
<script>
和<style>
数据只能有文本,不会有HTML解码和URL解码操作<textarea>
和<title>
里会有HTML解码操作,但不会有子元素- 其他元素数据(如
div
)和元素属性数据(如href
)中会有HTML解码操作 - 部分属性(如
href
)会有URL解码操作,但URL中的协议需为ASCII - JavaScript会对字符串和标识符Unicode解码
根据浏览器的自动解码,反向构造 XSS Payload 即可