漏洞之 CSRF(Cross-site request forgery,跨站请求伪造)

1、CSRF 是什么

CSRF(Cross-site request forgery,跨站请求伪造),也被称为:one click attack/session riding,缩写为:CSRF/XSRF。

2、CSRF 可以做什么

你这可以这么理解 CSRF 攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF 能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。

3、CSRF 漏洞现状

CSRF 这种攻击方式在 2000 年已经被国外的安全人员提出,但在国内直到 06 年才开始被关注,08 年国内外的多个大型社区和交互网站分别爆出 CSRF 漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube 和百度……而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。

4、CSRF 原理

下图简单阐述了CSRF攻击的思想:

从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:

  1. 登录受信任网站 A,并在本地生成 Cookie。

  2. 在不登出 A 的情况下,访问危险网站 B。

看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到 CSRF 的攻击”。是的,确实如此,但你不能保证以下情况不会发生:

  1. 你不能保证你登录了一个网站后,不再打开一个 tab 页面并访问另外的网站。

  2. 你不能保证你关闭浏览器了后,你本地的 Cookie 立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了……)

  3. 上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。

上面大概地讲了一下 CSRF 攻击的思想,下面我将用几个例子详细说说具体的 CSRF 攻击,这里我以一个银行转账的操作作为例子(仅仅是例子,真实的银行网站没这么傻)

示例1

银行网站A,它以 GET 请求来完成银行转账的操作,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000

危险网站B,它里面有一段HTML的代码如下:

1
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000\>

首先,你登录了银行网站A,然后访问危险网站B,噢,这时你会发现你的银行账户少了1000块……

为什么会这样呢?原因是银行网站A违反了 HTTP 规范,使用GET请求更新资源。在访问危险网站B的之前,你已经登录了银行网站A,而B中的以GET的方式请求第三方资源(这里的第三方就是指银行网站了,原本这是一个合法的请求,但这里被不法分子利用了),所以你的浏览器会带上你的银行网站A的Cookie发出Get请求,去获取资源 http://www.mybank.com/Transfer.php?toBankId=11&money=1000,结果银行网站服务器收到请求后,认为这是一个更新资源操作(转账操作),所以就立刻进行转账操作……

示例2

为了杜绝上面的问题,银行决定改用 POST 请求完成转账操作。

银行网站A的WEB表单如下:

1
2
3
4
5
<form action="Transfer.php" method="POST">
<p>ToBankId: <input type="text" name="toBankId" /></p>
<p>Money: <input type="text" name="money" /></p>
<p><input type="submit" value="Transfer" /></p>
</form>

后台处理页面 Transfer.php 如下:

