CSRF

0x00 概述

本文介绍CSRF的漏洞原理、利用方式、修复方式

0x01 漏洞简介

Cross-Site Request Forgery(CSRF)是一种Web前端攻击方式,它通过在站点、email中嵌入一段恶意JS或者HTML代码,当拥有信任站点登录态的用户在访问这些站点、email时,恶意代码执行并造成浏览器对信任站点发送带有用户登录态的HTTP/S请求,执行一些非用户意愿的操作

简单来说,CSRF是在用户不知情的情况下,通过浏览器冒用其身份发起HTTP/S请求

根据站点暴露的功能和拥有登录态用户的权限,CSRF可以造成的危害从转移个人用户个人资金、修改用户密码到用利用管理员身份对整个站点进行修改,在利用站点关系网络的情况下,CSRF可以直接或间接利用XSS漏洞造成大面积攻击

CSRF利用的是浏览器在用<a>,<img src="">,<form>,<script>等向跨域的服务器发送请求时会默认带上Cookie头部,而Cookie一般用来维持用户登录态,在用户不知情的情况下,恶意站点跨域发送了带有用户Cookie的请求,就等于伪造了用户的请求,由于是浏览器发送的请求,在请求只是读取数据的情况下,最后也只是被用户获取到,没有什么危害,但是在请求会改变服务器状态(数据库内容等)情况下,请求就产生了危害

0x02 威胁场景

首先来看看CSRF攻击的条件

  1. 没有CSRF防御的请求接口且接口的功能必须要改变服务器状态(数据等信息)
  2. 用户必须处在登录状态
  3. 构造有利用漏洞请求的页面并引导用户通过浏览器访问

接下来的场景里,我们假设自己为Alice,要给Bob转账,而Maria是坏人

GET请求下漏洞利用

在Web系统bank.com中比如下一条请求表示Alice会转账给Bob,服务器接到请求后会进行转账操作

在普通情况下,因为Web页面有用Cookie等身份验证,在不知道Cookie的情况下可以保证Alice的操作是正确转账给Bob的

GET /transfer.do?acct=BOB&amount=1000 HTTP/1.1
Host: bank.com
Cookie: id=a874920387490283740928374

Maria在hacker.com构造了如下的页面

<a href="http://bank.com/transfer.do?acct=MARIA&amount=1000000">View</a>
<img src="http://bank.com/transfer.do?acct=MARIA&amount=100000" width="0" height="0" border="0">

Maria在Alice登录了bank.com的时候(也就是浏览器记录了Alice在这个页面shang d Cookie)诱导Alice去访问hacker.com,此时页面在浏览器中加载的过程中<img src="">标签便会产生一条以Alice身份发送的请求,如果Alice点击了<a>标签的连接也会发送以Alice身份发送的请求,因为它们都带有Alice的Cookie

至此满足的CSRF漏洞利用的三个条件

  1. 没有CSRF防御的请求接口且接口的功能必须要改变服务器状态(数据等信息)
  2. 用户必须处在登录状态
  3. 构造有利用漏洞请求的页面并引导用户通过浏览器访问

POST请求下的漏洞利用

和上面的GET请求相同,我们类比一下, 绕过详尽的说明

正常请求如下

POST http://bank.com/transfer.do HTTP/1.1
Cookie: id=aljdflajsdhflasdfasf

acct=BOB&amount=100

Maria构造的恶意页面如下

<body onload="document.forms[0].submit()">
<form action="http://bank.com/transfer.do" method="POST">
<input type="hidden" name="acct" value="MARIA"/>
<input type="hidden" name="amount" value="100000"/>
<input type="submit" value="View my pictures"/>
</form>

此时诱导有登录态的Alice去访问恶意页面,页面一加载便会以Alice的身份发送POST请求,改变服务器状态(某些浏览器可能会禁止这种一加载就发表单请求的行为)

其他HTTP方法的利用方式

理论上来讲其他的HTTP方法比如OPTIONS和PUT都存在这种问题且利用方式一样,但是我并没有实验,因为其他方式使用还是少的并会越来越少,而且考虑到请求必须能改变服务器状态的限制,类似OPTIONS的方法也很少有直接改变服务器状态的,因此略过

JSON Hijacking读取数据的利用方式

上面说到的CSRF都是改变服务器状态的写数据的情况,但存在一种读数据的情况,就是JSON Hijacking

JSON Hijacking是JSONP带来的风险,但属于CSRF的范畴,因为仍然是因为<script>标签发送的请求带有Cookie造成的

典型的恶意页面如下

<script>
function hacker(v) {
    alert(v.data)
}
</script>
<script src="http://bank.com/?jsoncallbackfunc=hacker>

