CSRF Protection Bypass in Play Framework
2020年8月20日 - 作者:Luca Carettoni
这篇博客文章展示了我们在客户 engagement 期间发现的 Play 框架中的一个漏洞。该问题允许在特定配置下完全绕过跨站请求伪造(CSRF)保护。
根据官方描述,Play Framework 是一个用于 Java 和 Scala 的高性能 Web 框架。它构建在 Akka 之上,Akka 是一个用于构建高度并发、分布式和弹性消息驱动应用程序的工具包。
Play 是一个广泛使用的框架,部署在大小组织的 Web 平台上,例如 Verizon、Walmart、The Guardian、LinkedIn、Samsung 等许多公司。
传统的反 CSRF 机制
在框架的旧版本中,CSRF 保护是通过一个不安全的基线机制提供的 - 即使 HTTP 请求中不存在 CSRF 令牌。
该机制基于简单请求(Simple Requests)和预检请求(Preflighted Requests)之间的基本差异。让我们深入了解这些细节。
简单请求有严格的规则集。只要遵循这些规则,用户代理(例如浏览器)就不会发出 OPTIONS 请求,即使这是通过 XMLHttpRequest 发出的。所有规则和细节可以在 Mozilla 开发者页面查看,虽然我们主要关注 Content-Type 规则集。
简单请求的 Content-Type 头可以包含以下三个值之一:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
如果您指定不同的 Content-Type,例如 application/json,则浏览器将发送 OPTIONS 请求以验证 Web 服务器是否允许此类请求。
现在我们理解了预检请求和简单请求之间的区别,我们可以继续了解 Play 过去如何保护免受 CSRF 攻击。
在框架的旧版本中(直到版本 2.5 包括),对接收到的 Content-Type 头使用黑名单方法作为 CSRF 预防机制。
在 2.8.x 迁移指南中,我们可以看到用户如何恢复 Play 的旧默认行为(如果遗留系统或其他依赖项需要):
|
|
在上面的代码片段中,我们可以看到旧保护的核心。contentType.blackList 设置包含三个值,这些值与"简单请求"的内容类型相同。这被认为是有效的(虽然不理想)保护,因为可以防止以下场景:
-
attacker.com 嵌入一个
<form>
元素,该元素发布到 victim.com- 表单允许 form-urlencoded、multipart 或 plain,这些都被机制阻止
-
attacker.com 使用 XHR 以 application/json 方式 POST 到 victim.com
- 由于 application/json 不是"简单请求",将发送 OPTIONS 并且(假设配置正确)CORS 将阻止请求
-
victim.com 使用 XHR 以 application/json 方式 POST 到 victim.com
- 这按预期工作,因为请求不是跨站点而是在同一域内
因此,您现在有了 CSRF 保护。或者真的吗?
寻找绕过方法
有了这些知识,我们首先想到的是需要让浏览器发出一个不触发预检且不匹配 contentType.blackList 设置中任何值的请求。
我们做的第一件事是映射出我们可以修改而不发送 OPTIONS 预检的请求。这归结为单个请求:Content-Type: multipart/form-data
由于边界值,这立即显得有趣:Content-Type: multipart/form-data; boundary=something
描述可以在这里找到: 对于多部分实体,需要 boundary 指令,该指令由 1 到 70 个字符组成,这些字符来自已知通过电子邮件网关非常强大的字符集,并且不以空格结尾。它用于封装消息多个部分的边界。通常,头边界前面有两个破折号,最终边界末尾附加两个破折号。
因此,我们有一个实际上可以用许多不同字符修改的字段,并且所有这些都由攻击者控制。
现在我们需要深入研究这些头的解析。为了做到这一点,我们需要查看 Akka HTTP,这是 Play 框架的基础。
查看 HttpHeaderParser.scala,我们可以看到这些头总是被解析:
|
|
解析规则可以在 HeaderParser.scala 中看到,它遵循 RFC 7230 超文本传输协议(HTTP/1.1):消息语法和路由,2014 年 6 月。
|
|
如果不遵守这些解析规则,该值将被设置为 None。完美!这正是我们绕过 CSRF 保护所需要的 - 一个"简单请求",然后将其设置为 None,从而绕过黑名单。
我们如何实际伪造一个被浏览器允许,但被 Akka HTTP 解析代码视为无效的请求?
我们决定让模糊测试来回答这个问题,并很快发现以下转换有效:Content-Type: multipart/form-data; boundary=—some;randomboundaryvalue
边界值内的额外分号可以解决问题并将请求标记为非法:
|
|
响应:
|
|
这也可以通过查看开发模式下服务器的日志来确认:
|
|
并通过检测 Play 框架代码以打印 Content-Type 的值:
|
|
最后,我们构建了以下概念验证并通知了我们的客户(以及 Play 框架维护者):
|
|
致谢与披露
该漏洞由 Kevin Joensen 发现,并于 2020 年 4 月 24 日通过 security@playframework.com 报告给 Play 框架。该问题已在 Play 2.8.2 和 2.7.5 中修复。CVE-2020-12480 和所有详细信息已于 2020 年 8 月 10 日由供应商发布。感谢 Lightbend 的 James Roper 提供的协助。