1
2
3
4
5
6
<?php  
session_start();
if (isset($_REQUEST['toBankId'] && isset($_REQUEST['money'])){
buy_stocks($_REQUEST['toBankId'], $_REQUEST['money']);
}
?>

危险网站B,仍然只是包含那句HTML代码:

1
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000\>

和示例1中的操作一样,你首先登录了银行网站A,然后访问危险网站B,结果…..和示例1一样,你再次没了1000块,这次事故的原因是:银行后台使用了 $_REQUEST 去获取请求的数据,而 $_REQUEST 既可以获取GET请求的数据,也可以获取POST请求的数据,这就造成了在后台处理程序无法区分这到底是GET请求的数据还是POST请求的数据。

在PHP中,可以使用 $_GET$_POST 分别获取 GET 请求和 POST 请求的数据。在 JAVA 中,用于获取请求数据 request 一样存在不能区分GET请求数据和POST数据的问题。

示例3

经过前面2个惨痛的教训,银行决定把获取请求数据的方法也改了,改用 $_POST,只获取POST请求的数据,后台处理页面 Transfer.php 代码如下:

1
2
3
4
5
6
<?php 
session_start();
if (isset($_POST['toBankId'] && isset($_POST['money'])){
buy_stocks($_POST['toBankId'], _POST['money']);
}
?>

然而,危险网站B与时俱进,它改了一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>  
<head>
<script type="text/javascript">
function steal(){
iframe = document.frames["steal"];
iframe.document.Submit("transfer");
}
</script>
</head>
<body onload="steal()">
<iframe name="steal" display="none">
<form method="POST" name="transfer" action="http://www.myBank.com/Transfer.php">
<input type="hidden" name="toBankId" value="11">
<input type="hidden" name="money" value="1000">
</form>
</iframe>
</body>
</html>

如果用户仍是继续上面的操作,很不幸,结果将会是再次不见1000块……因为这里危险网站B暗地里发送了POST请求到银行!

总结一下上面3个例子,CSRF 主要的攻击模式基本上是以上的3种,其中以第 1,2 种最为严重,因为触发条件很简单,一个 <img> 就可以了,而第3种比较麻烦,需要使用JavaScript,所以使用的机会会比前面的少很多,但无论是哪种情况,只要触发了CSRF攻击,后果都有可能很严重。

理解上面的3种攻击模式,其实可以看出,CSRF攻击是源于WEB的隐式身份验证机制!WEB的身份验证机制虽然可以保证一个请求是来自某个用户的浏览器,但却无法保证该请求是用户批准发送的!

5、CSRF 漏洞检测

检测 CSRF 漏洞是一项比较繁琐的工作,最简单的方法就是抓取一个正常请求的数据包,去掉 Referer 字段后再重新提交,如果该提交还有效,那么基本上可以确定存在CSRF漏洞。

随着对 CSRF 漏洞研究的不断深入,不断涌现出一些专门针对 CSRF 漏洞进行检测的工具,如 CSRFTester,CSRF Request Builder 等。

以 CSRFTester 工具为例,CSRF 漏洞检测工具的测试原理如下:使用 CSRFTester 进行测试时,首先需要抓取我们在浏览器中访问过的所有链接以及所有的表单等信息,然后通过在 CSRFTester 中修改相应的表单等信息,重新提交,这相当于一次伪造客户端请求。如果修改后的测试请求成功被网站服务器接受,则说明存在 CSRF 漏洞,当然此款工具也可以被用来进行 CSRF 攻击。

6、CSRF 的特点

  1. 攻击通常在第三方网站发起,如图上的站点B,站点A无法防止攻击发生。
  2. 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作。网站并不会去获取 cookie 信息(cookie有同源策略)
  3. 跨站请求可以利用各种方式:图片URL,超链接,CORS,Form提交等等(来源不明的连接,不要点击)

7、CSRF 的防御

CSRF 的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的 CSRF 防御也都在服务端进行。

7.1、验证 HTTP Referer 字段

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。在通常情况下,访问一个安全受限页面的请求来自同一个网站,比如需要访问 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory ,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件。这时,该转帐请求的 Referer 值就会是转账按钮所在的页面的 URL,通常是以 bank.example 域名开头的地址。

而如果黑客要对银行网站实施 CSRF 攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的 Referer 是指向黑客自己的网站。因此,要防御 CSRF 攻击,银行网站只需要对于每一个转账请求验证其 Referer 值,如果是以 bank.example 开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。

这种方法的显而易见的好处就是简单易行,网站的普通开发人员不需要操心 CSRF 的漏洞,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。

然而,这种方法并非万无一失。Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。

事实上,对于某些浏览器,比如 IE6 或 FF2,目前已经有一些方法可以篡改 Referer 值。如果 bank.example 网站支持 IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。

即便是使用最新的浏览器,黑客无法篡改 Referer 值,这种方法仍然有问题。因为 Referer 值会记录下用户的访问来源,有些用户认为这样会侵犯到他们自己的隐私权,特别是有些组织担心 Referer 值会把组织内网中的某些信息泄露到外网中。因此,用户自己可以设置浏览器使其在发送请求时不再提供 Referer。当他们正常访问银行网站时,网站会因为请求没有 Referer 值而认为是 CSRF 攻击,拒绝合法用户的访问。

7.2、使用Token

CSRF 攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个 CSRF 攻击者无法获取到的 Token。服务器通过校验请求是否携带正确的 Token,来把正常的请求和攻击请求区分开。跟验证码类似,只是用户无感知

步骤:

  • 服务端给用户生成一个 token, 加密后传递给用户。
  • 用户在提交请求时,需要携带这个 token。
  • 服务端验证 token 是否正确。

7.2.1、在 HTTP 头中自定义属性并验证

这种方法把 token 放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性,并把 token 值放入其中。通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会透过 Referer 泄露到其他网站中去。

然而这种方法的局限性非常大。XMLHttpRequest 请求通常用于 Ajax 方法中对于页面局部的异步刷新,并非所有的请求都适合用这个类来发起,而且通过该类请求得到的页面不能被浏览器所记录下,从而进行前进,后退,刷新,收藏等操作,给用户带来不便。另外,对于没有进行 CSRF 防护的遗留系统来说,要采用这种方法来进行防护,要把所有请求都改为 XMLHttpRequest 请求,这样几乎是要重写整个网站,这代价无疑是不能接受的。

7.2.2、Cookie Hashing(所有表单都包含同一个伪随机值)

这可能是最简单的解决方案了,因为攻击者不能获得第三方的 Cookie(理论上),所以表单中的数据也就构造失败了。

1
2
3
4
<?php
//构造加密的Cookie信息
$value = "DefenseSCRF"; setcookie("cookie", $value, time()+3600);
?>

在表单里增加 Hash 值,以认证这确实是用户发送的请求。

1
2
3
4
5
6
7
8
9
<?php
$hash = md5($_COOKIE['cookie']);
?>
<form method="POST" action="transfer.php">
<input type="text" name="toBankId">
<input type="text" name="money">
<input type="hidden" name="hash" value="<?=$hash;?>">
<input type="submit" name="submit" value="Submit">
</form>

然后在服务器端进行Hash值验证

1
2
3
4
5
6
7
8
9
10
11
12
<?php
if(isset($_POST['check'])) {
$hash = md5($_COOKIE['cookie']);
if($_POST['check'] == $hash) {
doJob();
} else {
//...
}
} else {
//...
}
?>

这个方法个人觉得已经可以杜绝99%的CSRF攻击了,那还有1%呢….由于用户的 Cookie 很容易由于网站的 XSS 漏洞而被盗取,这就另外的1%。一般的攻击者看到有需要算 Hash 值,基本都会放弃了,某些除外,所以如果需要 100% 的杜绝,这个不是最好的方法。

7.2.3、验证码

这个方案的思路是:每次的用户提交都需要用户在表单中填写一个图片上的随机字符串,厄….这个方案可以完全解决 CSRF,但在易用性方面似乎不是太好,还有听闻是验证码图片的使用涉及了一个被称为 MHTML 的 Bug,可能在某些版本的微软IE中受影响。

验证码能够防御 CSRF 攻击,但是我们不可能每一次交互都需要验证码,否则用户的体验会非常差,但是我们可以在转账,交易等操作时,增加验证码,确保我们的账户安全。

7.2.4、One-Time Tokens(不同的表单包含一个不同的伪随机值)

在实现 One-Time Tokens 时,需要注意一点:就是“并行会话的兼容”。如果用户在一个站点上同时打开了两个不同的表单,CSRF 保护措施不应该影响到他对任何表单的提交。考虑一下如果每次表单被装入时站点生成一个伪随机值来覆盖以前的伪随机值将会发生什么情况:用户只能成功地提交他最后打开的表单,因为所有其他的表单都含有非法的伪随机值。必须小心操作以确保CSRF保护措施不会影响选项卡式的浏览或者利用多个浏览器窗口浏览一个站点。

7.3、Samesite Cookie属性

为了从源头上解决这个问题, Google 起草了一份草案来改进 HTTP 协议,为 Set-Cookie 响应头新增 Samesite 属性,它用来表明这个 Cookie 是个 “同站Cookie”,同站 Cookie 只能作为第一方 Cookie,不能作为第三方 Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax。

部署简单,并能有效防御 CSRF 攻击,但是存在兼容性问题

Samesite=Strict 被成为是严格模式,表明这个 Cookie 在任何情况都不可能作为第三方的 Cookie,有能力阻止所有 CSRF攻击。此时,我们在 B 站点下发起对 A 站点的任何请求,A站点的 Cookie 都不会包含在 cookie 请求头中。

CRSF 攻击很大程度上是利用了浏览器的 cookie,为了防止站内的 XSS 漏洞盗取 cookie,需要在 cookie 中设置“HttpOnly”属性,这样通过程序(如JavaScript脚本、Applet等)就无法读取到 cookie 信息,避免了攻击者伪造 cookie 的情况出现。

7.5、特殊情况

  • 网站本身存在 XSS 时,验证码可完全杜绝 CSRF
  • 钓鱼网站,CSRF_TOKEN 存在 cookie 中,并把 HttpOnly 设置为 TRUE 可以杜绝

Reference


漏洞之 CSRF(Cross-site request forgery,跨站请求伪造)
https://flepeng.github.io/081-security-漏洞-漏洞之-CSRF(Cross-site-request-forgery,跨站请求伪造)/
作者
Lepeng
发布于
2021年3月8日
许可协议