简介

​ GitLab 是一个用于仓库管理系统的开源项目,使用 Git 进行代码管理,并支持SAML认证功能,通过认证的用户可以实现单点登录。然而,Ruby-SAML库存在缺陷,允许攻击者绕过 SAML 身份验证机制进行未经授权的访问,而这就波及到了Gitlab的登录功能。

SAML认证

​ SAML是一种基于 XML 的开放标准,用于在身份提供者 (IdP) 和服务提供商 (SP) 之间交换身份验证和授权数据,确保此交换安全性。

SAML涉及实体

  • principal(主体):通常代表用户/浏览器终端
  • IdP(身份提供商):负责进行身份认证,并将用户的认证信息和授权信息传递给SP。
  • SP(服务提供商):验证用户的认证信息,并授权用户访问指定的资源信息。

认证过程

1、当用户(principal)想要登录时,首先请求SP。

2、SP 生成 SAML 请求,通过浏览器重定向,发送给IdP 。

3、IdP 解析 SAML 请求并将用户重定向到认证页面。

4、用户在认证页面完成登录。

5、IdP 生成 SAML Response,通过对浏览器重定向,发送给SP,其中包含 SAML Assertion 用于确定用户身份。

6、SP 对 SAML Response 的内容进行检验。

7、检验通过后根据认证信息成功登录。

Signature

​ 在IdP返回的SAML Response中包含了SAML Assertion,它主要用于身份验证,其中Signature标签是身份验证的核心。

这是一段完整的SAML Response示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<saml:Response xmlns:saml="urn:oasis:names:tc:SAML:2.0:protocol" 
ID="_responseID"
Version="2.0"
IssueInstant="2024-10-25T12:00:00Z"
Destination="https://sp.example.com/">
<saml:Issuer>https://idp.example.com/</saml:Issuer>
<saml:Status>
<saml:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml:Status>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_exampleID"
Version="2.0"
IssueInstant="2024-10-25T12:00:00Z">
<saml:Issuer>https://idp.example.com/</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">user@example.com</saml:NameID>
</saml:Subject>
<saml:AuthnStatement AuthnInstant="2024-10-25T12:00:00Z"/>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="#_exampleID">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<ds:DigestValue>DigestValue</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>SignatureValue</ds:SignatureValue>
</ds:Signature>
</saml:Assertion>
</saml:Response>

摘取Assertion部分重点看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" 
ID="_exampleID"
Version="2.0"
IssueInstant="2024-10-25T12:00:00Z">
<saml:Issuer>https://idp.example.com/</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">user@example.com</saml:NameID>
</saml:Subject>
<saml:AuthnStatement AuthnInstant="2024-10-25T12:00:00Z"/>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="#_exampleID">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<ds:DigestValue>DigestValue</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>SignatureValue</ds:SignatureValue>
</ds:Signature>
</saml:Assertion>
  • Signature:主要分为两部分SignedInfo、SignatureValue

    • SignedInfo:SignedInfo中有个重要的标签<ds:DigestValue>,他主要存储的是Assertion 部分(删除Signature节点后)的哈希值
  • SignatureValue:当SignedInfo中的值计算好后,就会使用 IdP 的私钥对整个 SignedInfo 进行签名,并将结果放置在SignatureValue元素中。

了解完这些后再捋下大体流程:

​ 用户登录时会请求SP,SP带着请求重定向给IdP,Idp接收到请求后生成SAML Response(其中就包含计算好的DigestValue和SignatureValue),SP接收到SAML Response后会做两次验证:

  1. 签名验证:SP 使用 IdP 的公钥来验证SignedInfo块上的签名(对SignedInfo进行hash计算,并与公钥解密SignatureValue后的值进行对比)。如果签名有效,则确认 IdP 已签署该消息,并且该消息未被修改。
  2. 摘要验证:SP 计算Assertion(删除Signature节点)的值,与DigestValue进行比较,如果相同则断言没有被篡改。

验证通过后用户成功登录。

Xpath节点选择

​ Gitlab的认证绕过,正是因为Xpath的解析机制导致的,在Gitlab的源码中发现,它是通过 //ds:DigestValue获取DigestValue的。

