文章目录
前言
浏览器解析
解析规则
字符编码
解析结果
转义实例
JS 转义
Html转义
自动转义
进阶示例
解析器原理
Html解析器
JS 解析器
URL解析器
解析顺序
总结
前言
作为 Web 漏洞家族常见的一员 —— XSS 跨站脚本攻击的身影依旧活跃在各类网站,而特殊字符转义作为 XSS 漏洞的主要修复和防御手段,被开发人员广泛应用。本文目的在于通过具体的代码实例,从浏览器的解析规则出发,分析 HTML 转义、JS 转义防御 XSS 攻击背后的原理,从而深入理解 XSS 漏洞。
浏览器解析
解析规则
浏览器在解析 HTML 文档时无论按照什么顺序,主要有三个过程:HTML 解析、JS 解析 和 URL 解析,每个解析器负责HTML文档中各自对应部分的解析工作。下面以一篇HTML文档解析来简单的讨论下解析器如何协同工作的。
首先浏览器接收到一个 HTML 文档时,会触发 HTML 解析器对 HTML 文档进行词法解析,这一过程完成 HTML解码 并创建 DOM 树;
接下来 JavaScript 解析器会介入对内联脚本进行解析,这一过程完成 JS 的解码工作;
如果浏览器遇到需要 URL 的上下文环境,这时 URL 解析器也会介入完成 URL 的解码工作。
URL 解析器的解码顺序会根据 URL 所在位置不同,导致在 JavaScript 解析器之前或之后解析。每个解析过程中也有许多细节,下面再做具体讨论。
字符编码
HTML字符实体:
在呈现 HTML 页面时,针对某些特殊字符如“<”或”>”直接使用,浏览器会误以为它们标签的开始或结束,若想正确的在 HTML 页面呈现特殊字符就需要用到其对应的字符实体。HTML 字符实体以& 开头 + 预先定义的实体名称,以分号结束,如“<”的实体名称为< 或以 & 开头+#符号以及字符的十进制数字(或者 十六 进制,都能解析),如”<”的实体编号为<。
JavaScript 编码:最常用的如 “\uXXXX” 这种写法为 Unicode 转义序列,表示一个字符,其中 xxxx 表示一个 16 进制数字,如”<” Unicode 编码为“\u003c”。
URL编码:%加字符的 ASCII 编码对于的 2 位 16 进制数字,如”/”对应的 URL 编码为%2f。
HTML 转义字符表 可以参考:
HTML在线编码转换 可以参考:
解析结果
简而言之,&#**;格式的字符串是 HTML 的转义字符,\是JS的转义符,转义的目的就是告诉解析器该符号为字符,而不是代码,防止代码出现歧义。
HTML 源代码 DOM 结构和浏览器解析后的 DOM 结构是不一样的:
在浏览器中我们右键查看网页源码(或者按住快捷键ctrl+u),看到的是服务器接受我们的请求后返回的 HTML 源码。
而按F12进入开发者工具面板,开发者工具分析出的 DOM 结构,就是浏览器的解析结果。
转义实例
JS 转义
来看一段测试代码 test1.php:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
将其部署在 phpstudy 的 WWW 文件夹下:
启动 phpstudy,访问测试页面:
当我们提交,前端并没有出现我们期待的弹窗,而是输出了以下字符串:
而当我们提交 提交后,得到的 html 源码如下:
当我们的 HTM L解析器解析到 。
这时它查找到是8行中的,而不是12行的。
这时和12行的之间的代码被当成字符串输出到前端页面。
而由于6行标签没有配对成功,故不会被浏览器解析为一个合法标签。 所以最终的解析结果是第8行的被解析为html标签。
故最终浏览器解析后的 DOM 结构如下:
当我们输入第二个 payload:。在到第8行时发现<\/script>标签,而不是,故继续往下,直到找寻到12行的标签,才完成了配对。
这时8行和11行的代码交给了 js 引擎去解析。 而这时又因为\为 js 语法中的转义字符,故在 js 解析引擎解析时,又能正常解析input_str变量的值为字符串,所以最总成功弹窗,很巧妙!
故最终浏览器解析后的 DOM 结构如下:
从上面实例我们可以具体了解到服务端返回的源码的 DOM 结构与浏览器解析后的 DOM 结构的差异,以及 JS 转义的巧妙作用!
Html转义
来看看另一段测试代码 test2.php:
<?php
if(isset($_POST['submit'])){
echo " str1";
echo "
";
echo "str2:".$_POST['str2'];
}
?>
str1:
str2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
同样放在 phpstudy 网站目录下,浏览器访问:
我们将javascript:alert(1);进行 html 转义(字符的十六进制)得到如下字符串,并填写到 str1 输入框:
javascript:alert(1);
1
同时将进行 html 转义后得到如下字符,并填写到str2输入框:
<script>alert(1)</script>
1
提交后发现点击 str1 链接可以弹框,说明前者被当代码来执行了,而后者被当字符串输出了:
如何解释以上结果?
原理分析
提交 payload 之后,服务器返回的 html 源码代码如下:
而浏览器 html 解析器解析后的结果如下:
从上面可以看到,两个 payload 其实在浏览器的 HTML 解析器解析之后都被当成了字符输出了。
只是当用户再次点击 str1 的链接时,前者被解码之后的字符会被浏览器的 JS 解析器当成代码执行了。
而后者<script>alert(1);</script>被浏览器 html 解析器解码后已经被当作一个普通字符串而非 js 代码(请重点注意此时被浏览器解析后的结果是跟 str2:一起被包围在双引号里当作一个字符串显示),所以已无法被执行。
可以进一步来看看 str2 直接输入的话是什么效果,答案是会触发弹框:
进一步看看浏览器的解析结果:
此时,已经不是跟“str2:"一样被当作字符串输出了。
由上面的实例分析,可以清晰地看到 HTML 转义字符对于 XSS 防御的意义,它会告诉浏览器将一些危险字符当作普通字符串输出,而不是当作 js 代码执行。
自动转义
PHP 语言里,我们在服务端可以使用htmlspecialchars($str);函数对用户传输过来的客户端输入值进行 HTML 转义。
htmlspecialchars($str);函数只转换5个字符,即:和号(&),双引号(“),单引号(‘),小于(<),大于(>),将它们转换为 HTML 实体形式,输出时浏览器会自动还原并将其当成普通字符输出的。
利用htmlspecialchars($str);我们对前面 JS 转义的测试代码 test1.php 进行变更:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
此时再在浏览器输入 payload:这样字符实体并不会被解码,也就不会执行JS。
2、RCDATA
在HTML中,属于 RCDATA Elements 的标签有两个:textarea、title。RCDATA Elements 类型的标签可以包含文本内容和字符实体。解析器解析到 textarea、title 标签的数据部分时,状态会进入RCDATA State。前面我们提到,处于RCDATA State状态时,字符实体是会被解析器解码的。
示例:
<script>alert(5)</script>
<和>被编码为实体<和>。
解析器解析到它们时会进行解码,最终得到<script>alert(5)</script>。
但是里面的JS同样还是不会被执行,原因还是因为解码字符实体状态机不会进入标签打开状态(Tag Open State),
因此里面的
不能弹窗,Raw text elements类型标签下的所有字符实体编码都不会被HTML解码
能弹窗,在XML中,(会被解析成(,在XML中实体会自动转义,除了<[CDATA[和]]>包含的实体
1
2
3
4
JS 解析器
形如 \uXXXX这样的 Unicode 字符转义序列或 Hex 编码是否能被解码需要看情况。
首先,JavaScript 中有三个地方可以出现 Unicode 字符转义序列:
字符串中
Unicode 转义序列出现在字符串中时,它只会被解释为普通字符,而不会破坏字符串的上下文。例如,被编码转义的部分为10,是字符串,会被正常解码,JS代码也会被执行。
标识符中
若 Unicode 转义序列存在于标识符中,即变量名(如函数名等…),它会被进行解码。例如:,被编码转义的部分为 alert 字符,是函数名,属于在标识符中的情况,因此会被正常解码,JS代码也会被执行。
控制字符中
若 Unicode 转义序列存在于控制字符中,那么它会被解码但不会被解释为控制字符,而会被解释为标识符或字符串字符的一部分。控制字符即'、"、()等。例如,,其中(进行了Unicode编码,那么解码后它不再是作为控制字符,而是作为标识符的一部分alert(。因此函数的括号之类的控制字符进行 Unicode 转义后是不能被正常解释的。
总结,Unicode序列不能出现在控制字符中,否则不能被解释。
示例1:
被编码部分为alert(11)。
该例子中的JS不会被执行,因为控制字符被编码了。
1
2
3
示例2:
被编码部分为alert及括号内为12。
该例子中JS不会被执行,原因在于括号内被编码的部分不能被正常解释,要么使用ASCII数字,要么加""或''使其变为字符串,作为字符串也只能作为普通字符。
1
2
3
示例3:
被编码处为'。
该例的JS不会执行,因为控制字符被编码了,解码后的'将变为字符串的一部分,而不再解释为控制字符。
因此该例中字符串是不完整的,因为没有'来结束字符串。
1
2
3
4
示例4:
该例的JS会被执行,因为被编码的部分处于字符串内,只会被解释为普通字符,
不会突破字符串上下文。
1
2
3
示例5:
无法执行
我们以浏览器的视角来看:首先读到<开始读取标签,然后读到onerror调用JS解析器。在JS中,单引号,双引号和圆括号等属于控制字符,
编码后将无法识别。所以对于防御来说,应该编码这些控制字符。
下面这种方式可以解析:
可以结合上面的HTML编码
按照解析顺序反过去,先JS编码然后HTML解码
浏览器读到了<标签开始构造语法树,然后HTML解码,解码之后发现onerror于是进行一个JS解码,成功弹窗
延伸:
开发人员单纯的设置HTML实体编码为防御xss的手段,但是用户输入点在alert中
如果用户正常输入的话凡是存在< ," 等都能被转码
攻击者可以通过语句 ");alert("test,在服务端被转码:
弹窗两次,是因为浏览器进行HTML解码发现存在两个alert()
所以对于这种情况,正确防御XSS的方法:
应该是先JavaScript编码然后再进行HTML编码
用户输入 ");alert("test 后在服务端先JavaScript编码然后再进行HTML编码
在
在浏览器端:
首先经过第一步HTML解码后变为\u0022\u0029\u003B\u0061\u006C\u0065\u0072\u0074\u0028\u0022\u0074\u0065\u0073\u0074
JavaScript解析器工作,变为 ");alert("test ,刚才已经讲过JavaScript解析时只有标识符名称不会被当做字符串,
控制字符仅会被解析为标示符名称或者字符串,因此\u0022被解释成双引号文本,\u0028和\u0029被解释成为圆括号文本,不会变为控制字符被解析执行。
在这里采用的先JS编码后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
26
27
28
29
30
URL解析器
通用URI的格式如下:
1
URL 解析器也被建模为状态机,文档输入流中的字符可以将其导向不同的状态。首先,要注意的是 URL 的 Scheme 部分(协议部分)必须为 ASCII 字符,即不能被任何编码,否则URL解析器的状态机将进入No Scheme状态。
示例:
URL编码部分的是javascript:alert(1)。
JS不会被执行,因为作为Scheme部分的"javascript"这个字符串被编码,导致URL解析器状态机进入No Scheme状态。
1
2
3
URL中的:也不能被以任何方式编码,否则URL解析器的状态机也将进入 No Scheme 状态。
由于:被URL编码为%3a,导致URL状态机进入No Scheme状态,JS代码不能执行。
1
2
示例:
"javascript"这个字符串被实体化编码,:没有被编码,alert(2)被URL编码。
成功执行
首先,在HTML解析器中我们谈到过,HTML状态机处于属性值状态(Attribute Value State)时,字符实体时会被解码的,此处在href属性中,所以被实体化编码的"javascript"字符串会被解码。其次,HTML解析是在URL解析之前的,所以在进行URL解析之前,Scheme部分的"javascript"字符串已被解码,而并不再是被实体编码的状态。
1
2
3
4
解析顺序
首先浏览器接收到一个 HTML 文档时,会触发 HTML 解析器对 HTML 文档进行词法解析,这一过程完成 HTML 解码并创建 DOM 树。
接下来 JavaScript 解析器会介入对内联脚本进行解析,这一过程完成J S的解码工作。
如果浏览器遇到需要 URL 的上下文环境,这时 URL 解析器也会介入完成 URL 的解码工作,URL 解析器的解码顺序会根据 URL 所在位置不同,可能在 JavaScript 解析器之前或之后解析。但HTML解析总是第一步。
URL 解析和 JavaScript 解析,它们的解析顺序要根据情况而定。
示例1:
该例子中,首先由HTML解析器对UserInput部分进行字符实体解码;
接着URL解析器对UserInput进行URL decode;
如果URL的Scheme部分为javascript的话,JavaScript解析器会再对UserInput进行解码。
所以解析顺序是:HTML解析->URL解析->JavaScript解析。
1
2
3
4
5
示例2:
该例子中,首先由HTML解析器对UserInput部分进行字符实体解码;
接着由JavaScript解析器会再对onclick部分的JS进行解析并执行JS;
执行JS后window.open('UserInput')函数的参数会传入URL,所以再由URL解析器对UserInput部分进行解码。
因此解析顺序为:HTML解析->JavaScript解析->URL解析。
1
2
3
4
5
示例3:
该例子中,首先还是由HTML解析器对UserInput部分进行字符实体解码;
接着由URL解析器解析href的属性值;
然后由于Scheme为javascript,所以由JavaScript解析;
解析执行JS后window.open('UserInput')函数传入URL,所以再由URL解析器解析。
所以解析顺序为:HTML解析->URL解析->JavaScript解析->URL解析。
1
2
3
4
5
6
综合实例:
1
2
3
4
5
6
7
8
9
10
11
首先 HTML 解析器进行解析,解析到 href 属性的值时,状态机进入属性值状态(Attribute Value State),该状态会解码字符实体。接着由URL解析器进行解析并解码,再接着由于 Scheme 为 javascript,因此由 JavaScript 解析器解析并解码,加上编码部分是函数名,属于标识符,因此可以正常解码解释。经过三轮解析解码后得到结果:。
总结
本文参考文章:
XSS与字符编码及浏览器解析原理;
XSS中的JS转义和HTML转义 ;
【应用安全——XSS】字符编码绕过;
深入探究浏览器编码及XSS Bypass ;
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_39190897/article/details/113198859