2019-11-09
3.4k
Web安全学习之XSS漏洞实战学习
- 编写一个存在 xss 漏洞的页面
- 利用 xss 漏洞获取当前用户 cookie
- 思考,利用 xss 漏洞能干嘛(参考 beef)
- 扩展学习:学习如何防御 xss、总结防御策略加到报告里
要求:记录编写的页面代码、操作流程、思考总结
0x01 XSS漏洞
XSS(Cross Site Scripting)跨站脚本攻击,也是一种注入攻击,当web应用对用户输入过滤不严格,攻击者写入恶意的脚本代码(HTML、JavaScript)到网页中时,如果用户访问了含有恶意代码的页面,恶意脚本就会被浏览器解析执行导致用户被攻击。
常见的危害有:cookie窃取,session劫持,钓鱼攻击,蠕虫,ddos等。
0x02 XSS漏洞分类和代码案例
反射型XSS
原理
反射型xss一般出现在URL参数中及网站搜索栏中,由于需要点击包含恶意代码的URL才可以触发,并且只能触发一次,所以也被称”非持久性xss”。
代码案例
-
Low Level
漏洞代码:
1 2 3 4 5 6 7 8 9
| <?php
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) { echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>'; } setcookie("Cookie", "Test"); ?>
|
漏洞利用:

-
Medium Level
漏洞代码:
1 2 3 4 5 6 7 8 9 10
| <?php
if (array_key_exists("name", $_GET) && $_GET['name'] != NULL) { $name = str_replace('<script>', '', $_GET['name']);
echo "<pre>Hello {$name}</pre>"; } setcookie("Medium", "CookieTest");
?>
|
漏洞利用:
- 双写
<script>
标签:<s<script>cript>
- 标签转换大小写:
<SCRipt>

-
High Level
漏洞代码:
1 2 3 4 5 6 7 8 9 10 11 12
| <?php // Is there any input? if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) { // Get input $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] ); // Feedback for end user echo "<pre>Hello ${name}</pre>"; } ?>
|
漏洞利用:
这里使用了正则表达式过滤了<script>
标签, 这时候不论是大小写、双层<script>
都无法绕过,此时可以使用别的标签,比如<img>
。
payload: <img src=0 onerror=alert(document.cookie)>

存储型XSS
原理
允许用户提交数据的Web应用程序都有可能会出现存储型XSS漏洞,当攻击者提交一段XSS代码后,被服务器接收并存储,当攻击者再次访问某个页面时,这段XSS代码被程序读出来响应给浏览器,造成XSS跨站攻击,这就是存储型XSS。
代码案例
-
Low Level
前端页面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <form method="post" name="guestform" action="low.php"> <table width="550" border="0" cellpadding="2" cellspacing="1"> <tr> <td width="100">Name *</td> <td> <input name="txtName" type="text" size="30" maxlength="10"></td> </tr> <tr> <td width="100">Message *</td> <td> <textarea name="mtxMessage" cols="50" rows="3" maxlength="50"></textarea></td> </tr> <tr> <td width="100"> </td> <td> <input name="btnSign" type="submit" value="Sign Guestbook"></td> </tr> </table> </form>
|
服务器端代码:
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <?php
error_reporting(E_ALL);
if(isset($_POST['btnSign'])) {
$servername = "localhost"; $username = "root"; $password = "root"; $dbname = "security"; $conn = new mysqli($servername, $username, $password, $dbname); if ($conn->connect_error) { die("数据库连接失败: " . $conn->connect_error); } else echo "数据库连接成功"; echo "<hr>";
echo $_POST['mtxMessage'];
$message = trim( $_POST[ 'mtxMessage' ] ); $name = trim( $_POST[ 'txtName' ] );
$message = stripslashes( $message ); $message = mysqli_real_escape_string( $message );
$name = mysqli_real_escape_string( $name ); $sql = "INSERT INTO guestbook (comment,name) VALUES ('$message','$name');";
echo "SQL插入语句:".$sql;
echo "<hr>"; if ($conn->query($sql) === TRUE) { echo "新记录插入成功"; } else { echo "Error: " . $sql . "<br>" . $conn->error; }
$conn->close(); } ?>
|
相关函数介绍:
trim(string,charlist)
函数移除字符串两侧的空白字符或其他预定义字符,预定义字符包括、\t、\n、\x0B、\r以及空格,可选参数charlist支持添加额外需要删除的字符。
mysqli_real_escape_string(string,connection)
函数会对字符串中的特殊符号(\x00,\n,\r,\,‘,“,\x1a)进行转义。
stripslashes(string)
函数删除字符串中的反斜杠。
可以看到,对输入并没有做XSS方面的过滤与检查,且存储在数据库中,因此这里存在明显的存储型XSS漏洞。
对Message
字段注入XSS代码:


由于Name字段在前端有字数限制,可以直接使用Burp Suite抓包修改参数。
-
Medium Level
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| <?php
error_reporting(E_ALL);
if(isset($_POST['btnSign'])) {
$servername = "localhost"; $username = "root"; $password = "root"; $dbname = "security"; $conn = new mysqli($servername, $username, $password, $dbname); if ($conn->connect_error) { die("连接失败: " . $conn->connect_error); } else echo "数据库连接成功"; echo "<hr>";
echo $_POST['mtxMessage'];
$message = trim( $_POST[ 'mtxMessage' ] ); $name = trim( $_POST[ 'txtName' ] );
$message = strip_tags( addslashes( $message ) ); $message = mysqli_real_escape_string( $message ); $message = htmlspecialchars( $message );
$name = str_replace( '<script>', '', $name ); $name = mysqli_real_escape_string( $name ); $sql = "INSERT INTO guestbook (comment,name) VALUES ('$message','$name');";
echo "SQL插入语句:".$sql;
echo "<hr>";
if ($conn->query($sql) === TRUE) { echo "新记录插入成功"; } else { echo "Error: " . $sql . "<br>" . $conn->error; }
$conn->close(); }
?>
|
相关函数介绍:
htmlspecialchars
:将特殊字符转换为 HTML 实体
字符 |
替换后 |
& |
& |
" |
",除非设置了 ENT_NOQUOTES |
’ |
设置了 ENT_QUOTES 后, '(如果是 ENT_HTML401 ) ,或者 '(如果是 ENT_XML1 、 ENT_XHTML 或 ENT_HTML5 )。 |
< |
< |
> |
> |
可以看到,由于对message参数使用了htmlspecialchars函数进行编码,因此无法再通过message参数注入XSS代码,但是对于name参数,只是简单过滤了<script>
字符串,仍然存在存储型的XSS。跟反射型XSS一样,可以抓包修改参数使用双写绕过或大小写混淆绕过。
-
High Level
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <?php
error_reporting(E_ALL);
if(isset($_POST['btnSign'])) {
$servername = "localhost"; $username = "root"; $password = "root"; $dbname = "security"; $conn = new mysqli($servername, $username, $password, $dbname); if ($conn->connect_error) { die("连接失败: " . $conn->connect_error); } else echo "数据库连接成功"; echo "<hr>";
echo $_POST['mtxMessage'];
$message = trim( $_POST[ 'mtxMessage' ] ); $name = trim( $_POST[ 'txtName' ] );
$message = strip_tags( addslashes( $message ) ); $message = mysqli_real_escape_string( $message ); $message = htmlspecialchars( $message );
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name ); $name = mysqli_real_escape_string( $name ); $sql = "INSERT INTO guestbook (comment,name) VALUES ('$message','$name');";
echo "SQL插入语句:".$sql;
echo "<hr>"; if ($conn->query($sql) === TRUE) { echo "新记录插入成功"; } else { echo "Error: " . $sql . "<br>" . $conn->error; }
$conn->close(); }
?>
|
可以看到,这里使用正则表达式过滤了<script>
标签,但是却忽略了img
、iframe
等其它危险的标签,因此name参数依旧存在存储型XSS。 同样抓包修改name参数为 <img src=1 onerror=alert(\xss\)>
DOM型XSS
原理
用户请求一个经过专门设计的URL,它由攻击者提交,而且其中包含XSS代码。服务器的响应不会以任何形式包含攻击者的脚本。当用户的浏览器处理这个响应时,DOM对象就会处理XSS代码,导致存在XSS漏洞。
由于DOM是在客户端修改节点的,所以基于DOM型的XSS漏洞不需要于服务器端交互,它只发生在客户端处理数据的阶段。
代码案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <!DOCTYPE html> <html> <head> <title>Test DOM_XSS</title> <script type="text/javascript"> function replace(){ document.getElementById("id1").innerHTML = document.getElementById("dom_input").value; } </script> </head> <body> <center> <h6 id="id1">这里会显示输入的内容</h6> <form action="" method="post"> <input type="text" id="dom_input" value="输入"><br> <input type="button" value="替换" onclick="replace()"> </form> <hr> </center>
</body> </html>
|

