:::info 跨站请求伪造(Cross-site Request Forgery,简称 CSRF)是一种挟制用户在当前已登录的 Web 页面上执行非本意的操作的攻击方法。

CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。 XSS 相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

:::

攻击

CSRF 漏洞即利用网站权限校验漏洞在用户不知觉情况下发送请求,达到 伪装 用户的目的。 典型的 CSRF 攻击有着如下的流程:
  1. 受害者登录 a.com,并保留了登录凭证 Cookie
  2. 攻击者 引诱 受害者访问了 b.com
  3. b.com a.com 发送了一个请求:a.com/act=xx,浏览器会默认携带 a.com 的 Cookie
  4. a.com 接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求
  5. a.com 以受害者的名义执行了 act=xxx
  6. 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让 a.com 执行了自己定义的操作
攻击者利用 CSRF 实现的攻击主要方式:
  • 攻击者能够欺骗受害用户完成该受害者所允许的任一状态改变的操作 - 如:更新账号信息、完成购物、注销甚至登陆等操作
  • 获取用户的隐私数据
  • CSRF 蠕虫
  • 配合其他漏洞攻击

GET 类型

在受害者访问含有这个 img 的页面后,浏览器会自动向 http://bank.example/transfer?account=xiaoming&amount=10000&for=hacker 发出一次 HTTP 请求。bank.example 就会收到包含受害者登录信息的一次跨域请求。
  1. <img src="http://bank.example/transfer?amount=10000&for=hacker" />

POST 类型

这种类型的 CSRF 利用起来通常使用的是一个自动提交的表单,如:
  1. <form action="http://bank.example/transfer" method="POST">
  2. <input type="hidden" name="account" value="xiaoming" />
  3. <input type="hidden" name="amount" value="10000" />
  4. <input type="hidden" name="for" value="hacker" />
  5. </form>
  6. <script>
  7. document.forms[0].submit();
  8. </script>
访问该页面后,表单会自动提交,相当于模拟用户完成了一次 POST 操作。 POST 类型的攻击通常比 GET 要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许 POST 上面。

链接类型

链接类型的 CSRF 并不常见,比起其他两种用户打开页面就中招的情况,这种 需要用户点击链接 才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:
  1. <a href="http://test.com/csrf/transfer.php?amount=1000&for=hacker" taget="_blank">重磅消息!!</a>
由于之前用户登录了信任的网站,并且保存登录状态,只要用户主动访问上面的这个 PHP 页面,则表示攻击成功。

攻击特点

  • 攻击 一般发起在第三方网站,而不是被攻击的网站,被攻击的网站无法防止攻击发生
  • 攻击 利用受害者被攻击网站的登录凭证,冒充受害者提交操作,而不是直接窃取数据
  • 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是 冒用
  • 跨站请求可以用各种方式:图片 URL、超链接、CORS、Form 表单提交等等,部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪
对于服务器返回的结果,由于浏览器 同源策略 的限制,黑客也无法进行解析。因此,黑客无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。所以,我们要保护的对象是那些可以直接产生数据改变的服务,而对于读取数据的服务,则不需要进行 CSRF 的保护。比如银行系统中转账的请求会直接改变账户的金额,会遭到 CSRF 攻击,需要保护。而查询金额是对金额的读取操作,不会改变数据,CSRF 攻击无法解析服务器返回的结果,无需保护。

防御策略

:::info

防御思路:
  1. 我们能不能 区分 一个请求是来自于自己的前端页面,还是第三方的网站?
  2. 怎么让自己的前端页面和伪造的请求变得 不一样 呢?
针对 CSRF 的特点制定防护策略:
  • 阻止不明外域访问 - 同源检测机制:服务器通过请求头附带的 Origin Referer 字段确定请求的来源域 - Samesite Cookie
  • 提交时要求附加本域才能获取的信息 - CSRF Token - 双重 Cookie 验证
  • 保证网络请求由真实用户发出 - 用户操作限制(验证码)

:::

同源检测机制

