01 概念
跨站请求伪造(也称为 CSRF)是一种 Web 安全漏洞,攻击者可以利用它诱使用户执行他们原本无意执行的操作。它允许攻击者部分绕过同源策略,而同源策略旨在防止不同网站相互干扰。
利用用户已登录目标网站的合法身份(如 Cookie、Session 等会话凭证),以及浏览器自动携带 Cookie 的特性,诱导用户在已登录状态下触发恶意请求,让服务器误以为是用户本人的操作,从而执行攻击者预设的指令
02 原理
1. 关键条件
要使 CSRF 攻击成为可能,必须满足三个关键条件:
- 相关操作: 攻击者有理由诱使应用程序执行某个操作。这可能是特权操作(例如修改其他用户的权限),也可能是对用户特定数据的任何操作(例如更改用户自己的密码)。
- 基于 Cookie 的会话处理: 执行操作涉及发出一个或多个 HTTP 请求,应用程序仅依赖会话 Cookie 来识别发出请求的用户。没有其他机制来跟踪会话或验证用户请求。
- 没有不可预测的请求参数: 执行操作的请求不包含任何攻击者无法确定或猜测其值的参数。例如,当要求用户更改密码时,即使攻击者需要知道现有密码的值,该功能也不会受到攻击。
2. 案例分析
假设一个应用程序包含一个允许用户更改其帐户电子邮件地址的功能。 当用户执行此操作时,他们会发出如下所示的 HTTP 请求:
POST /email/change HTTP/1.1Host: vulnerable-website.comContent-Type: application/x-www-form-urlencodedContent-Length: 3Cookie: session=yvthwsztyeQkAPzeQ5gHgTvlyxHfsAfE
email=wiener@normal-user.com这就满足了必要条件
- 攻击者会关注用户账户电子邮件地址的更改行为。在此操作之后,攻击者通常能够触发密码重置,并完全控制用户的账户。
- 该应用程序使用会话 cookie 来识别发出请求的用户。没有其他令牌或机制来跟踪用户会话。
- 攻击者可以很容易地确定执行该操作所需的请求参数值。
满足了这些条件,我们就可以构建一个包含恶意代码的网页
<html> <body> <form action="https://vulnerable-website.com/email/change" method="POST"> <input type="hidden" name="email" value="pwned@evil-user.net" /> </form> <script> document.forms[0].submit(); </script> </body></html>如果受害用户访问攻击者的网页,将会发生以下情况:
- 攻击者的页面将触发向存在漏洞的网站发出 HTTP 请求。
- 如果用户已登录到存在漏洞的网站,则其浏览器会自动将会话 cookie 包含在请求中(假设未使用SameSite cookie
- 存在漏洞的网站会按正常方式处理请求,将其视为受害用户发出的请求,并更改其电子邮件地址。
03 攻击
1. 攻击构建
构建 CSRF 攻击的最简单方法是使用Burp Suite Professional 内置的CSRF PoC 生成器:
- 在 Burp Suite Professional 中选择要测试或利用的请求。
- 从右键菜单中,选择“互动工具”/“生成 CSRF PoC”。
- Burp Suite 将生成一些 HTML 代码来触发选定的请求(不包括 cookie,cookie 将由受害者的浏览器自动添加)。
- 可以在 CSRF PoC 生成器中调整各种选项,以微调攻击的各个方面。在某些特殊情况下,可能需要这样做来处理请求的特殊特性。
- 将生成的 HTML 复制到网页中,在已登录到易受攻击网站的浏览器中查看,并测试是否成功发出预期请求并执行所需操作。2. 攻击步骤
1. 用户登录目标网站(如银行网站)并获得身份认证 Cookie。 这一步利用自动 Cookie 发送机制。在银行网站设置身份认证 Cookie 后,浏览器会自动将此 Cookie 附加到发送给该网站的每个请求中。
2. 用户在未退出的情况下访问恶意网站。 此时,由于同源策略,恶意站点不能直接读取或修改银行站点的 Cookie。这保护用户的身份信息不被直接窃取。
3. 恶意站点包含向目标站点的请求(如转账操作)。 虽然同源策略限制跨来源访问,但它允许跨来源网络请求,如通过 <img>、<form> 标签发起的请求。攻击者利用这个“漏洞”。
4. 用户的浏览器自动发送此请求,并附带目标站点的 Cookie。 这是 CSRF 攻击的核心。它利用了同源策略允许跨来源请求和自动 Cookie 发送机制(即使是恶意站点触发的请求也会携带匹配域的 Cookie)。
5. 目标站点接收请求,验证 Cookie 有效并执行操作。 服务器无法判断此请求是否来自合法用户操作,因为附带的 Cookie 是有效的。3. 攻击实施
GET请求
我们可以将GET请求插入一个img标签中这样访问就会被攻击
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<img src="http://www.baidu.com" >
</body></html>POST请求
像是一个from表单我们也可以伪造,受害者电脑就会自动提交,猝不及防
<form action="http://bank.example/withdraw" method=POST><input type="hidden" name="account" value="xiaoming" /><input type="hidden" name="amount" value="10000" /><input type="hidden" name="for" value="hacker" /></form><script> document.forms[0].submit(); </script>链接型请求
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">重磅消息!! <a/>04 防御
1. CSRF令牌
CSRF令牌是由服务器端应用程序生成并与客户端共享的唯一、秘密且不可预测的值
防御原理
1. 服务器为每个会话生成一个唯一且难以预测的令牌。2. 该令牌嵌入到所有需要的表单中。3. 当用户提交表单时,服务器验证令牌的有效性。无法获悉或猜测该值(因为每个会话都不同),所以说即使欺骗用户发送请求,服务器也会因缺少有效的 CSRF 令牌而拒绝请求。
示例 在服务器端(使用 Node.js 和 Express):
const express = require('express');const crypto = require('crypto');const app = express();n// 生成 CSRF 令牌function generateCSRFToken() { return crypto.randomBytes(16).toString('hex');}
// CSRF 防护中间件app.use((req, res, next) => { if (req.method === 'GET') { // 为每个 GET 请求生成新的 CSRF 令牌 req.session.csrfToken = generateCSRFToken(); } else if (['POST', 'PUT', 'DELETE'].includes(req.method)) { // 检查 CSRF 令牌 const submittedToken = req.body._csrf || req.headers['x-csrf-token']; if (submittedToken !== req.session.csrfToken) { return res.status(403).json({ error: 'CSRF token validation failed' }); } } next();});在前端 JavaScript 中:
// 发送带有 CSRF 令牌的请求async function sendRequestWithCSRFToken(url, method, data) { const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); const response = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify(data) }); return response.json();}2. 同源策略相关
这里需要有关同源策略的前置知识,我在另一篇中进行了单独整理。
检查 Referer 标头
Referer 标头包含发起请求的页面的 URL。通过检查 Referer 标头,服务器可以判断请求是否来自合法来源。
由于 CSRF 攻击通常来自不同的域,Referer 标头将显示攻击者的域名。通过验证 Referer 是否为预期值,可以阻止来自未知来源的请求。
但是
学过HTTP协议我们可以发现每一个异步请求都会携带两个Header,用于标记来源域名:
-
Origin Header
-
Referer Header
我们可以通过上面的两个请求头判断是否为用户,但是不够安全为了隐私有的用户可能不提供referer值,那我们也不可能对所有不提供请求头的进行拦截。所以需要其它方式。
SameSite Cookie
谷歌提出了same-site cookies概念,same-site cookies 是基于 Chrome 和 Mozilla 开发者花了三年多时间制定的 IETF 标准。它是在原有的Cookie中,新添加了一个SameSite属性,它标识着在非同源的请求中,是否可以带上Cookie,它可以设置为3个值,分别为:Strict、Lax、None Strict是最严格的,它完全禁止在跨站情况下,发送Cookie。只有在自己的网站内部发送请求,才会带上Cookie。不过这个规则过于严格,会影响用户的体验。比如在一个网站中有一个链接,这个链接连接到了GitHub上,由于SameSite设置为Strict,跳转到GitHub后,GitHub总是未登录状态。 Lax的规则稍稍放宽了些,大部分跨站的请求也不会带上Cookie,但是一些导航的Get请求会带上Cookie,如下:
| 请求类型 | 示例 | Lax情况 |
|---|---|---|
| 链接 | <a href=".."></a> | 发送 Cookie |
| 预加载 | <link rel="prerender" href=".."/> | 发送 Cookie |
| GET 表单 | <form method="GET" action=".."> | 发送 Cookie |
| POST 表单 | <form method="POST" action=".."> | 不发送 |
| iframe | frameLabelStart--frameLabelEnd | 不发送 |
| AJAX | $.get("..") | 不发送 |
| Image | <img src=".."> | 不发送 |
| None就是关闭SameSite属性,所有的情况下都发送Cookie。不过SameSite设置None,还要同时设置Cookie的Secure属性,否则是不生效的。 |
简言之
SameSite 是一个 Cookie 属性, 用于控制 Cookie 是否随着跨站请求发送。它有三个可能的值:
Strict: Cookie 仅在同站请求中发送。Lax: Cookie 在同站请求和顶级导航中发送。None: Cookie 在所有跨站请求中发送(必须与Secure属性一起使用)。
当 SameSite 设置为 Strict 时,可以完全防止第三方网站发送 Cookie,从而有效防止 CSRF 攻击。 如果 SameSite 设置为 Lax,可以在保护敏感操作的同时,允许一些常见的跨站用例(如从外部链接进入网站)。
示例
res.cookie('session_id', 'abc123', { httpOnly: true, secure: true, sameSite: 'strict'});使用自定义请求头
对于 AJAX 请求,可以添加自定义请求头。由于同源策略的限制,攻击者无法在跨站请求中设置自定义头。服务器可以检查此自定义头的存在以验证请求的合法性。示例 在前端
fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify(data)});在服务器端
app.use((req, res, next) => { if (req.xhr || req.headers['x-requested-with'] === 'XMLHttpRequest') { // 处理 AJAX 请求 } else { // 处理非 AJAX 请求 } next();});3. 本域
token验证
比如我们可以加一个一次性生成的token,根据浏览器的同源策略这个token并不能被csrf获取让每个请求带上这个token并进行验证(这里的token可以放在http请求的参数里),这样的话,CSRF无法获取Token从而隔绝攻击,当然实战渗透测试的时候可能有些网站会有token但并无作用- -
验证码验证
也可以在每个请求上增加验证码,不过这样子会很影响用户体验
双重cookie验证
双重Cookie采用以下流程:
-
在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如
csrfcookie=v8g9e4ksfhw)。 -
在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例
POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw)。 -
后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。
此方法相对于CSRF Token就简单了许多。简单来讲就是请求中要带着cookie里的参数,因为CSRF无法获取Cookie,所以变相的防御了CSRF。
当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有CSRF Token高,原因我们举例进行说明。
由于任何跨域都会导致前端无法获取Cookie中的字段(包括子域名之间),于是发生了如下情况:
如果用户访问的网站为www.a.com,而后端的api域名为api.a.com。那么在www.a.com下,前端拿不到api.a.com的Cookie,也就无法完成双重Cookie认证。
于是这个认证Cookie必须被种在a.com下,这样每个子域都可以访问。
任何一个子域都可以修改a.com下的Cookie。
某个子域名存在漏洞被XSS攻击(例如upload.a.com)。虽然这个子域下并没有什么值得窃取的信息。但攻击者修改了a.com下的Cookie。
攻击者可以直接使用自己配置的Cookie,对XSS中招的用户再向www.a.com下,发起CSRF攻击。