☰
生产级SSO安全测试完整复盘:从SAML到JWT的攻击面
2026/10/1 9:11:53 网站建设 项目流程

从零开始拿下一个生产级SSO:我的安全测试完整复盘

做安全的这些年,我吃过最大的亏,就是轻视SSO。看着只是一个登录页,背后却串联着身份提供商(IdP)、服务提供商(SP)、令牌签发、会话同步、跨域回跳这一整套链路。任何一个环节校验不严,攻击者拿到的就不只是一个账号,而是整个系统的入口权限。这篇文章不聊教科书概念,把我这些年对单点登录系统做安全测试的完整思路、实操步骤和踩坑记录全部端出来,包括协议层攻击面、工具使用细节、常见误报判定方法,希望对正在做渗透测试或安全建设的同行有帮助。

适合看的兄弟:一是做Web渗透测试、想深入身份认证领域的安全从业者;二是企业里负责SSO平台运维、想自查风险的研发同学;三是刚学安全、准备面试时被问SSO测试要点的新人。下面内容偏实战,我会把原理、过程和结论放在一起讲,不搞空对空。

1. 测试前必须想清楚的整体设计思路

SSO的安全测试跟普通Web站点的测试有个很大的区别:普通站点你只需要关心“这一个系统”的漏洞,而SSO涉及三方角色——用户浏览器、SP(业务系统)、IdP(统一认证中心),攻击路径天然就多了一条。我在测试前,一般会先画清楚数据流,再做威胁建模,最后才开搞。

1.1 先理清SSO的三种主流技术选型

不同SSO实现,攻击面侧重点完全不同。常见的三种是SAML、OAuth 2.0/OIDC、以及企业内部自研的Cookie+Session共享方案,测试时不能一概而论。

协议/方案典型应用场景核心安全关注点
SAML 2.0企业办公系统、云应用接入XML签名校验是否可绕过、断言是否可篡改
OAuth 2.0 / OIDC移动端、Web第三方登录(微信、GitHub等)授权码流程、redirect_uri校验、state参数防CSRF
自研Cookie共享方案老系统的内部单点登录会话票据是否可预测、Cookie属性是否安全、跨域信任边界

这一点必须放在前面考虑:测试方向选错了,后面全是白干。比如一个纯SAML场景,你花大量时间测OAuth的授权码截获,意义基本为零。接手一个目标,我第一件事就是抓包看协议类型:看看token是XML格式还是JWT,看看跳转用的是HTTP Redirect还是POST表单,然后再决定测试case怎么写。

1.2 用白盒心态画攻击面,但用黑盒手段验证

我这几年做下来最深的一个体会是:SSO测试最缺的不是工具,是攻击面建模能力。

每次开始测试,我都会用文本把整个SSO链路走一遍,列出所有可被操纵的点,大概包括这些方向:

  • 认证请求的完整性:SP生成的AuthnRequest是否被签名、签名是否被校验
  • 断言的信任边界:IdP返回的Assertion是否经过篡改、XML签名是否真正验证到关键节点
  • 回跳参数的可靠性:RelayState、state、redirect_uri等参数是否被校验、是否存在任意跳转
  • 令牌的生命周期:JWT的签名算法、过期时间、issued-at时间是否可以操纵
  • 会话的同步机制:SP与IdP之间的session映射关系是否存在“session fixation”
  • 登出机制:单点登出(SLO)是否可被滥用、是否导致会话不终止

为什么要求测试依赖外部工具而不是只靠浏览器点?因为SSO链路中,中间任何一次307跳转、表单POST自动提交、跨域Cookie写入,都会影响最终结果。浏览器的开发工具只能看个大概,完整的中间人代理才是我们的眼睛。

1.3 授权和范围确认——这是安全从业者的第一条红线

再强调一遍,不管是做外包渗透测试还是内部安全自查,测试前先确认授权范围。SSO系统一旦被破坏,影响的是所有下游业务系统,弹跳效应非常大。我一般会在授权中明确写明:

  • 允许测试的SP/IdP域名列表
  • 允许使用的攻击方式边界(比如禁止DDoS、禁止对IdP基础服务做拒绝服务测试)
  • 可测试的账号类型(普通用户、管理员用户,是否允许创建测试应用)
  • 测试时间窗口(避免业务高峰)

这一步看起来是形式,但真的出事时,它是保护自己的唯一凭证。

2. 那三条路:SAML、OAuth2/OIDC、JWT的核心攻击面

协议原理不展开讲全,只讲安全测试中最常出问题的位置。你如果连流程都没跑通,建议先跟着一个正常登录流程抓一遍所有包,再往下读。