既然 CSRF 大多来自第三方网站,那么我们就直接禁止外域(或者不受信任的域名)对我们发起请求。 在 HTTP 协议中,每个一部请求都会携带两个 Header,用于标记来源域名: 这两个 Header 在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。服务器可以通过解析这两个 Header 中的域名,确定请求的来源域。

Cookie 的 SameSite 属性

Cookie 的 SameSite 属性能用于限制第三方 Cookie,从而减少安全风险。
  1. Set-Cookie: CookieName=CookieValue; SameSite=Lax;

CSRF Token

前面讲到 CSRF 的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用 Cookie 中的信息。而 CSRF 攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个 CSRF 攻击者无法获取到的 Token。服务器通过校验请求是否携带正确的 Token,来把正常的请求和攻击的请求区分开,也可以防范 CSRF 的攻击。 步骤:
  1. 用户使用用户名密码登陆,服务端下发一个 随机的 Token 字段,并且服务端把这个字段保存在 Session 中
  2. 客户端把这个 Token 保存起来,放到隐藏字段
  3. 用户在登录状态下,在之后访问的时候,都要携带这个 Token 字段
  4. 服务端从 Session 中拿出 Token 值进行对比,如果一致,说明请求合法
  5. 用户推出,Session 销毁,Token 失效

分布式校验

在大型网站中,使用 Session 存储 CSRF Token 会带来很大的压力。访问单台服务器 session 是同一个。但是现在的大型网站中,我们的服务器通常不止一台,可能是几十台甚至几百台之多,甚至多个机房都可能在不同的省份,用户发起的 HTTP 请求通常要经过像 Ngnix 之类的负载均衡器之后,再路由到具体的服务器上,由于 Session 默认存储在单机服务器内存中,因此在分布式环境下同一个用户发送的多次 HTTP 请求可能会先后落到不同的服务器上,导致后面发起的 HTTP 请求无法拿到之前的 HTTP 请求存储在服务器中的 Session 数据,从而使得 Session 机制在分布式环境下失效,因此在分布式集群中 CSRF Token 需要存储在 Redis 之类的公共存储空间。 由于使用 Session 存储,读取和验证 CSRF Token 会引起比较大的复杂度和性能问题,目前很多网站采用 Encrypted Token Pattern 方式。这种方法的 Token 是一个计算出来的结果,而非随机生成的字符串。这样在校验时无需再去读取存储的 Token,只用再次计算一次即可。 这种 Token 的值通常是使用 UserID、时间戳和随机数,通过加密的方法生成。这样既可以保证分布式服务的 Token 一致,又能保证 Token 不容易被破解。 在 Token 解密成功之后,服务器可以访问解析值,Token 中包含的 UserID 和时间戳将会被拿来被验证有效性,将 UserID 与当前登录的 UserID 进行比较,并将时间戳与当前时间进行比较。

双重 Cookie 验证

指在请求借口时,除了常规带上 Cookie 中的用户凭证信息,如 session_id 外,还把 Cookie 中的用户凭证信息读出来附在接口请求参数重;这种方案对比 CSRF Token 方案来说,好在不需要生成额外得 Token,也同样能够起到防御 CSRF 攻击的效果。 流程:
  • 在用户访问网站页面时,向请求域名注入一个 Cookie,内容为随机字符串(例如 csrfcookie=v8g9e4ksfhw);
  • 在前端向后端发起请求时,取出 Cookie,并添加到 URL 的参数中(接上例 POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw);
  • 后端接口验证 Cookie 中的字段与 URL 参数中的字段是否一致,不一致则拒绝。
此方法相对于 CSRF Token 就简单了许多。可以直接通过前后端拦截的的方法自动化实现。后端校验也更加方便,只需进行请求中字段的对比,而不需要再进行查询和存储 Token。 当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有 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 攻击。

用户操作限制

CSRF 攻击过程中,用户是在不知情的情况下构造了网络请求,因此添加验证码能强制用户必须与应用进行交互,服务器通过验证码来识别是不是用户主动发送的请求,由于一定强度的验证码机器无法识别,因此危险网站不能伪造一个完整的请求。
  • 优点:简洁有效,低成本
  • 缺点:对用户不友好,无法给所有的操作都加上验证码