bank.com对JSONP的处理大概是

jsoncallbackfunc = request.args.get("jsoncallbackfunc")
return "{}({'data':'data'})".format(jsoncallbackfunc)

此时恶意页面便可以获取用户数据

和XSS结合的蠕虫

注意:我们讨论CSRF漏洞的前提是没有XSS漏洞

XSS不是CSRF工作的必要条件,但是,XSS可以简单地读取页面内容并用XMLHttpRequest生成CSRF请求,参考MySpace(Samy) worm事件。为了CSRF防御可以起效,没有XSS漏洞是必要的

0x03 修复方式

攻击可以成立的本质:所有的参数都可以被攻击者伪造

自动防御CSRF攻击的一般建议(不需要用户干预,并且只允许同源,不允许CORS跨域),步骤如下:

  • 检查标准头部验证请求是否同源
  • 检查CSRF token

通过标准头部验证请求是否同源

有两步检查

  • 确定Source Origin(request来源同源)
  • 确定Target Origin(request去向同源)

这两个步骤都依赖于检查HTTP请求头,虽然用JS可以在浏览器上伪造这些头部,但是一般是不可能做到在受害者浏览器CSRF攻击过程中修改这些头,除非有XSS漏洞

更重要的是,对于这个推荐的同源检查,一些HTTP头不能由JS设置,因为它们位于“禁止”标题列表中,只有浏览器本身可以未这些HEADER设定值,这使它们更值得信任,因为XSS也不能修改它们

这里推荐来源检查依赖三个保护头:Origin、Referer和HOST

检查Source Origin

  • Origin Header
    • IE11在CORS请求中可能不带Origin
    • 302 redirect cross-origin可能不带
  • Referer Header
  • 两个头部都没有
    • 在没有CSRF token的情况下建议拒绝请求
    • 在可以查询记录的情况下这种情况持续一段时间要求用户重新登录,没有站点没有这种查询记录的基础功能,就拒绝请求

检查Target Origin

  • Host HEADER检查
    • 同样可能被代理修改
  • X-Frowarded-Host检查
    • 注意各个主流浏览器支持情况
  • 匹配URL不可以,因为在有类似反向代理的情况下,URL的值和target origin并不一定一样,除非可以确定直接来自用户

两种都检查,并且匹配Source Origin和Target Origin是否相同,不同则可能产生CSRF问题

CSRF Token类型的防御(CSRF Token和其他高安全性的头部)

  • Synchronizer (CSRF) Token
  • Double Cookie Defense
  • Encrypted Token Pattern
  • Custom Header - e.g., X-Requested-With: XMLHttpRequest
Synchronizer (CSRF) Token防御
  • 任何能使服务器状态变化的请求(最好使用POST方法),都需要一个安全随机的Token防止CSRF攻击
  • CSRF Token的特性
    • 对每个用户session唯一
    • 长随机值
    • 用Secure Random Generator(一般语言自带Secure Random库)和密码算法生成
  • Token用用户不可见的方式加在POST请求的参数中(From表单的hidden标签等)
  • 当CSRF Token验证出错服务器拒绝请求

使用时页面代码大致如下

  <form action="/transfer.do" method="post">
  <input type="hidden" name="CSRFToken" 
  value="OWY4NmQwODE4ODRjN2Q2NTlhMmZlYWE...
  wYzU1YWQwMTVhM2JmNGYxYjJiMGI4MjJjZDE1ZDZ...
  MGYwMGEwOA==">
  …
  </form>

实现可以参考的代码资料:https://github.com/aramrami/OWASP-CSRFGuard

当不得不使用GET方法去改变服务器状态时,存在Token放在URL中泄露的问题

可能泄露的地方

  • 浏览器历史
  • HTTP日志文件
  • proxies留有日志
  • referer头,如果站点链接到外部网站,HTTPS和HTTP转换中去掉HTTPS的情况

所以在不得不使用GET方法去改变服务器状态时,CSRF Token可以作为临时策略,但要加上一个较短的时效

这种方式和上面不同的是,这个Synchronizer (CSRF) Token分别加到了用户的Cookie的参数和提交的参数中, 这样服务器端就不用存储Token了

因为UI和服务请求未必来自同一服务器(API和页面不同服务器提供),在UI服务器返回的UI中用JS向API服务器发送请求,需要JS读取Cookie加到参数中,确保参数有Token的性质,不会被伪造,因此也最好用在POST方法中,而因为攻击者无法读取Cookie的值,因此也无法获取Token的值,不同的服务器也无需存储用户Cookie

Encrypted Token Pattern