2.1 SAML测试:要找的不是漏洞,是“信任”

SAML的流程一般是这样:用户访问SP资源 -> SP生成AuthnRequest -> 302重定向到IdP -> 用户登录 -> IdP返回SAMLResponse -> SP的ACS节点接收并处理 -> 建立本地会话。

整个SAML体系的核心逻辑是“信任”,SP相信IdP签名的XML断言。所以安全测试的核心就是验证信任链条是否可以被伪造。我最常测的三个点:

  1. XML签名校验绕过。这个是最经典的洞。攻击者可以自己生成一个SAMLResponse,用自己生成的私钥签名,然后通过某种方式让SP“认为”签名是合法的。历史上出现过不少SP只校验了第一个签名节点,却没有校验整个DocumentElement的情况。测试的时候,我会把一个正常的SAMLResponse保存下来,改掉Assertion里的用户ID字段,用自己的密钥重新签名,然后直接POST给ACS节点,看是否登录成功。

  2. 签名缺失时是否仍被接受。有的SP实现偷懒,把签名验证写成一个“可选”逻辑。我经常直接把SAMLResponse中的Signature节点整体删掉,再重放一遍。如果SP接受了,那恭喜,这等于开着一扇没有锁的门。

  3. Assertion注入/替换。如果在同一Request中同时传入多个Assertion,或者SAMLResponse中带了加密断言的同时还有个未加密断言,SP取了哪个?测试方法也不难:Burp里拦截返回的SAMLResponse,把<saml:Assertion>复制一份加进去,看SP的处理逻辑。

SAML测试有个非常恶心的前提,就是你需要能读懂结构。每一条payload我都是先在本地解一遍Base64 + decompress(SAMLResponse通常用deflate压缩),确认修改点没问题,再放回请求里去发。

提示:SAMLResponse是Base64编码,且很多实现会先deflate再编码。不要直接在原始请求里改字符串,大概率只会得到一个解压失败的错误。正确做法是先用Burp的Decoder或本地工具还原原文,修改后再压缩编码回去。

2.2 OAuth2/OIDC测试:redirect_uri和state决定生死

OAuth2/OIDC的流程比SAML更贴近现代Web生态:用户点击第三方登录 -> 302到授权服务器 -> 登录并授权 -> 授权码code回调到redirect_uri -> 后端用code换token -> 访问用户信息。这里漏洞高发地非常集中。

第一个必测点是redirect_uri的开放性。很多系统只做了“前缀匹配”或“包含匹配”,没有做精确匹配。测试时我会把redirect_uri改成这些形态:

原:https://app.example.com/callback 改:https://app.example.com/callback.evil.com 改:https://app.example.com/callback@evil.com 改:https://app.example.com/callback/../../evil 改:https://evil.com/?ref=https://app.example.com/callback

如果授权服务器接受了这些变体并回传code,那攻击者就能通过诱导用户访问一个恶意链接,把授权码转发到自己控制的域名上。这个是OAuth2圈子里最经典的全账户劫持路径。

第二个必测点是state参数的防CSRF功能。state参数的作用是绑定发起请求的浏览器会话,防止攻击者诱导已登录用户点击第三方登录链接。很多系统根本不校验state,或校验逻辑只在请求方而不在回调方。我在测试时会把第一次请求的state值替换成随意值,再手动完成整个授权流程,回到回调地址后看是否报错。如果正常放行,说明存在Login CSRF/会话固定风险。

第三个关注点是授权码的“一次性”验证。授权码code默认应该只能用一次,而且必须与client_id、redirect_uri绑定。测试时我会在正常拿到code后,连续两次请求token端点,第二次如果还是返回token,说明code重放防御失效,这在我测过的系统里出现过不止一次。

OIDC相对OAuth2多了一个ID Token(JWT格式),它的安全测试就跟JWT挂钩了。ID Token里最常出问题的字段是nonce和at_hash——一个是防止重放,一个是防止token替换。很多自研实现根本没校验这两个值。

2.3 JWT测试:签名算法混淆是一个老生常谈但有奇效的洞

JWT在SSO体系里几乎无处不在,SP和IdP之间换token用JWT,Web会话用JWT,甚至有些系统把整个用户角色权限都写在JWT里。它的攻击面堪称为“筛子专业户”。

算法混淆攻击(alg混淆)是最经典的一个:如果你把header里的alg改成none并删掉签名,系统是否还认?如果服务端同时支持RSA和HMAC,你能否把公钥作为HS256的对称密钥来伪造签名?我最近一次测试里,把原本RS256签名的JWT改成alg:"HS256",然后拿公钥原文去签名,结果直接伪造出了一个admin token,这属于典型实现缺陷。

