Cookie-Form型CSRF防御机制的不足与反思

今天看了 https://hackerone.com/reports/26647 有感。这个漏洞很漂亮,另外让我联想到很多之前自己挖过的漏洞和写过的程序,有感而发。

Django已经在昨天修复了该漏洞 https://www.djangoproject.com/weblog/2016/sep/26/security-releases/

0x01 借助Session防御CSRF漏洞

我最早接触Web安全的时候(大概大一暑假),写过一个站点。当时边看道哥的《白帽子讲Web安全》,边在写站点的过程中熟悉每种漏洞,并编写尽量安全的代码。

初识CSRF漏洞的我使用了一种中规中矩的方法来防御CSRF漏洞:

  1. 后端生成随机字符串Token,储存在SESSION中。
  2. 每当有表单时,从SESSION中取出Token,写入一个隐藏框中,放在表单最底部。
  3. 接受POST数据时,先验证$_POST['token'] === $_SESSION['token'],再执行其他逻辑。

这是一个很标准的CSRF防御方法,也很难找出其破绽。但这个方法有个致命的弱点:Session。原因有二:

  1. 所有用户,不论是否会提交表单,不论是否会用到这些功能,都将生成一个Session,这将是很大的资源浪费。举个例子,Sec-News的Session储存在redis里,每天会生成数千到数万的Session,自动化脚本每天夜里会遍历并清理没有使用的Session,以避免过度消耗资源。
  2. 除了PHP的很多开发语言中,Session是可选项,很多网站根本没有Server Session。开发框架不能强迫开发者使用Session,所以在设计防御机制的时候也不会使用Session。

所以,像Django之类的Python框架,会选择基于Cookie的CSRF防御方式。

顾名思义,Cookie-Form型CSRF防御机制,是和Cookie和Form有关。它确切的名字我还不太清楚,暂且这样称之。

Sec-News曾经分享过一篇文章 《前后端分离架构下的CSRF防御机制》,当时 @neargle 就提出过疑问。

sp160927_044048.png

其实借助Cookie来防御CSRF的方法是一个通用的防御方法,单纯应对CSRF漏洞是绝对可行的。该文章的解决方案是,后端生成一个token和一个散列,均储存于Cookie中,在提交表单时将token附带在表单中提交给后端,后端即可根据表单中的token和cookie中的散列来验证是否存在CSRF攻击。

实际上散列这一步是没有必要的,后端只需要生成好一个随机token储存于Cookie中,前端提交表单时提交该Cookie基本就万无一失了。

我第一次接触这种防御方法是在学习CodeIgniter的过程中(这里提一下,CI框架默认的CSRF防御方法就是本文说的这个方法),当时认为这种防御方法很不可理喻。因为Cookie是可以控制的,如果攻击者将Cookie控制地和Form中相同,不就可以绕过这个防御了么?

但是细想来,立马打脸了:攻击者如何修改受害者的Cookie?

既然是CSRF漏洞,也就不能控制目标域的脚本,当然就无法获取Cookie(如果能获取Cookie就不叫CSRF漏洞了)。

总结一下,基于Cookie的CSRF防御方法,较基于Session的方法有如下优点:

  1. 无需使用Session,适用面更广,适合“Secure By Default“原则。
  2. Token储存于客户端中,不会给服务器带来压力。
  3. 没有其他漏洞的情况下,黑客无法接触Cookie,所以保证了Token的机密性,也就可以防御CSRF漏洞。

那么,基于Cookie的CSRF防御机制,有什么弊端?

弊端也很明显:一旦有其他漏洞(即使是看起来很鸡肋的漏洞)的存在,很容易就能破坏这种防御手法。

我曾经分享过知乎的一个漏洞《知乎某处XSS+刷粉超详细漏洞技术分析》,很经典的一个案例。攻击者获得了一个”看似十分鸡肋“的XSS漏洞(domain是子域名,而且关键cookie都有httponly),无法做一些正常XSS漏洞可以做的攻击,但却可以写入Cookie。

攻击者通过写入一个新的"CSRF_TOKEN",将原有的无法获取的Token覆盖掉,就成功绕过了0x02中描述的防御手法。

这种绕过方法的核心就是:利用其它漏洞写入Cookie,覆盖原有Cookie,来达到Form[token]===Cookie[token]的目的。

那么,寻找此类绕过漏洞的核心就是寻找注入新Cookie的方法,看过一些案例,我归纳出来几种:

  1. 某些单纯而不做作的前端编写的页面可以写入Cookie
  2. 鸡肋XSS漏洞
  3. 利用CRLF漏洞注入Cookie
  4. 利用畸形字符使后端解析Cookie出错,注入Cookie

第一种,很久以前我在QQ空间的不止一处看到过,某些页面从location.search中获取参数并设置为Cookie。但找这种地方比较难,没有什么特别的方法,可遇而不可求。

第二种,就看知乎那个案例吧。

第三种,@/fd 曾用一个Twitter的overflow漏洞演示了Cookie的注入:《Overflow Trilogy》。这个漏洞原本是可以用来绕过Twitter的CSRF检测的,不过后来Twitter把CSRF防御方式从0x02换成0x01了,有点可惜:

sp160927_055401.png

0x04 Web Server解析Cookie的特性

第四种,就是利用Google Analytics来绕过Django的CSRF防御方式。这个方法其实作者早在 2015年 就已经提出来了(当时是作为Twitter的一个漏洞提交的)。

Google Analytics会将网站的path写入Cookie中,而没有进行编码,导致攻击者可以输入一些“特殊”的字符。

