【DVWA】CSRF详解
一、low级别
1、测试流程
如图,输入一次新密码和一次确认新密码就可更改密码,并没有索要旧密码进行验证。
用burp抓包看一下:
发现是get请求,而且并没有token,所以明显存在CSRF漏洞。
伪造一个请求链接:
http://222.24.28.118/dvwa/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change
将password_new和password_conf参数的值改为123456,然后将此链接发送给攻击目标,如果目标在此网站处于登录状态并且点击此链接,则会将该用户的密码重置为123456
模拟用户点击链接:
在虚拟机中登录DVWA,并将安全等级设置为low,然后访问:
http://222.24.28.118/dvwa/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change
如图所示,密码已被更改。
值得注意的是,用户必须用登录此网站的浏览器访问该链接,攻击才会成功。因为后端会校验cookie值,如果换浏览器请求数据包将无法获取cookie,后端的cookie会校验失败,从而跳转到登录页面攻击就会失效。看来CSRF攻击的关键就是利用受害者的cookie向服务器发送伪造请求。
但是,这样的攻击方式是十分简陋的,用户可以直接看到链接是一个更改密码的链接从而心有防备。
现实中,攻击者往往会先搭建一个站点,然后上传一个html文档,该文档中含有恶意的链接。让后将这个html文档的地址发送给用户,用户一旦点击将会自动加载恶意链接完成攻击。
2、源码分析
<?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)) ? "" : "")); //防SQL注入
$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);
}
?>
可以看到,后端仅仅检查change参数是否设置,以及password_new和password_conf是否相等做了判定,而并没有做任何防CSRF的措施。
二、medium级别
1、测试流程
按照上面的方法我们做一下测试,发现报错了:
我们直接看一下源码
2、源码分析
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) { //检测Referer中是否有host
// 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);
}
?>
stripos(strs, str)
函数:返回字符str在字符串strs中的位置
HTTP_REFERER表示数据包中的referer字段(表示数据包的来源链接),SERVER_NAME表示数据包中的host(要访问的主机地址),所以后端会检测看referer中是否有host,如果有才会通过。
绕过思路:直接将low级别制作的html文档名称改为[host].html,这样referer中就会存在host,用户一旦点击就会绕过检测攻击成功!
三、high级别
### 1、测试流程
访问恶意链接,报错CSRF token is incorrect.
看来是用了token防范
2、源码分析
<?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();
?>
high级别中加入了Anti-csrf token机制,由checkToken函数来实现,用户每次访问更改密码页面时,服务器会返回一个随机的token,之后每次向服务器发起请求之前需要获取服务器返回的user_token,利用user_token绕过验证。
可以使用burp的一个插件CSRF Token Tracker:
设置host和name
然后抓包重放就行
四、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();
?>
impossible级别同样采用了Anti-CSRF token机制,有效的防御了CSRF攻击,而且采用PDO预处理和参数化技术预防了sql注入。
五、防范建议
- 验证请求的Referer值,如果Referer是以自己的网站开头的域名,则说明该请求来自网站自己,是合法的,如果Referer是其他网站域名或空白,就有可能是CSRF攻击,那么服务器应拒绝该请求,但此方法存在被绕过的可能。
- 设置token机制,使伪造请求变得困难。