Web安全-CSRF跨站请求伪造
概述
CSRF攻击场景
GET
POST
由上面可见,对于CSRF来说,POST、GET请求是没有任何区别的,只不过POST请求方式多了一些代码。
浏览器Cookie机制
所以,安全研究人员在测试CSRF漏洞时,一定要考虑浏览器问题,每个浏览器的Cookie机制都不完全相同,像FireFox也可以发送Cookie。
假设 Http://wwwxxser.com/ 存在CSRF漏洞,用户登录后,Cookie存储在本地,当用户使用Chrome访问 Http://www.shuijiao8.com(带有xxer.com的CSRF POC)后,攻击将会成功,但是如果使用IE,则可能会失败,这样就可以看出浏览器Cookie机制的重要性。
检测CSRF漏洞
检测CSRF攻击主要分为两种:手工检测和半自动检测。这里并不是说没有全自动的CSRF检测工具,只是全自动CSRF工具的误报率较大,故不再介绍全自动检测工具。
手工检测
POC测试,即Proof of Concept,是业界流行的针对客户具体应用的验证性测试。
半自动检测
预防跨站请求伪造
在预防CSRF攻击时,不像其他漏洞那样复杂,你只需要在关键部分增加一些小操作就可以防御CSRF攻击。
二次确认
Token认证
DVWA-CSRF
CSRF(Cross-site request forgery),跨站请求伪造,简写 CSRF/XSRF。
指利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密等)。
LOW 安全级别
首先我们把安全级别设置为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);
}
?>
附:以上代码的查看方式为:在DVWA的安装目录下打开相应的网页资源文件。
从系统源码可以看到,服务器收到修改密码的请求后,会检查参数password_new与password_conf是否相同,如果相同,就会修改密码,并没有任何的防CSRF机制。即:
DVWA系统的Low级别的CSRF漏洞,无需确认原始密码,只需要用户在cookie还有效的时间内在相同的浏览器访问我们给定的URL,就可以实现CSRF攻击,修改用户密码,所以这里存在CSRF漏洞。
【漏洞利用】
注意:成功修改密码后,URL发生了变化。
【低级攻击】
所以,我们可以直接通过构造链接,诱导用户点击以下链接即可实现密码更改:
但是这样很拙劣……一眼就可以看出来,而且会跳转到提示密码更改成功的页面。在做渗透测试的时候可以用,但是实际的攻击中一般情况下是不会攻击成功的。
【进阶攻击】
那么咱们怎样才能诱导用户点击这个URL呢?我们已经可以预测所有参数,现在需要去构造请求。生成 text.html 文件(编写POC文件),搭建在攻击者的服务器上(127.0.0.1):
<img src="http://127.0.0.1:8088/DVWA/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#" border="0" style="display:none;"/>
<h1>
404 ERROR!
<h1>
<h2>
Files Not Found.
<h2>
存储位置为:D:\SoftWare\PhpStudy\PHPTutorial\WWW\DVWA\vulnerabilities\csrf
然后访问诱骗用户点击这个页面的链接(http://127.0.0.1:8088/DVWA/vulnerabilities/csrf/test.html):
用户会看到一个类似404的错误页面,但是用户却不知道自己的密码已经悄悄被人修改了!!!
Medium 安全级别
首先我们把安全级别设置为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);
}
?>
通过对源代码进行分析,我们可以知道Medium端主要的防御措施是通过if( eregi( $_SERVER[ 'SERVER_NAME' ], $_SERVER[ 'HTTP_REFERER' ] ) )
的ersgi( )函数来检查 HTTP_REFERER
(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME
(http包头的Host参数,及要访问的主机名,这里是127.0.0.1:8088
),希望通过这种机制抵御CSRF攻击。
由于我们前面构造的网页(test.html)就是在本机上,所以其Referer中就已经包含了DVWA平台的主机号。那如果是在另外的地方搭建的网页,我们也可以进行绕过,比如我们可以将构造的文件的名字更改为对方主机名即可,比如127.0.0.1:8088.html
。
High 安全级别
我们同样以正常用户的操作先进行一遍操作,发现密码修改后,URL中新增了一个参数即user_token参数:
我们来看下服务器端源代码:
<?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();
?>
通过源代码分析我们可以看到:
该模块中加入了Anti-CSRF token来防范CSRF攻击,同时每次随机生成了一个token,当用户提交的时候,在服务器端比对一下token值是否正确,不正确就丢弃掉,正确就验证通过。
【漏洞利用】
这个安全等级,自己主要是利用了DVWA的XSS漏洞和CSRF漏洞共同完成的:
(1)我们找到DVWA的XSS模块,通过XSS漏洞获取浏览器cookie:
(2)返回DVWA的CSRF模块,向输入框中输入任意你想要修改的密码,通过BrupSuite进行拦截,获取数据包并把数据包发送到Repeater模块:
(3)把通过XSS漏洞获取的cookie直接粘贴到这个数据包中,通过brupSuite对数据包进行篡改,发送,修改密码:
至此,密码更改成功!
Impossible 安全级别
把安全级别设置为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();
?>
通过源代码分析我们可以看到:Impossiable端代码添加了一个输入原来密码的操作,这样我们一开始不知道密码的情况下是不可能修改密码的,所以有效的防御了CSRF攻击。