跨站请求伪造攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,修改密码,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
攻击原理:
1. 用户打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
2. 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功;
3. 用户未退出网站A的情况下,在同一浏览器中访问网站B;
4. 网站B接收到用户请求后,返回攻击代码,并发出一个请求要求访问第三方站点A;
5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。
6.网站A只要检验Cookie是否正确,正确则执行相应的操作。
网站A并不知道该请求其实是由B发起的,所以会根据用户的Cookie信息以用户的权限处理该请求,导致网站B的恶意代码被执行。
由此我们可以知道构成CSRF攻击是有条件的
1、客户端必须在同一个网站并生成cookie凭证存储在浏览器中。
2、该cookie没有清除,客户端又重新进入了另一个页面访问其他网站
CSRF漏洞分类:
GET型
GET类型的CSRF利用非常简单,只需要一个HTTP请求,所以,一般会这样利用:
<img src=http://漏洞地址>
POST型
<form action=http://漏洞地址 method=POST>
<input type="text" name="xx" value="11" />
</form>
<script> document.forms[0].submit(); </script>
访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。
攻击的实现:
这是一个修改密码的页面,我们通过源码来判断攻击和防御的具体措施。
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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
mysql_close();
}
?>
我们来看看重点的几行:
服务器只检查参数password_new与password_conf是否相同,如果相同,就可以进行修改密码的操作,整个过程中并没有任何的防CSRF机制(cookie、会话等)
在此级别时我们只需要修改URL中的password的值,然后访问此链接就能直接进行改密的操作,非常不安全,攻击者只需要引诱用户点击这个链接,就能直接修改用户的密码,而用户完全没有此意愿。
medium级别:
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( eregi( $_SERVER[ 'SERVER_NAME' ], $_SERVER[ 'HTTP_REFERER' ] ) ) {
// 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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</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>";
}
mysql_close();
}
?>
重点的几行:
Medium级别的代码引入了eregi函数,Eregi函数的原理是用来检验第二个参数中是否含有第一个参数,并且Eregi不区分大小写,在此处的代码中,该函数起到的作用为检查HTTP_REFERER中是否包含SERVER_NAME,如果包含,则符合条件,继续进行接下来的操作。代码希望通过这种机制来抵御CSRF攻击。
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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
mysql_close();
}
// Generate Anti-CSRF token
generateSessionToken();
?>
重要的几句:
High级别的带入引入了Anti-CSRF token,每次修改密码服务器会通过generateSessionToken()随机生成一个token,只有客户端提交的token参数与服务器端一致时,服务器端才会处理客户端的响应。到这个级别已经基本不会被CSRF攻击了。
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 = mysql_real_escape_string( $pass_curr );
$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 = mysql_real_escape_string( $pass_new );
$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();
?>
在前面的基础上上引入了PDO,同时需要客户端在修改密码时必须正确输入原密码才能进行修改操作。如果不知道原密码,则无法进行CSRF攻击。
总结:CSRF攻击是源于web的隐式身份验证机制!Web的身份验证机制虽然可以保证一个请求是来自某个用户的浏览器,但却无法保证该请求是用户批准发送的。
CSRF防御:
1、重要的数据交互采用POST进行接收,但是用POST也不是万能的,伪造一个form表即可破解,就是相对比较麻烦一点。
2、使用验证码,要是涉及到敏感数据时,就先进行验证码验证,这个方式可以解决CSRF,但是网站不可能给所有的操作都加上验证码。因此只能作为一种辅助手段,不能作为主要解决方案。
3、验证HTTP referrer字段,该字段记录了此次HTTP请求的来源地址,最常见的应用是图片防盗链。PHP中可以采用Apache URL重写规则进行防御。
4、为每个表单添加token并验证。
CSRF攻击之所以能够成功,是因为攻击者可以伪造用户的请求,该请求中所有的用户验证信息都存在于Cookie中,因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的Cookie来通过安全验证。由此可知,抵御CSRF攻击的关键在于:在请求中放入攻击者所不能伪。造的信息,并且该信息不存在于Cookie之中。
我们将每一个表单生成一个随机数密钥,并在服务器端建立一个拦截器来验证这个token,如果请求中没有token或者token内容不正确,则认为CSRF攻击。这个token是随机不可预测的,而且为隐藏的,攻击者也就不可能伪造出这个表单进行CSRF了。