SSO单点登录安全:CAS与SAML攻防
网络安全系列 · 认证与授权篇
单点登录(Single Sign-On,SSO)是现代企业系统的标配。用户只需一次认证,就能访问多个应用——体验确实好,但安全边界也因此被拉长了。一旦 SSO 体系被攻破,攻击者拿到的不是某个应用的权限,而是整片系统的钥匙。
本文聚焦两种主流 SSO 协议——CAS 和 SAML,从协议原理讲到真实攻击手法,帮你建立完整的防御视角。
一、SSO 的核心逻辑
SSO 的本质是把认证从应用剥离,交给一个独立的身份提供者(IdP)。常见角色:
- IdP(Identity Provider):负责验证用户身份,签发令牌
- SP(Service Provider):业务应用,信任 IdP 的认证结果
- 用户:一次登录,多处通行
流程简化:
用户访问 SP → 未登录 → 重定向到 IdP
用户在 IdP 认证成功 → IdP 签发凭证 → 返回 SP
SP 验证凭证 → 建立本地会话问题就出在这个"凭证"和"验证"环节上。
二、CAS 协议详解
2.1 CAS 工作流程
CAS(Central Authentication Service)是耶鲁大学的开源协议,目前最常用的是 CAS 2.0/3.0。
标准登录流程:
1. 用户访问 https://app.example.com(SP)
2. SP 发现无会话,重定向到 https://cas.example.com/login?service=https://app.example.com/callback
3. 用户在 CAS 服务器输入用户名密码
4. CAS 验证成功,生成 TGT(Ticket Granting Ticket),写入 Cookie
5. CAS 重定向回 SP,附带 ST(Service Ticket):.../callback?ticket=ST-12345
6. SP 向 CAS 发送 /serviceValidate?ticket=ST-12345&service=... 验证 ST
7. CAS 返回用户身份信息(XML 格式)
8. SP 建立本地会话关键票据:
| 票据 | 作用 | 有效期 |
|---|---|---|
| TGT | 用户与 CAS 的会话凭证 | 通常 2-8 小时 |
| ST | 一次性票据,用于 SP 验证 | 通常 10 秒-5 分钟 |
| PGT | 代理票据,用于服务间代理 | 与 TGT 相同 |
| PT | 代理票据的一次性使用版本 | 与 ST 相同 |
2.2 CAS 的常见攻击
攻击一:Service 参数伪造(未校验回调地址)
漏洞原理: CAS 服务端未严格校验 service 参数,攻击者可构造恶意回调地址。
攻击步骤:
1. 攻击者构造链接:https://cas.example.com/login?service=https://attacker.com/callback
2. 诱导用户点击,用户在 CAS 正常登录
3. CAS 重定向到 https://attacker.com/callback?ticket=ST-12345
4. 攻击者拿到有效 ST,立即向 CAS 验证
5. 攻击者获得合法用户身份防御: CAS 服务端必须维护服务注册表,只允许白名单内的 service 地址。
// 错误示例:只检查前缀
if (service.startsWith("https://app.example.com")) {
// 可被 https://app.example.com.attacker.com 绕过
}
// 正确做法:精确匹配或维护注册表
Service registeredService = serviceRegistry.findServiceBy(service);
if (registeredService == null || !registeredService.matches(service)) {
throw new UnauthorizedServiceException();
}攻击二:ST 票据劫持与重放
ST 理论上是一次性票据,但如果:
- ST 有效期过长(>30 秒)
- CAS 服务端未正确标记 ST 为已使用
- 网络传输未使用 HTTPS
攻击者可通过中间人、浏览器历史、Referer 日志等方式获取 ST。
防御:
- ST 有效期控制在 10-30 秒
- CAS 服务端收到
/serviceValidate后立即销毁 ST - 全链路强制 HTTPS
- 敏感操作要求重新认证(step-up authentication)
攻击三:XML 响应篡改(CAS 2.0 协议漏洞)
CAS 2.0 的 /serviceValidate 返回 XML:
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
<cas:authenticationSuccess>
<cas:user>admin</cas:user>
<cas:attributes>
<cas:email>[email protected]</cas:email>
</cas:attributes>
</cas:authenticationSuccess>
</cas:serviceResponse>攻击场景: 如果 SP 使用不安全的 XML 解析器,可能遭受:
- XXE 攻击:通过
<!DOCTYPE>读取服务器文件 - XML 签名绕过:篡改响应中的用户名
防御:
# Python 安全解析示例
import xml.etree.ElementTree as ET
from defusedxml import ElementTree
# 禁用外部实体
parser = ET.XMLParser(resolve_entities=False)
tree = ElementTree.parse(response, parser=parser)攻击四:TGT 窃取与会话固定
TGT 存储在 CAS 域的 Cookie 中(TGC Cookie)。如果:
- CAS 未设置
HttpOnly、Secure、SameSite属性 - 子域存在 XSS 漏洞
- 使用了不安全的传输
攻击者可窃取 TGC,直接获得用户的 CAS 会话。
防御配置:
# CAS 服务端 Cookie 配置
cas.tgc.secure=true
cas.tgc.httpOnly=true
cas.tgc.sameSite=Strict
cas.tgc.crypto.encryption.key=...
cas.tgc.crypto.signing.key=...三、SAML 协议详解
3.1 SAML 基础
SAML(Security Assertion Markup Language)是 OASIS 标准,企业级应用广泛(如 Office 365、Salesforce、AWS)。
核心概念:
- Assertion:IdP 对用户身份的声明(包含 NameID、属性、认证上下文)
- Protocol:请求/响应的交互规则(AuthnRequest、Response、LogoutRequest)
- Binding:传输方式(HTTP Redirect、HTTP POST、Artifact)
典型流程(SP-initiated):
1. 用户访问 SP,点击登录
2. SP 生成 SAML AuthnRequest,通过浏览器重定向到 IdP
3. 用户在 IdP 认证
4. IdP 生成 SAML Response(包含 Assertion),通过浏览器 POST 到 SP
5. SP 验证 Response 的签名,提取用户信息,建立会话3.2 SAML 消息结构
<saml2:Response xmlns:saml2="urn:oasis:names:tc:SAML:2.0:protocol">
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
<ds:Signature>...</ds:Signature> <!-- 签名 -->
<saml2:Subject>
<saml2:NameID>[email protected]</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData
InResponseTo="id-123"
NotOnOrAfter="2026-06-03T10:00:00Z"
Recipient="https://sp.example.com/saml/acs"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions>
<saml2:AudienceRestriction>
<saml2:Audience>https://sp.example.com</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AuthnStatement>
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
</saml2:Assertion>
</saml2:Response>3.3 SAML 的常见攻击
攻击一:XML 签名剥离(Signature Wrapping)
漏洞原理: SP 只验证 Response 外层签名,但使用内层未签名的 Assertion。
攻击构造:
<!-- 原始合法 Response -->
<Response>
<SignedInfo>...</SignedInfo>
<SignatureValue>...</SignatureValue>
<Assertion ID="original"> <!-- 有签名 -->
<Subject><NameID>admin</NameID></Subject>
</Assertion>
</Response>
<!-- 攻击者构造 -->
<Response>
<SignedInfo>...</SignedInfo> <!-- 原签名仍然有效 -->
<SignatureValue>...</SignatureValue>
<Assertion ID="evil"> <!-- 攻击者插入的新 Assertion,无签名 -->
<Subject><NameID>admin</NameID></Subject>
</Assertion>
<Assertion ID="original"> <!-- 原始 Assertion 被移到后面 -->
<Subject><NameID>user</NameID></Subject>
</Assertion>
</Response>如果 SP 的解析逻辑取第一个 Assertion,攻击者就能以 admin 身份登录。
防御:
- 严格验证签名覆盖的范围
- 确保使用的 Assertion 确实被签名
- 使用成熟的 SAML 库(如 OneLogin、OpenSAML),不要自行解析
攻击二:Audience 限制绕过
Assertion 中的 <Audience> 指定了合法的 SP。如果 SP 不验证 Audience:
<Conditions>
<AudienceRestriction>
<Audience>https://evil-sp.example.com</Audience>
</AudienceRestriction>
</Conditions>攻击者可以:
- 在合法 IdP 下注册一个恶意 SP
- 诱导用户登录恶意 SP,获取 SAML Response
- 将 Response 重放到目标 SP
- 如果目标 SP 不检查 Audience,攻击者就能登录
防御: SP 必须严格校验 Audience 是否匹配自己的 Entity ID。
攻击三:RelayState 操控与开放重定向
SAML 的 RelayState 参数用于保持登录前的状态,但常被滥用:
https://idp.example.com/sso?SAMLRequest=...&RelayState=https://attacker.com如果 SP 登录后直接跳转到 RelayState 指定的地址,就是开放重定向。
防御:
- RelayState 使用白名单校验
- 或使用加密的 state 参数存储跳转目标
攻击四:SAML Response 重放
如果 SP 不跟踪已处理的 SAML Response ID(InResponseTo)或 Assertion ID:
- 攻击者截获合法的 SAML Response
- 在有效期内重复提交
- 如果 SP 不做去重,就能重复建立会话
防御:
# 维护已处理的 Assertion ID 集合
processed_assertions = set() # 建议用 Redis,TTL 设为 Assertion 有效期
def validate_saml_response(response):
assertion_id = response.assertion.id
if assertion_id in processed_assertions:
raise ReplayedAssertionError()
processed_assertions.add(assertion_id)攻击五:证书伪造与算法降级
SAML 依赖 XML 签名,如果:
- IdP 使用弱签名算法(如 SHA-1)
- 证书未正确校验有效期、颁发者
- 证书轮换时 SP 未及时更新
攻击者可以构造碰撞或伪造证书。
防御:
- 强制使用 SHA-256 或更强的签名算法
- 建立证书轮换机制,提前通知 SP
- 定期检查证书有效期
四、SSO 安全加固清单
4.1 IdP 侧
| 检查项 | 要求 |
|---|---|
| Service 注册表 | 严格白名单,精确匹配 |
| 票据有效期 | ST < 30s,TGT < 8h |
| Cookie 安全 | HttpOnly + Secure + SameSite=Strict |
| 通信加密 | 全链路 TLS 1.2+ |
| 签名算法 | SHA-256 或更强 |
| 审计日志 | 记录所有认证、票据验证事件 |
| 多因素认证 | 敏感系统强制 MFA |
4.2 SP 侧
| 检查项 | 要求 |
|---|---|
| 签名验证 | 验证所有签名,不跳过 |
| Audience 校验 | 严格匹配自己的 Entity ID |
| 时间校验 | 检查 NotBefore、NotOnOrAfter |
| 重放防护 | 维护已处理的 Assertion ID |
| RelayState | 白名单或加密校验 |
| 会话管理 | 独立会话,不依赖 SSO 票据 |
| 登出同步 | 支持 SLO(Single Logout) |
4.3 通用最佳实践
- 最小权限原则:SSO 只认证身份,授权(RBAC)在应用内独立管理
- step-up 认证:敏感操作要求重新输入密码或二次验证
- 监控异常:同一用户短时间内多地登录、异常时间登录
- 定期轮换:密钥、证书定期更换
- 渗透测试:定期对 SSO 流程做端到端安全测试
五、实战:检测 SAML 签名 wrapping 漏洞
使用 Burp Suite + SAML Raider 插件:
- 拦截 SAML Response
- 发送到 SAML Raider
- 尝试 "Sign SAML Message" / "Remove Signatures"
- 修改 Assertion 内的 NameID
- 观察 SP 是否接受篡改后的 Assertion
手动测试 Payload:
<saml2:Response>
<saml2:Assertion ID="evil">
<saml2:Subject>
<saml2:NameID>[email protected]</saml2:NameID>
</saml2:Subject>
</saml2:Assertion>
<!-- 原始 Assertion 移到后面,保留签名 -->
</saml2:Response>六、总结
SSO 是"把鸡蛋放在一个篮子里"的典型——方便,但篮子必须足够结实。
| 协议 | 主要风险 | 核心防御 |
|---|---|---|
| CAS | Service 伪造、ST 劫持、TGC 窃取 | 服务注册表、短票据、安全 Cookie |
| SAML | 签名 wrapping、Audience 绕过、重放 | 严格签名验证、Audience 校验、ID 去重 |
无论使用哪种协议,记住:信任但验证。IdP 的声明不是圣旨,SP 必须独立验证每一个安全断言。
下一篇预告:《Session 管理安全:从 Cookie 到 Token》——深入会话生命周期,解析 Session Fixation、Cookie 安全属性,以及现代 Token 方案的攻防。