Claim篡改测试:把exp改大、把iat改成未来时间、把sub换成目标用户ID,很多服务端根本没有正确校验。测试的时候我不会一上来就改一堆字段,而是每次只改一个,分多次请求,确保能知道是哪一步校验缺失。

CVE级别的坑:如果目标使用了较老版本的JWT库,我还会去搜一遍该库的历史CVE,例如密钥泄露、填充攻击相关的已知问题。这不丢人,成熟攻击者都是这么干的——能靠已知漏洞的打法,绝不硬猜。

下表是我在SSO测试中会标准检查的一组JWT内容:

测试项操作方式预期安全表现
alg:none去掉签名部分,alg改为none应返回401/403
算法混淆用公钥做HS256密钥签名应拒绝
过期时间将exp改为10年后应过期校验不通过
用户标识把sub/admin字段改为admin应校验签名/信任源
敏感信息解码JWT看payload不应含密码、密钥等明文数据

3. 四个必须死磕的关键安全测试重点

协议和令牌层面的问题说完了,再往下纵深一步。这部分讲的是SSO真正高危的四个测试方向,往往普通渗透测试报告里不会覆盖到。

3.1 会话标识:别让“单点登录”变成“单点沦陷”

SSO登录成功之后,SP和IdP之间建立会话映射。这个映射最终表现成一个session ID或者一个service ticket,问题出在这种凭证的随机性和绑定关系上。

我最常测的:

  • 会话ID是否可预测:抓若干次登录后的session ID,分析规律,尤其是自研系统,经常出现时间戳+自增ID拼出来的东西。
  • 会话是否与IP绑定:IdP侧发出来的会话,SP侧做了绑定没有?用两个不同出口IP跑同一个会话试探,如果都能通过,那意味着拿到一个session就能到处用。
  • 会话固定(Session Fixation):在用户未登录前先获得一个合法的session ID,然后诱导用户用这个session去完成登录。登录后服务端如果不重新颁发会话ID,攻击者就拿到了用户登录态的钥匙。
  • 单点登出(SLO)的完整性:在一个SP执行登出,其他SP的会话有没有同步失效?这个问题很多企业压根没做。我测试时会特意设计一个流程:先在A系统登录,再到B系统登录,然后在A系统登出,回到B系统看会话是否失效。B系统还可用的,算一个中危问题。

3.2 跨域与CORS配置:对信任边界的“踩线”测试

SSO系统天生就是跨域的:认证域、业务域、回调域各不相同,这给了CORS配置错误的天然舞台。很多开发写Access-Control-Allow-Origin时喜欢用“反射”或者“通配符”,直接把信任范围炸开了。

测试点包括:

  • 是否能通过任意Origin直接跨域读取接口:用Burp发请求,Origin设置为https://evil.com,看响应是否带Access-Control-Allow-Origin: https://evil.com,同时是否返回了Access-Control-Allow-Credentials: true。
  • 是否能利用“子域名前缀绕过”:有些系统会把developer.example.com.evil.com当作合法域名反射出来,看起来是前缀校验其实是后缀匹配。我见过太多这种案例。
  • 预检请求的处理细节:OPTIONS请求返回的Allow-Headers、Allow-Methods是否过大。

另外事务上还有一个容易忽略的点:许多IdP允许通过URL参数指定POST回跳地址,如果开发者没有在服务端配置白名单,而是直接用前端的参数取值,就存在“任意附件回跳+敏感token携带”的组合问题。

3.3 令牌生命周期与密钥管理:看配置不看代码也能发现高危

SSO系统的安全性很大程度取决于密钥的安全性。但很多团队把重心放在签名算法上,忽视了密钥的存储与轮换。测试时可以从这些层面入手:

  1. JWT签名公钥是否可被获取:如果/.well-known/jwks.json直接暴露且无任何速率限制,攻击者虽然拿不到私钥,但可以做公钥替换测试(在某些库中存在)。
  2. IdP的私钥是否落到了客户端缓存:抓包看是否存在私钥或加密材料被静态资源引用。
  3. 令牌有效期过长:我见过access token有效期设为一周的。时间越长,泄露后攻击窗口越大。
  4. refresh token是否可重复使用:OAuth2的refresh token理论上是一次性的,如果是永久有效且无设备绑定,等于拿到了永久门票。

测试时总结起来,我建议按“部位-风险-证据”的方式做表格记录,每个高风险项要单独写清触发条件和影响路径,不然报告发出去开发都看不懂要改什么。

