SAML轮盘赌:黑客必胜的XML解析漏洞链利用

本文详细分析了如何通过组合XML往返攻击和命名空间混淆漏洞,在GitLab企业版中实现未授权管理员访问。研究揭示了ruby-saml库的深层安全缺陷,包括签名验证绕过技术和WS-Federation元数据的恶意利用。

SAML轮盘赌:黑客必胜 | PortSwigger研究

Gareth Heyes
研究员
@garethheyes

发布时间: 2025年3月18日 14:55 UTC
更新时间: 2025年3月31日 07:10 UTC

引言

本文将展示如何通过链接往返攻击和命名空间混淆,利用ruby-saml库在GitLab企业版上实现未授权管理员访问。

在研究过程中,GitHub独立发现并修补了我们的漏洞。然而,他们的披露遗漏了关键技术细节,包括特定变异方式以及如何在无认证情况下利用它。
https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/

我们相信分享这些攻击的完整细节对于提升安全至关重要,通过让所有人掌握有效识别、缓解和防御此类威胁所需的知识。

这项研究始于我们发现Juho Forsén关于XML往返漏洞的精彩文章。最初的好奇心很快转变为对SAML复杂性的深入探索,结果远超预期。我们花费数月研究各种往返攻击,目标是在黑帽大会上展示成果。然而巧合的是,我们与Alexander Tan(ahacker1)发生研究碰撞,导致发现被提前修补。尽管有此波折,我们仍认为这项研究值得分享。

往返攻击基础

SAML库通常解析XML文档,将其存储为字符串,随后重新解析。在Ruby-SAML中,此过程涉及两个不同解析器:REXML(用于解析文档和验证签名)和Nokogiri(用于访问属性)。如果在此过程中发生任何变异,文档在第二次解析时可能不再相同。

为确保安全授权,文档必须被一致地解析和序列化;否则可能出现结构不一致。这些不一致可被往返攻击利用。通过操纵XML注释和CDATA部分,攻击者可在变异过程中操纵文档结构,绕过签名验证,最终通过冒充其他用户身份获得未授权访问。

Ruby SAML/REXML往返攻击

为便于测试,我们开发了测试平台来识别往返漏洞并高效评估多个SAML库。我首先检查文档类型定义(DOCTYPE),因为过去曾发现类似漏洞。初始方法聚焦分析XML实体解析方式,并在此领域进行测试。

在Juho的原始发现中,使用符号声明引入引号解释不一致。基于此,我调查是否有其他漏洞被忽略。经大量测试,发现可在SYSTEM标识符内引入变异。

在文档初始解析期间,遇到的第一个标签是原始"assertion":

1
<![CDATA[<assertion>]]>

但重新解析文档时,结果完全改变,现在反映攻击者的"assertion":

1
<assertion>

如图所示,单引号系统标识符被转换为双引号。但由于标识符内部包含双引号,这改变了XML文档语法,导致XML注释被处理并产生完全不同的节点。我的同事Zak将此变异精炼为更简化的攻击向量:

1
<!DOCTYPE assertion SYSTEM "<!--><assertion>">

该向量允许通过操纵文档和伪造断言来利用GitLab及任何使用Ruby-SAML库的应用程序,使攻击者能够以任何用户身份登录。但这仅是攻击的一部分。我的同事Zak将展示如何将此升级为在GitLab上实现未授权管理员访问。

通过往返攻击在Gitlab实现权限提升

理解漏洞

GitLab依赖Ruby-SAML库进行SAML认证。但要实现未授权访问,需仔细查看验证过程,因其在攻击中起关键作用。

在往返发生前,库验证SAMLResponse是否包含文档中嵌入的有效证书。通过计算证书哈希并与服务器存储的指纹比较完成此操作。随后,此证书用于验证签名。请注意签名是此攻击的关键方面,因为它允许在无组织凭证情况下完全接管账户。

签名验证过程

从SAMLResponse提取证书后,实际签名验证过程开始。首先,文档从内存表示转换回XML格式。此时Gareth的往返攻击发挥作用。在此阶段,库忽略攻击者断言并继续验证原始元素的签名。

如果攻击者以绕过签名验证的方式伪造断言元素,则会触发额外安全检查。最关键检查包括:

  • 签名元素的ID应在两个文档中匹配
  • 规范化属性(标准化XML文档)必须在两个版本中相同

然而,所有其他验证检查均在攻击者断言而非原始签名文档上操作。这允许攻击者任意修改验证字段而不破坏签名验证过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
  <SignedInfo>
    <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
    <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
    <Reference URI="#_CONFUSED_NAMESPACE">
      <Transforms>
        <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
      </Transforms>
      <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
      <DigestValue>REAL_DIGEST</DigestValue>
    </Reference>
  </SignedInfo>
  <SignatureValue>...</SignatureValue>
</Signature>

克服XML模式限制

伪造签名XML文档的一个挑战是使用Nokogiri和预定义模式文件执行XML模式验证。这带来限制:攻击者要伪造有效签名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元素:

1
//ds:Signature

此处,ds指XML命名空间。通常命名空间防止元素名称冲突,但我们利用XPath搜索中命名空间解释的差异。

考虑以下场景:

1
2
3
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
  ...
</Signature>

第一个Signature元素缺少直接命名空间声明(xmlns=“http://www.w3.org/2000/09/xmldsig#")。相反,我们使用XML Doctype技巧:

1
2
3
<!DOCTYPE root [
<!ATTLIST Signature xmlns CDATA "http://www.w3.org/2000/09/xmldsig#">
]>

安全专家通常关注XXE攻击中的!ENTITY声明,但!ATTLIST声明也可用于利用。!ATTLIST定义Signature元素并为其分配命名空间属性。REXML和Nokogiri都支持基于doctype的命名空间声明,但REXML存在关键缺陷:

XML标准禁止具有相同名称的重复属性。但REXML在doctype声明中忽略此限制。

这允许攻击者定义两个冲突的命名空间属性,其中第二个覆盖第一个。

结果,REXML读取FAKE摘要值,而Nokogiri读取REAL摘要值。

1
2
3
<Signature xmlns="http://attacker.com/FAKE" xmlns="http://www.w3.org/2000/09/xmldsig#">
  <DigestValue>FAKE_DIGEST</DigestValue>
</Signature>

利用策略

要利用此差异:

  1. 创建两个Signature元素:
    • 一个带有有效Digest值
    • 一个带有伪造值

这允许攻击者绕过Ruby-SAML的摘要验证过程。

通过组合命名空间混淆与往返攻击利用Ruby < 3.4.2

虽然命名空间混淆单独可利用Ruby-SAML,但面临一个限制:

REXML对XML编组/解组的处理不佳引入另一个往返问题。在Ruby 3.4.2之前,REXML截断doctype声明中的!ATTLIST字符串,使利用变得脆弱。在GitLab中,这会破坏攻击,但组合两个漏洞仍可使用:

第一次XML解析:REXML初始忽略!ATTLIST值,将其视为字符串文字。
第二次XML解析:REXML随后识别!ATTLIST声明,导致完全利用。

利用WS-Federation获取签名XML

找到有效签名XML文档可能具有挑战性。幸运的是,身份提供者(IdP)默认为每个租户静默支持单点登录协议:WS-Federation。WS-Federation提供签名元数据XML端点,例如:

1
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以保持更新!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计