SAML轮盘赌:黑客总是赢家 | PortSwigger研究
引言
在这篇文章中,我们将精确展示如何通过链式往返攻击和命名空间混淆,利用ruby-saml库实现对GitLab企业版的未授权管理员访问。
在研究过程中,GitHub独立发现并修补了我们的漏洞。然而,他们的披露遗漏了关键的技术细节,包括特定的变异方式以及如何在没有认证的情况下利用它。 https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/
我们相信分享这些攻击的完整细节对于提高安全性至关重要,通过赋予每个人识别、缓解和有效防御此类威胁所需的知识。
这项研究始于我们阅读了Juho Forsén的一篇关于XML往返漏洞的精彩文章。最初的好奇心迅速演变为对SAML复杂性的深入探索,揭示的内容远超我们最初的预期。我们花了数月时间探索各种往返攻击,目标是在Black Hat上展示我们的发现。然而,运气使然,我们与Alexander Tan(ahacker1)发生了研究碰撞,导致我们的发现在提交前就被修补。尽管有此转折,我们相信这项工作仍然值得分享,虽然今年可能不会登上Black Hat,但我们希望您能发现它同样引人入胜。
往返攻击101
SAML库通常解析XML文档,将其存储为字符串,然后重新解析。在Ruby-SAML中,这个过程涉及两个不同的解析器:REXML用于解析文档和验证签名,而Nokogiri用于访问属性。如果在此过程中发生任何变异,文档在第二次解析时可能不完全相同。
为了安全授权,文档必须一致地解析和序列化;否则,可能会出现结构不一致。这些不一致可以在往返攻击中被利用。通过利用XML注释和CDATA部分,攻击者可以在变异过程中操纵文档的结构,绕过签名验证,并有效地通过冒充其他用户的身份获得未授权访问。
Ruby SAML/REXML上的往返攻击
为了便于测试,我们开发了一个测试平台来识别往返漏洞并高效评估多个SAML库。我首先检查了文档类型定义(DOCTYPE),因为过去曾发现过类似的漏洞。我的初始方法侧重于分析XML实体如何被解析,因此我在该领域进行了测试。
在Juho的原始发现中,使用了符号声明来引入引号解释的不一致性。在此基础上,我调查了是否有任何额外的漏洞被忽略。经过大量测试,我发现可以在SYSTEM标识符中引入变异。
在文档的初始解析过程中,遇到的第一个标签是原始的“assertion”:
|
|
然而,在重新解析文档时,结果完全改变,现在反映了攻击者的“assertion”:
|
|
如图所示,单引号的系统标识符被转换为双引号。然而,由于标识符内部包含双引号,这改变了XML文档的语法,导致XML注释被处理,并产生完全不同的节点。我的高技能同事Zak将这个变异精炼成一个更 streamlined 和有效的攻击向量:
|
|
这个向量允许通过操纵文档和伪造断言来利用GitLab和任何其他使用Ruby-SAML库的应用程序,有效地使攻击者能够以任何用户身份登录。然而,这只是攻击的一部分。我的同事Zak将展示如何将其升级以实现GitLab上的未授权管理员访问。
通过往返攻击在Gitlab上提升权限
理解漏洞
GitLab依赖Ruby-SAML库进行SAML认证。然而,为了实现未授权访问,我们需要仔细查看验证过程,因为它在攻击中扮演关键角色。
在往返发生之前,库会验证SAMLResponse是否包含文档中嵌入的有效证书。这是通过计算证书的哈希值并与服务器上存储的指纹进行比较来完成的。之后,该证书用于验证签名。请记住,签名是此攻击的一个关键方面,因为它允许在没有组织凭证的情况下完全接管账户。
签名验证过程
一旦从SAMLResponse中提取出证书,实际的签名验证过程就开始了。首先,文档从其内存表示转换回XML格式。这就是Gareth的往返攻击发挥作用的地方。在这个阶段,库忽略攻击者断言,并继续验证原始元素的签名。
如果攻击者以绕过签名验证的方式伪造断言元素,额外的安全检查就会启动。最关键的检查包括:
- 签名元素的ID应在两个文档中匹配。
- 规范化属性(用于规范化XML文档)必须在两个版本中相同。
然而,所有其他验证检查都针对攻击者断言而不是原始签名文档进行操作。这允许攻击者任意修改验证字段而不破坏签名验证过程:
|
|
克服XML模式限制
伪造签名XML文档的一个挑战是XML模式验证是使用Nokogiri和预定义的模式文件执行的。这提出了一个限制:攻击者要伪造有效的签名XML文档,必须首先获得一个通过XML模式验证的文档。
理解XML模式验证
XML模式定义了SAML XML文档的结构,指定了:
- 有效的元素和属性
- 子元素的顺序和数量
- 元素和属性的数据类型
换句话说,签名元素必须是有效的SAML协议元素——例如登录响应、注销响应或元数据。您可能在开发者论坛上找到签名的XML文档,但这种情况不太可能。因此,我们将采取不同的方法。相反,我们引入了命名空间混淆攻击,它允许对任何使用Ruby-SAML的应用程序进行未授权访问。
对Gitlab的未授权访问
在深入攻击之前,让我们回顾一下SAML模式验证的工作原理。身份提供者(IdP)只签名Signature节点,而不是整个断言。
由于Ruby-SAML使用两个XML解析器:
- REXML读取Signature元素。
- Nokogiri读取DigestValue。
这两个解析器之间的差异可以允许我们绕过签名验证。
Ruby-SAML使用XPath查询搜索Signature元素:
|
|
这里,ds指的是XML命名空间。通常,命名空间防止元素名称冲突,但我们利用了XPath搜索中命名空间解释的差异。
考虑以下场景:
|
|
第一个Signature元素缺乏直接的命名空间声明(xmlns=“http://www.w3.org/2000/09/xmldsig#")。相反,我们使用XML Doctype技巧:
安全专家通常关注XXE攻击中的!ENTITY声明,但!ATTLIST声明也可以用于利用。!ATTLIST定义了Signature元素并为其分配了一个命名空间属性。REXML和Nokogiri都支持基于doctype的命名空间声明,但REXML有一个关键缺陷:
- XML标准禁止具有相同名称的重复属性。
- 然而,REXML在doctype声明中忽略此限制。
这允许攻击者定义两个冲突的命名空间属性,其中第二个覆盖第一个。
结果,REXML读取一个假的摘要值,而Nokogiri读取真的摘要值。
利用策略
要利用此差异: 创建两个Signature元素:
- 一个具有有效的摘要值。
- 一个具有伪造的摘要值。
这允许攻击者绕过Ruby-SAML的摘要验证过程。
通过结合命名空间混淆与往返攻击利用Ruby < 3.4.2
虽然命名空间混淆 alone 可以利用Ruby-SAML,但它面临一个限制:
REXML对XML编组/解组的处理不佳引入了另一个往返问题。在Ruby 3.4.2之前,REXML在doctype声明中截断!ATTLIST字符串,使利用变得脆弱。在GitLab中,这破坏了攻击,但结合两个漏洞仍然可以使用:
- 第一次XML解析:REXML最初忽略!ATTLIST值,将其视为字符串文字。
- 第二次XML解析:REXML然后识别!ATTLIST声明,导致完全利用。
利用WS-Federation获取签名XML
找到一个有效的签名XML文档可能具有挑战性。幸运的是,身份提供者(IdPs)默认为每个租户静默支持单点登录协议:WS-Federation。WS-Federation提供签名的元数据XML端点,例如: https://login.microsoftonline.com/contoso.onmicrosoft.com….
联邦元数据文档对任何未授权用户都是公开可访问的——所需要的只是应用程序的唯一ID,可以轻松从身份提供者的URL中提取或使用搜索引擎找到。
虽然此元数据不是有效的SAML元数据文档,但命名空间混淆攻击只需要一个有效的Signature元素——一个使用与服务提供者存储的相同证书签名的元素。而它确实是。
通过使用此公开可用的签名文档,攻击者可以:
- 提取合法的Signature元素。
- 伪造一个假的签名断言。
- 完全绕过GitLab SAML认证。
结论
这次攻击突显了如何通过结合往返攻击与命名空间混淆来实现对GitLab的未授权访问。漏洞源于不同XML解析器处理文档验证的不一致性,允许攻击者操纵签名验证。
关键要点:
- XML解析的差异可能引入可利用的不一致性。
- 命名空间混淆可用于绕过签名验证。
- 合法的签名WS-Federation元数据XML可以被重新用于伪造断言。
缓解措施
为防止此类攻击,请确保使用相同的库来解析和验证签名XML文档。避免编组和解组不受信任的用户数据。这些漏洞已在GitLab社区版(CE)和企业版(EE)的17.9.2、17.8.5、17.7.7版本中修复。
确保在X(前Twitter)和Bluesky上关注我们,并加入官方PortSwigger Discord以保持更新!