这种方式和上面的Double Submit Cookie有点类似,不同的是,这种方法即需要服务器端存储Token,也不需要存储用户Session

用户访问时用户ID(用户认证方式,存在Cookie或者Session中)、时间戳和随机数等参数加密生成一个Token,服务器端保留加密算法的Key,将Token放在POST方法的参数中,如果用户请求时解密结果正确则是正常访问

这种方式的网站站点往往是将Session保存在Cookie中,服务器不记录Session数据,如Python的Flask框架是我常用的框架,它就使用这种方式

Custom Header

加HTTP Header中增加一个自定义字段,存储Token

适合Ajax的防御,自定义请求头部,默认情况下,JS可以定义请求头,但浏览器不允许JS跨域请求,因为不用担心恶意站点的伪造的请求会带上这个请求头

有用户交互的防御方式

  • 发现不正常请求重新认证用户
  • One-Time Token(手机验证码)
  • CAPTCHA(人机识别的验证码)

samesit属性

Chrome和Firefox支持SameSite属性,防止向第三方站点送Cookie

Set-Cookie: JSESSIONID=xxxxx; SameSite=Strict

Set-Cookie: JSESSIONID=xxxxx; SameSite=Lax

重要功能多步操作

0x04 深入攻防

假如我们正常的请求应该如下

POST http://192.168.199.129:5000/testpost HTTP/1.1

{ "acct":"BOB", "amount":100 }

我们知道Form表单是无法伪造JSON格式的请求数据的,

是否可以通过Ajax伪造上面的请求并带上用户Cookie呢

<script>
function post() {
	var x = new XMLHttpRequest();
	x.open("POST","http://192.168.199.129:5000/testpost",true);
	x.send(JSON.stringify({"acct":"BOB", "amount":100})); 
}
</script>
<body onload="post()">

在Chrome上测试上面的例子,用Chrome开发者工具发现Chrome会发送OPTIONS方法的请求给服务器,根据服务器返回判断是否允许跨域Ajax,也就是根据Access-Control-Allow-Origin的Header进行判断,正常情况下,我们的网站是不允许跨域的,要跨也只能跨子域

所以我们在服务端加上resp.headers['Access-Control-Allow-Origin'] = '*'

此时我们可以发现,请求正常发送了,但是没有带上Cookie,因为即使Ajax可以跨域,浏览器也不会带上Cookie信息,我们要在服务器端再加上response.headers['Access-Control-Allow-Credentials'] = 'true'

同时我们恶意页面的代码改成

<script>
function post() {
	var x = new XMLHttpRequest();
	x.withCredentials = true;
	x.open("POST","http://192.168.199.129:5000/testpost",true);
	x.send(JSON.stringify({"acct":"BOB", "amount":100})); 
}
</script>
<body onload="post()">

这时用Chrome开发者工具就会发现我们发送了Cookie,并且服务端正常返回,但是页面却没有显示,Console中的报错信息告诉我们Access-Control-Allow-Origin头部不能为*,必须指定域名,但其实这时我们的CSRF攻击已经成功了,因为服务端接收了请求并改变了状态

举这样一个例子,是想说明

  • JSON一类的格式不能用Form表单直接发送时可以使用Ajax的方式
  • Ajax是有限制的,必须要服务端有开通这些限制
  • 像例子中浏览器拦截了返回包的显示,而不是没有发送返回包,所以头部中限制跨域并不能防止CSRF
  • 因此涉及到网站确实需要跨域时服务器头部的设置要认真对待

同时,存在一些情况,当我们把JSON数据作为Form表单的请求name的时候,发送的数据为{ "acct":"BOB", "amount":100 }=,某些JSON解释器健壮性过强,这种格式也会正常解析= =

0x05 总结

改变服务器状态的请求只使用POST

注意JSON Hijacking的特殊情况会导致用户数据因为CSRF被读取,也就是注意JSONP的使用

使用对用户透明的安全令牌,和CSRF Token类似,但不是专门防御CSRF Token,是一种良好习惯,重要的是连接开始时生成一个随机串,和用户时间等参数绑定

注意CSRF与XSS的区别

  • XSS是用户信任网站,放任来自网站的网站在浏览器上任意执行
  • CSRF是网站过分信任用户,放任通过网站设定的访问控制方式的用户对服务器状态进行任意改变

0x06 参考资料

[OWASP Cross-Site_Request_Forgery_(CSRF)Prevention_Cheat_Sheet](https://www.owasp.org/index.php/Cross-Site_Request_Forgery(CSRF)_Prevention_Cheat_Sheet)

OWASP Cross-Site_Request_Forgery_(CSRF)

坚持原创技术分享,您的支持将鼓励我继续创作!