0x03 XSS漏洞防御
因为XSS漏洞涉及输入和输出两个部分,所以其修复也分为两种。
- 过滤输入的数据,包括
'
’,"
,<
,>
,on*
等非法字符。
- 对输入到页面的数据进行相应的编码转换,包括HTML实体编码、Javascript编码等。
HttpOnly
HttpOnly主要防御的是XSS漏洞中的Cookie劫持,浏览器禁止页面的JavaScript访问带有HttpOnly属性的Cookie。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <? php setcookie("cookie1", "test1", NULL, NULL, NULL, NULL, FALSE); setcookie("cookie2", "test2", NULL, NULL, NULL, NULL, TRUE); ?>
<html> <body> <script type="text/javascript"> alert(document.cookie); </script>
</body> </html>
|
这段代码中,cookie1没有HttpOnly,cookie2被标记为HttpOnly。
输入检查(XSS Filter)
在XSS的防御上,输入检查一般是检查用户输入的数据中是否包含一些特殊字符,如<、>、’、"等。如果发现存在特殊字符,则将这些字符过滤或者编码。比较智能的输入检查还会匹配XSS的特征,比如查找用户数据中是否包含了<script>
、javascript
等敏感字符。而且输入检查的逻辑必须放在服务器端代码实现。
但是XSS Filter有一个问题,就是对"<"、">"等字符的处理,可能会改变用户输入数据的语义。并且,输入的数据会被展示在多个地方,每个地方的语境各不相同,例如:
用户输入的昵称如下:
如果被XSS Filter转义后:
1 2
| $nickname = '我是\"天才\"';
|
如果在HTML代码中展示:
如果在JavaScript代码展示:
1 2 3
| var nick = '我是\"天才\"'; document.write(nick);
|
输出检查
一般来说,除了富文本的输出,在变量输入到HTML页面时,可以使用编码或转义的方式来防御XSS攻击。
HTMLEncode
针对HTML代码的编码方式是HtmlEncode,在HtmlEncode中要求至少转换:&
、<
、>
、"
、'
、\
。
在PHP中,有htmlentities()
和htmlspecialchars()
两个函数可以满足安全要求。
JavaScriptEncode
JavaScriptEncode需要使用\
对特殊字符进行转义,而且在对抗XSS漏洞时,还要求输出的变量必须在引号内部,例如:
1 2 3
| var x = escapeJavaScript($evil); var y = '"'+escapeJavaScript($evil)+'"';
|
如果输入的是1; alert(2)
,则:
1 2 3
| var x = 1; alert(2); var y = "1; alert(2)";
|
第一行执行了额外的代码,第二行则是安全的。
或者也可以使用更加严格的JavaScriptEncode函数来保证安全——除了数字和字母外的所有字符,都使用十六进制\xHH
的方式进行编码,例如:
1 2
| var x = 1\x3balert\x282\x29;
|
具体实例
在HTML标签中输出
1 2 3
| <div>$var</div> <a href=#>$var</a>
|
这种情况一般是构造一个<script>
标签,payload:
1 2 3
| <div><script>alert(xss)</script></div> <a href=#><img src=# onerror=alert(1)></a>
|
防御方式是对变量使用HtmlEncode。
在HTML属性中输出
1 2
| <div id="abc" name="$var"></div>
|
payload:
1 2 3 4
| <div id="abc" name=""> <script>alert(1)</script><""> </div>>
|
防御方式也是采用HtmlEncode。
在<script>标签中输出
在<script>标签中输出,首先应该确保输出的变量在引号中:
1 2 3 4
| <script> var x = "$var"; </script>
|
攻击者首先要先闭合引号才能实施XSS攻击:
1 2 3 4
| <script> var x = "";alert(/xss/); </script>
|
防御时使用JavaScriptEncode。
在事件中输出
1 2
| <a href=# onclick="funcA('$var')" >test</a>
|
payload:var = '); alert(/xss/);//
即:
1 2
| <a href=# onclick="funcA(''); alert(/xss/);//')" >test</a>
|
防御时使用JavaScriptEncode编码。
在CSS中输出
一般来说,尽可能禁止用户可控制的变量在”<style>标签“、”HTML标签的style属性“以及”CSS文件“中输出。
在地址中输出
一个URL组成如下:
1 2
| [Protocal][Host][Path][Search][Hash]
|
例如:
1 2 3 4 5 6 7
| https://www.evil.com/a/b/c/?abc=123#ssss [Protocal] = "https://" [Host] = "www.evil.com" [Path] = "/a/b/c/" [Search] = "?abc=123" [Hash] = "#ssss"
|
一般来说,在URL的path(路径)或者search(参数)中输出,使用URLEncode即可。URLEncode会将字符转换为”%HH“形式。
但在Protocal和Host中不能使用严格的URLEncode编码,因为会把://
、.
等都编码掉。
1 2
| <a href="$var" >Test</a>
|
攻击者可以使用伪协议实施攻击:
1 2
| <a href="javascript:alert(1);">Test</a>
|
对于Mozilla支持的dataURI
伪协议,它能够将一段代码写在URI中,例如:
1 2
| <a href="dataURI:test/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></a>
|
这段代买的意思时,以test/html的格式加载编码为base64的数据。加载完成后实际上时:
1 2
| <script>alert(1)</script>
|
对于这种手段的攻击,首先应该检查变量是否是以http
开头,如果不是则自动加上,再对变量进行URLEncode,即可保证没有此类的XSS攻击。