今天在给客户做渗透测试时,遇到了一处XSS。虽然很简单,但有点小意思。引发了我对js转义和html转义在XSS中的思考,故做个小笔记记录一下。两个例子都会以仿写现实场景的代码的说明问题。
0x01 一点知识贮备
1.1 关于转义
&#**;
格式的字符串是html的转义字符,\
是JS的转义符,转义的目的就是告诉解析器该符号为字符,而不是代码,防止代码出现歧义。
1.2 浏览器解析原则
- 若果存在html转义字符串,HTML解析引擎会先把转义字符解析为字符串
- HTML解析引擎按照从上到下,从外向里解析html标签
- 遇到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
| javascript:alert(1);
|
我们将<script>alert(1);</script>
html转义后得到如下字符,并填写到str2输入框
1
| <script>alert(1);</script>
|
提交后发现点击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='javascript:alert(1);'>str1</a><br/>str2:<script>alert(1);</script> <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:<script>alert(1);</script><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解析器解码后为<script>alert(1);</script>
,而不是<script>alert(1);</script>
,所以js代码自然无法执行。所以str2应该为<script>alert(1);</script>
,才可以触发XSS。
0x04 总结
通过对js转义和html转义在XSS中的应用,让我对浏览器解析html代码的解析过程有了更深的了解。可以借鉴其中的原理来构造更简洁,精巧的XSS的payload,也可以尝试用来绕waf。
参考文章
XSS绕过学习