当时使用的是逗号“,”,有些Web Server在解析Cookie时,逗号也会成为分隔符。这样就导致了Cookie: param1=1111,param2=2222;这样的Cookie被解析成Cookie[param1]=1111Cookie[param2]=2222,成功注入了一个新Cookie,Param2 。

这次Django的Cookie注入也类似。其实原因来自于Python原生的cookielib库,在分割Cookie头的时候,将“]”也作为了分隔符,导致Cookie: param1=value1]param2=value2被解析成Cookie[param1]=value1Cookie[param2]=value2,成功注入了一个新Cookie,Param2。

关于畸形Cookie注入的一些姿势,可以看看 https://habrahabr.ru/post/272187/

成功注入Cookie后,后续“CSRF攻击”流程就和0x03中讲的一样了,不再赘述。思路很不错,所以写文章说说,和大家分享一下自己的一些看法。

赞赏

喜欢这篇文章?打赏1元

评论

fireseed 回复

另外前后端分离的架构下,token放在cookie里这个是不能http-only的,否则前端js无法读取,就无法拿出来传给后端验证了。

phithon 回复

@fireseed
前后端分离的架构,只要主域名相同,cookie就会在请求里呀,为何要js拿出来传?
当然,域名如果完全不相同,那么可能需要存储在local storage里。

fireseed 回复

有个疑问,token服务端生成之后肯定是要存一份用来和前端传过来的请求cookie里的token进行校验的,你攻击者怎么能预测到正确的token呢?

phithon 回复

@fireseed
我说的是“Cookie-Form型CSRF防御机制”,如果是把token存到后端,不在本文讨论范畴内。
另外你没理解CSRF漏洞防御的原理,“用来和前端传过来的请求cookie里的token进行校验”,这个就是错的。

fireseed 回复

@phithon token一定是要校验的,我理解你文中提到的token+hash(token)的方案才是正确的,而你说的不需要hash是错误的。因为当csrf利用点是用form表单提交的时候,受害者在自己浏览器静默提交了表单,是会带着cookie的。你不校验cookie里的token的有效性,不就防不住了吗?一定要和header里的或者请求body里的token校验才有效。这样才能让服务端不用维护token。即使是ajax的csrf,ajax跨域的时候也是可以设置携带目的域的cookie的,所以依然无法只靠cookie里有token,而不校验token有效性就能防御的。
同理,前后端分离的架构下,当基于cookie存储token,不管是服务端生成的还是客户端生成的,也都是要用js从cookie里把token取出来放在表单里或者header里。因为攻击者无法控制受害者的cookie。所以他无法自己构造参数token的值使其和cookie里的匹配。所以这个token这个cookie肯定不能设置为httponly。

phithon 回复

@fireseed
……
我回复的是你这个问题:“有个疑问,token服务端生成之后肯定是要存一份用来和前端传过来的请求cookie里的token进行校验的,你攻击者怎么能预测到正确的token呢?”
服务端没有存,而是从form里取出token,和cookie里取出的token进行比较。
至于怎么让二者相同,我文中也说了,几种方法:

1. 写入cookie,覆盖原本的cookie(知乎案例)
2. 利用后端的bug,让后端取出的cookie不是正确的那个(django案例)

这都是实际案例。

我为什么说你说的是错的,因为校验,也只有两种情况:

1. 后端存储的token,和form里提交的token进行比较
2. cookie里的token,和form里提交的token进行比较

前者是server-side csrf,后者是本文讲的cookie-form csrf。不存在后端存储的token和cookie里存储的token比较这种情况。

james 回复

@phithon 你讲的很详细,真的赞

南城夕雾 回复

@phithon 讲的很清晰,太棒了,每次读你的文章都会有很大的收获!

小白白 回复

有个问题,用cookie防御CSRF的方案,如果cookie是HTTP-only,是不是理论上是安全的?

phithon 回复

@小白白
你应该没看懂我的文章。
httponly只能限制让JavaScript无法读取某个cookie,但在这里不需要读取,我可以设置一个同名的cookie覆盖之即可。
比如我文中知乎的例子,其实就是httponly的。

红尘 回复

彼此彼此我还提交了个css跨域漏洞今天,就是道哥那本书上的css跨域漏洞,还好没过,否则贻笑大方了。。

neargle 回复

当时遇到这个问题的时候,总觉得自己想的不对,但又不知道错在哪里...想了很久,决定写了一个demo试看看,就用python实现了一遍把token的校验值放在cookie里面的做法,然后自己去攻击。
伪造表单的时候才发现,哦,cookie在这个时候我是动不了的......诶有时候脑子转不过弯真可怕...

phithon 回复

@neargle:你已经不错了,我大一学的那会把这东西当漏洞提交了,辛亏没过,要不然就贻笑大方了……

xbtsec 回复

@phithon:说起贻笑大方,又一次把一个cms下载来 自己搭建 然后没设置 就 目录遍历了 然后 我竟然提交补天了 辛亏没过,要不然就贻笑大方了……

duke 回复

@phithon 有个疑问请教, 针对Cookie-Form型CSRF防御机制,如果cookie和token都没有过期, 比如受害者不久前就打开过正常网站A,这个时候又去打开另一个有威胁的网站B, 这个网站B隐藏了提交网站A的的表单, 受害者如果触发了提交表单的这个行为, 这样不是可以直接利用已有的cookie和token来提交表单了么?因为不需要去操作cookie, 而且cookie中的token和body中的token本来就是一致的。

captcha