GitLab身份验证绕过(CVE-2024-45409)
简介
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 | <saml:Response xmlns:saml="urn:oasis:names:tc:SAML:2.0:protocol" |
摘取Assertion部分重点看下:
1 | <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" |
Signature:主要分为两部分SignedInfo、SignatureValue
- SignedInfo:SignedInfo中有个重要的标签
<ds:DigestValue>
,他主要存储的是Assertion 部分(删除Signature节点后)的哈希值
- SignedInfo:SignedInfo中有个重要的标签
SignatureValue:当SignedInfo中的值计算好后,就会使用 IdP 的私钥对整个 SignedInfo 进行签名,并将结果放置在SignatureValue元素中。
了解完这些后再捋下大体流程:
用户登录时会请求SP,SP带着请求重定向给IdP,Idp接收到请求后生成SAML Response(其中就包含计算好的DigestValue和SignatureValue),SP接收到SAML Response后会做两次验证:
- 签名验证:SP 使用 IdP 的公钥来验证SignedInfo块上的签名(对SignedInfo进行hash计算,并与公钥解密SignatureValue后的值进行对比)。如果签名有效,则确认 IdP 已签署该消息,并且该消息未被修改。
- 摘要验证:SP 计算Assertion(删除Signature节点)的值,与DigestValue进行比较,如果相同则断言没有被篡改。
验证通过后用户成功登录。
Xpath节点选择
Gitlab的认证绕过,正是因为Xpath的解析机制导致的,在Gitlab的源码中发现,它是通过 //ds:DigestValue
获取DigestValue的。
而Xpath共有选取节点方式共有三种:
/ds:DigestValue
:从根节点开始选择节点,直接指向第一个ds:DigestValue 元素。./ds:DigestValue
:从当前节点开始查找 ds:DigestValue 元素//ds:DigestValue
:从文档中的任何位置选择节点,包括所有嵌套节点中查找。、
如下:
1 | <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 | encoded_digest_value = REXML::XPath.first( |
会获取到全文中的第一个DigestValue,如果我们伪造一个DigestValue放到最开始位置,那根据SP两次验证原理即可构造绕过
原认证过程:
1 | 1. 签名验证:SP 使用 IdP 的公钥来验证SignedInfo块上的签名(对SignedInfo进行hash计算,并用公钥解密SignatureValue后的值进行对比)。如果签名有效,则确认 IdP 已签署该消息,并且该消息未被修改。 |
在摘要验证部分,会计算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 | <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
绕过视频:
修复
在后续版本中,Ruby SAML将//
改为了./
便无法再通过构造DigestValue
值达到绕过的目的。
Ruby-SAML Bypass && GitLab SAML Bypass(CVE-2024-45409)
https://blog.projectdiscovery.io/ruby-saml-gitlab-auth-bypass/