3.4 敏感信息泄露:从登录接口能捞到什么?

SSO登录接口是攻击者最先碰到的点,这里的信息泄露往往不被人注意,但实际影响不小。我一般会重点看:

  • 错误提示是否区分“用户不存在”和“密码错误”:虽然这属于用户枚举,但通过SSO接口枚举企业员工账号,对后续钓鱼和撞库非常有帮助。
  • 是否返回了token或断言明文:在调试模式下,SP返回的报文里是否包含SAMLResponse明文、JWT payload等。
  • 是否泄露后端框架报错:比如Spring报错堆栈、数据库唯一约束错误。
  • 请求日志审计:这个其实是测试侧的信息:如果你作为攻击者能访问到SP的某些调试接口或静态文件,里面可能记录着认证日志。

有道是:安全测试的价值不是“发现System.out.println的堆栈”,而是证明攻击者能利用它往下走到哪一步。我在报告中每次都会把这层利用链写清楚。

4. 实操:在Kali环境下跑一次完整的SSO安全测试

接下来进实战。这里以攻击者视角走一遍整个流程,环境基于我的常用配置:Kali Linux + Burp Suite + Python脚本辅助。不装什么高深工具,关键在于怎么用。

4.1 测试准备与代理配置

常规流程:在Kali里把Burp监听在127.0.0.1:8080,浏览器配置代理指到Burp,同时安装Burp的CA证书到系统信任库。有人会问为什么不用浏览器直接抓,因为我们需要修改请求后重放,还需要处理HTTPS证书异常,Burp这一切都能搞定。

证书安装的坑说一下:Kali上Firefox如果不额外导入Burp CA,访问HTTPS站点会不断报证书错误。虽然测试时可以直接忽略,但部分JS/CSS加载会失败,影响页面行为。正确做法是把Burp CA证书导出为.der格式,在Firefox证书管理里导入,并勾选“信任此CA标识网站”。

4.2 登录流程抓包 + 关键端点映射

打开Burp,确认HTTP History里目标域名的流量随身可见。正常跑一遍SSO登录流程:

  1. 访问SP受保护资源,记录SP生成的AuthnRequest参数
  2. 跟随302跳转到IdP,记录IdP登录页面对应的POST参数
  3. 提交用户名密码登录,记录IdP返回的响应(这中间会有一个自动POST到SP的步骤)
  4. 最终回到SP并建立会话

在这个过程里,Burp History中会出现几个关键端点:/saml/authnrequest、/login、/saml/acs、/slo。把这些保存为Burp的Site Map中的“关键项”,后面所有攻击都围绕这些端点展开。

4.3 三个必打的攻击用例实操

用例一:修改SAMLResponse中的用户名,伪造身份

正常登录一遍拿到SAMLResponse,转成可编辑格式。用Burp自带的Decoder把Base64解出来,再对压缩的内容解压。我用的是流程:

# 命令行快速解析SAMLResponse echo -n "BASE64_STRING" | base64 -d > saml.xml python3 -c " import zlib,sys data=open('saml.xml','rb').read() try: print(zlib.decompress(data,-15).decode('utf-8')) except: print(data.decode('utf-8',errors='ignore')) "

拿到原文后把<saml:NameID>改成目标用户名(比如admin),再用自己的私钥对整段XML签名,重新压缩+Base64编码,发到ACS节点。如果SP返回“欢迎admin”,说明签名校验形同虚设。

注意:这部分操作不要对线上生产系统的真实账号做,用两个测试账号互相切换即可,别把自己锁在外面。

用例二:OAuth2 redirect_uri绕过

如果目标是OAuth2场景,我在Burp里用Repeater改授权URL:

原始:https://idp.example.com/oauth/authorize?client_id=test&redirect_uri=https://app.example.com/callback&response_type=code 攻击:https://idp.example.com/oauth/authorize?client_id=test&redirect_uri=https://app.example.com/callback.evil.com&response_type=code

如果授权服务器没有做精确匹配,它会带着code跳到callback.evil.com。我把evil.com配置成自己服务器并监听443,就能收到code。拿到code后,再去调用token端点,直接获取token,完成整个账号劫持链路。

用例三:JWT算法混淆

把正常签发得到的JWT拆成三部分:header、payload、signature。在header里把alg改成none,删掉signature,重放给SP。如果SP不校验签名,直接返回正常业务数据,这就是一个可以任意伪造身份的点。反之如果401,说明服务端对这个攻击有防御。再用HS256把公钥做密钥签名重放一遍。

我平时用一段小脚本快速做这个事:

