目录
1.原理介绍
CSRF(Cross-site request forgery)跨站请求伪造,也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF
原理图
关键处
受害者
1.登录受信任网站A,并在本地生成Cookie
2.在不登出A的情况下,访问危险网站B
防御
1.生成token值
2.验证码
3.验证HTTP Referer字段
4.验证原密码
2.实验
2.1 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);
}
?>
变量分析
$GLOBALS:引用全局作用域中可用的全部变量(一个包含了全部变量的全局组合数组。变量的名字就是数组的键),$GLOBALS在PHP代码中任何地方总是可用的
注:$GLOBALS['var']是外部的全局变量本身,而global $var是外部$var的同名引用或者指针,也就是说global在函数产生一个指向函数外部变量的别名变量,而不是真正的函数外部变量,而$GLOBALS[]确确实实调用的是外部的变量,函数内外会始终保持一致
函数分析:
- mysqli_real_escape_string(connection,escapestring);,根据当前连接的字符集,对于 SQL 语句中的特殊字符进行转义((\x00、\n、\r 、\、'、"、\x1a))
connection | 必需。规定要使用的 MySQL 连接。 |
escapestring | 必需。要转义的字符串。编码的字符是 NUL(ASCII 0)、\n、\r、\、'、" 和 Control-Z。 |
- is_object ( mixed
$var
) : bool,如果var
是一个 object 则返回true
,否则返回false
。检测变量是否是一个对象
源码分析:
通过GET方法,获取旧密码和新密码,对用户输入的两个密码进行判断,看是否相等。不相等就提示密码不匹配,相等的话,就使用mysqli_real_escape_string函数去转义一些字符,如果不是的话输出错误。然后再用md5加密
利用漏洞
我们随便在内容框里输入点东西,会有一个连接,如果我们将这个连接复制下来,将参数修改为相等的,然后发送给受害者,受害者也同样处于登陆状态
比如http://192.168.31.51/DVWA/vulnerabilities/csrf/?password_new=234&password_conf=234&Change=Change#,受害者打开这个连接密码就会修改成功。
2.2Medim
<?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);
}
?>
对比:
与LOW级别的相比,多了一个if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) 判断,加上了对用户请求头中的Referer字段进行验证
函数分析:
stripos(string,find,start)
查找字符串在另一字符串中第一次出现的位置(不区分大小写),如果没有找到字符串则返回 FALSE。
//查找 "php" 在字符串中第一次出现的位置:
<?php
echo stripos("You love php, I love php too!","PHP");
?>
//结果 9
改变代码分析:
用户的请求头中的Refer字段必须包含了服务器的名字。
HTTP Referer字段,“HTTPReferer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。”
漏洞利用
如果我们还像第一次那样做的话,就会提示错误
这个时候,就是拿出身体burpsuite来做代理测试的时候了。
可以看到正常操作的话会有refer值
打开另一个连接后,输入url,没有refer
我们可以自己添加Refer,只要包含主机头192.168.31.51就可以,如果是域名,需要包含域名,看下图,我们已经成功绕过Referer
2.3 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();
?>
与Low等级比较增加的代码
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Generate Anti-CSRF token
generateSessionToken();
代码分析:
High级别加入了Anti-CSRF token机制,用户每次访问修改密码的页面时,服务器都会返回一个随机的token,当浏览器向服务器发起请求是,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。
增加token验证,比验证refer更加的安全。
在Get请求中,我们可以在url中看到token值
复现攻击
因为加入了token,所以我们就需要获取token,怎么获取到token是需要解决的问题。
想获取token,就必须利用用户的cookie值去访问修改密码的页面,然后截取服务器返回的token值,才能构造url进行csrf攻击
参考网上很多人的资料,说结合XSS进行,我在想既然可以使用XSS漏洞进入数据库了,我们还在这csrf搞啥,哈哈哈哈,不过确实是一种办法。
这里还涉及到同源策略和跨域的操作,可自行百度了解。
由于跨域是不能实现的,所以我们要将攻击代码注入到目标服务器中,利用High级别的XSS漏洞协助获取Anti-CSRF token(因为这里的XSS注入有长度限制,不能够注入完整的攻击脚本,所以只获取Anti-CSRF token)。
<iframe src="../csrf" onload=alert(frames[0].document.getElementsByName('user_token')[0].value)>
2.4Impossible
<?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();
?>
可以看到,在high的等级下,加入了确认原来密码的策略
// Get input
$pass_curr = $_GET[ 'password_current' ];
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
而我们并不知道原来的密码,所以这里暂时不存在CSRF了。