而Xpath共有选取节点方式共有三种:

  • /ds:DigestValue:从根节点开始选择节点,直接指向第一个ds:DigestValue 元素。
  • ./ds:DigestValue:从当前节点开始查找 ds:DigestValue 元素
  • //ds:DigestValue:从文档中的任何位置选择节点,包括所有嵌套节点中查找。、

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<samlp:Response>
<samlp:Extensions >
<ds:DigestValue >
123
</ds:DigestValue>
</samlp:Extensions>
<ds:DigestValue>
456
</ds:DigestValue>
<saml:Assertion >
<ds:Signature>
<ds:SignedInfo>
<ds:DigestValue>789</ds:DigestValue>
</ds:SignedInfo>
</ds:Signature>
</saml:Assertion>
</samlp:Response>

执行/ds:DigestValue后返回值为空,ds:DigestValue 是在 samlp:Response 的子元素下,但因为 XPath 从根开始查找,并没有在根节点上找到 ds:DigestValue,因此该表达式将返回空集合。

执行./ds:DigestValue在 samlp:Response 下有一个直接的 ds:DigestValue 元素,所以是456

//ds:DigestValue是在任何位置节点选择,所以获取到123、456、789

Bypass分析

​ Gitlab获取xpath的方式为:

1
2
3
4
5
encoded_digest_value = REXML::XPath.first(
ref,
"//ds:DigestValue",
{ "ds" => DSIG }
)

会获取到全文中的第一个DigestValue,如果我们伪造一个DigestValue放到最开始位置,那根据SP两次验证原理即可构造绕过

原认证过程:

1
2
1. 签名验证:SP 使用 IdP 的公钥来验证SignedInfo块上的签名(对SignedInfo进行hash计算,并用公钥解密SignatureValue后的值进行对比)。如果签名有效,则确认 IdP 已签署该消息,并且该消息未被修改。
2. 摘要验证:SP 计算Assertion(删除Signature节点)的值,与DigestValue进行比较,如果相同则断言没有被篡改。

​ 在摘要验证部分,会计算Assertion(删除Signature节点)的值与DigestValue进行比较,如果将Assertion(除Signature节点)的部分参数进行修改,如前边示例中的NameID等 :

1
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">root</saml:NameID>

​ 将原始NameID等信息修改,此时DigestValue一定也会发生改变,通过计算出新的DigestValue值,并将他放到整个Response最前方,当SP获取到新的Assertion属性并进行hash计算和DigestValue进行比较,通过验证后即可进行身份绕过。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<saml:Response 
....
<samlp:Extensions>
<DigestValue xmlns="http://www.w3.org/2000/09/xmldsig#">
new DigestValue
</DigestValue>
</samlp:Extensions>

<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
.....
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
......
<ds:DigestValue>DigestValue</ds:DigestValue>
</ds:SignedInfo>
<ds:SignatureValue>SignatureValue</ds:SignatureValue>
</ds:Signature>
</saml:Assertion>
</saml:Response>

​ 在原始数据中加入samlp:Extensions并将新的DigestValue值嵌入其中,Gitlab通过//ds:DigestValue表达后就获取到了新的new DigestValue,并与通过hash计算后的新的Assertion属性进行对比,便通过了摘要验证部分,而自始至终我们并没有修改Signature的值,因此签名验证便也通过了验证,进行了身份绕过。

以下为Nuclei的利⽤模板:

cloud.projectdiscovery.io/?template=CVE-2024-45409&ref=blog.projectdiscovery.io

绕过视频:

https://blog.projectdiscovery.io/content/media/2024/10/Screen-Recording-2024-10-04-at-10.13.31-PM.mp4

修复

​ 在后续版本中,Ruby SAML将//改为了./便无法再通过构造DigestValue值达到绕过的目的。

Release 1.12.3 to include critical vulnerability CVE-2024-45409 fix · SAML-Toolkits/ruby-saml@1ec5392 · GitHub

image-20241101100027302

Ruby-SAML Bypass && GitLab SAML Bypass(CVE-2024-45409)

https://blog.projectdiscovery.io/ruby-saml-gitlab-auth-bypass/