import jwt, time public_key = open('public.pem').read() payload = {"user": "admin", "exp": int(time.time()) + 3600} token = jwt.encode(payload, public_key, algorithm='HS256', headers={"kid": "public"}) print(token)

这里的kid也要注意,很多系统会去读kid指定的文件内容做公钥查找,历史上出过利用kid做任意文件读取的漏洞。我也会顺手测一下kid传一个不存在的值,看服务端会不会抛异常。

4.4 辅助测试工具与脚本沉淀

除了Burp,我日常SSO测试还配合这些工具:

工具用途使用心得
SAML Raider(Burp扩展)SAML消息签名、篡改、重放能自动识别签名节点,大幅提升效率,但偶尔对新版Burp兼容性不佳
jwt_toolJWT头部/载荷/签名攻击支持字典爆破弱密钥,带alg混淆模块,实测比手写脚本稳定
xsstrike(备用)如果SSO页面存在反射型XSS只作为补充,SSO核心测试不需要它
sqlmapIdP登录表单是否存在SQL注入需要小心授权边界,别对生产库做大量注入测试

工具一堆,其实真正关键还是你对协议的把握。我见过有人只拿工具跑了一圈,一个漏洞都没发现,但我手动改一个SAML断言就进后台了。这行当,“复现”永远比“扫描”值钱。

5. 常见问题与排查技巧实录

SSO测试过程里,我踩过很多坑,有些坑可能一卡就是半天。整理几个高频问题,给后来者排雷。

5.1 解不开SAMLResponse:不是Base64,是压缩

一直有同事跑来问我,为什么SAMLResponse解出来是乱码。这十有八九是没做过解压。SAML的Response签名部分经常是deflate压缩后再Base64的。你直接base64 -d看到的就是压缩后的二进制。处理方法上面写过了,zlib.decompress(data, -15),关键是第二个参数要写对,否则解不出来。

5.2 测试请求被WAF拦截:学会改指纹

SSO系统往往套了WAF。BurP的默认指纹太明显,User-Agent、TLS指纹都容易被拦。我一般会:

  • 修改Repeater和Intruder的User-Agent为正常浏览器UA
  • 如果使用了Burp自带TLS指纹,碰到无法连接的情况就换用系统原生支持
  • 低频率测试,不要一把梭直接跑Intruder字典,否则IP封了事小,惊动目标蓝队事大

5.3 区分“误报”与“真实漏洞”的标准

SSO测试中误报率极高,尤其是CORS和redirect_uri这类。我判断一个漏洞是否成立,会强制问自己三个问题:

  1. 影响链是否完整?即攻击者能触发吗?是直接访问还是需要管理员配合?
  2. 权限影响是否真实存在?比如JWTalg:none,虽然服务器返回了200,但如果返回的数据和未授权访问一样,只是一个通用提示,那就不是漏洞。
  3. 是否有锦上添花的Bypass?比如SAML伪造成功了,但我只是伪造了一个无权限的测试账号,也算真实风险,但如果连现有业务接口都无法访问,那只是一个“局部凭证伪造”。

报告里我一般把“可利用性”与“影响”分开写,不给开发留模糊空间。

5.4 与开发团队沟通不顺畅?写“修复建议”时直接给代码级方案

做测试报告,我最烦的就是写“建议加强对SAML签名校验”,这种话等于没说。通常我会给到代码补丁级别的建议,比如:

// 修复SAML签名校验,拒绝unsigned assertion if (assertion.getSignature() == null) { throw new SAMLException("Missing signature on assertion"); }

或者给OAuth2的redirect_uri正确校验逻辑示例:

parsed = urlparse(redirect_uri) if parsed.scheme not in ("https",): raise ValueError("非HTTPS的redirect_uri") if parsed.netloc not in allowed_redirect_hosts: raise ValueError("redirect_uri非法")

这种写法,开发可以直接抄,这才是一份好的渗透测试报告该有的样子。只报漏洞不报修法,是拿不到下次合作的。

写在最后

SSO安全测试是一个越做越觉得深的领域。从SAML到OAuth2,再到JWT,每次感觉摸清了一个协议,下一个版本就冒出新的绕过姿势。我个人这几年的体会是:不要迷信工具,把协议链路走通、把攻击面自己先在纸上画一遍,弄透每一个参数从哪来到哪去,比装一百个扫描器都有用。

如果你刚接触这一块,建议先在自己本地搭一套简单的SAML或OIDC IdP和SP环境,用测试账号反复抓包改包,等踩够坑了再上真实项目。最后再提醒一句:任何安全测试都要在授权范围内进行,别拿别人的系统练手,这是底线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询