绕过

CARF Token

CSRF 令牌的验证取决于请求方法

某些应用程序在请求使用 POST 方法时验证令牌,但是在使用 GET 方法时跳过验证,这时候我们可以将请求方法切换到 GET 方法绕过验证

CSRF Token 验证取决于令牌是否存在

某些应用程序会在令牌存在是验证令牌,但是如果令牌被省略则跳过验证。

所以我们可以删除令牌,来绕过验证

CSRF 令牌未绑定用户会话

某些应用程序不会验证令牌是否和发出请求的用户属于同一会话,相反应用程序维护它已发布的全局令牌池,并接受出现在该池中的任何令牌。

在这种情况下,攻击者可以使用自己的账号登陆应用程序获取有效令牌,然后将令牌提供给受害者用户进行 CSRF 攻击

CSRF 令牌绑定到非会话 COOKIE

某些应用程序将 CSRF 绑定到一个 COOKIE 但是这个 COOKIE 不是用于跟踪会话的 COOKIE,当应用程序使用两种不同的框架时,就容易发生这种情况,一个 COOKIE 用于会话处理,另一个用于 CSRF 保护

  1. POST /email/change HTTP/1.1
  2. Host: vulnerable-website.com
  3. Content-Type: application/x-www-form-urlencoded
  4. Content-Length: 68
  5. Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv
  6. csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY&email=wiener@normal-user.com

攻击者可以提取出 csrfKey 和 csrf 提供给受害者,然后利用 SET-COOKIE

  1. <img src="https://YOUR-LAB-ID.web-security-academy.net/?search=test%0d%0aSet-Cookie:%20csrfKey=YOUR-KEY%3b%20SameSite=None" onerror="document.forms[0].submit()">

CSRF 令牌只是在 COOKIE 中复制

应用程序值验证请求参数中提交的令牌是否和 COOKIE 中提交值匹配

  1. POST /email/change HTTP/1.1
  2. Host: vulnerable-website.com
  3. Content-Type: application/x-www-form-urlencoded
  4. Content-Length: 68
  5. Cookie: session=1DQGdzYbOJQzLP7460tfyiv3do7MjyPw; csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa
  6. csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa&email=wiener@normal-user.com

SamSite

参考原文 —> 链接

一个参考小文章 —> 当SameSite属性为默认值Lax时,绕过它并获得一个CSRF

Referrer

一些服务器使用 HTTP 的 Referer 标头来尝试防御 CSRF 攻击,通常是通过验证请求是否来自应用程序自己的域,这种方法效率比较低

Referer 的验证取决于 HTTP 头是否存在

某些应用程序 Referer 会在请求中出现标头时验证标头,但如果省略标头则跳过验证。

在这种情况下,攻击者可以通过导致受害用户的浏览器在生成的请求中删除标头的方式来设计他们的CSRF 漏洞。Referer 有多种方法可以实现这一点,但最简单的方法是在承载CSRF 攻击的 HTML 页面中使用 META 标记:

  1. <meta name="referrer" content="never">

绕过 Referer 验证

一些应用程序Referer以一种可以绕过的天真方式验证标头。例如,如果应用程序验证域以Referer预期值开头,则攻击者可以将其作为自己域的子域:

  1. http://vulnerable-website.com.attacker-website.com/csrf-attack

同样,如果应用程序只是验证Referer包含自己的域名,那么攻击者可以将所需的值放在 URL 的其他位置:

  1. http://attacker-website.com/csrf-attack?vulnerable-website.com

尽管您可以使用 Burp 识别此行为,但当您在浏览器中测试您的概念验证时,您通常会发现此方法不再有效。为了降低敏感数据以这种方式泄露的风险,许多浏览器现在Referer默认从标头中删除查询字符串。

您可以通过确保包含您的漏洞利用的响应Referrer-Policy: unsafe-url设置了标头来覆盖此行为(注意Referrer在这种情况下拼写正确,只是为了确保您注意!)。这确保将发送完整的 URL,包括查询字符串

参考

参考