JWT安全攻防实战:从签名算法绕过到密钥混淆的5大漏洞解析

📅 2026/7/5 9:25:01
JWT安全攻防实战:从签名算法绕过到密钥混淆的5大漏洞解析
1. 项目概述从靶场到实战的JWT安全攻防最近在复盘一些渗透测试项目时发现JWTJSON Web Token相关的安全问题出现频率越来越高。很多开发团队在引入JWT作为认证方案后往往只关注其带来的便利性却忽视了其自身的安全配置和潜在风险。这让我想起之前在CTFHub靶场里专门练习JWT漏洞的场景那些精心设计的挑战恰恰是现实世界中漏洞的缩影。从靶场的“解题”思维切换到真实渗透测试的“发现风险”思维中间需要跨越的不仅是技术点更是对漏洞原理、利用场景和修复方案的深度理解。JWT本质上是一种开放标准用于在各方之间安全地传输信息作为JSON对象。由于其紧凑、自包含的特性它特别适合用于分布式站点的单点登录、API认证等场景。一个典型的JWT由三部分组成头部、载荷和签名中间用点分隔形如xxxxx.yyyyy.zzzzz。问题往往就出在开发者对这三部分的生成、验证和处理的逻辑理解不透彻上。在CTF中我们可能只需要找到一种特定的攻击手法拿到flag但在真实渗透中我们需要系统性地审视整个JWT的生命周期找出其中任何一个环节的薄弱点。这篇文章我将结合在CTFHub靶场中的实战经验以及真实渗透测试项目中遇到的案例系统梳理JWT安全漏洞的5种核心利用手法。更重要的是我会详细拆解每一种手法背后的原理、在真实环境中的利用条件、具体的操作步骤并给出具有可操作性的修复建议。无论你是安全研究员、渗透测试工程师还是负责开发维护的工程师理解这些内容都能帮助你更好地构建和防御基于JWT的认证体系。2. JWT安全漏洞的5种核心利用手法深度解析JWT的安全问题大多源于实现上的疏漏而非协议本身的设计缺陷。攻击者正是利用了这些疏漏伪造、篡改或滥用Token从而绕过认证、提升权限或窃取信息。下面我们逐一拆解这五种常见手法。2.1 手法一签名算法置空与“None”攻击这是JWT安全中最经典也最容易被忽视的一种攻击方式。其核心原理在于JWT规范允许使用“none”算法即不进行签名验证。这种设计本意是用于调试或某些不需要完整性的场景但如果在生产环境中服务端错误地接受了“none”算法攻击者就可以伪造任意Token。漏洞成因深度分析JWT的头部Header中有一个alg字段用于声明签名所使用的算法如HS256、RS256等。当服务端接收到一个JWT时它需要根据这个alg字段的值来选择对应的验证逻辑。漏洞产生的关键在于服务端的验证代码存在逻辑缺陷。一种常见的有缺陷的验证逻辑伪代码如下def verify_token(token): header decode_header(token) # 解码头部 alg header.get(alg) if alg HS256: # 使用密钥验证HMAC签名 return verify_hmac(token, secret_key) elif alg RS256: # 使用公钥验证RSA签名 return verify_rsa(token, public_key) # 缺少对‘none’或其他未知算法的拒绝逻辑 # 或者错误地认为‘none’算法等同于验证通过 else: # 错误示例直接返回True return True从代码中可以看出如果服务端没有显式拒绝alg为none的Token或者错误地将none等同于验证成功那么攻击者就可以构造一个头部为{alg:none,typ:JWT}载荷为任意内容例如将用户名改为admin签名部分为空的Token。服务端在处理时发现算法是none可能就会跳过签名验证步骤直接信任载荷中的信息。真实环境利用场景与实操在真实的黑盒渗透测试中我们如何发现和利用这类漏洞首先你需要获得一个有效的JWT通常通过登录获取。然后使用Burp Suite的Decoder或类似工具对其进行Base64解码观察其头部。接着尝试篡改。捕获Token使用代理工具拦截含有JWT的请求通常在Authorization: Bearer头或Cookie中。修改头部将头部的alg字段改为none。注意JWT各部分都是Base64Url编码的修改后需要重新编码。同时必须将签名部分置空。发送请求将篡改后的Token替换原Token重放请求。观察响应如果请求成功例如返回了管理员数据或访问了未授权接口则说明存在漏洞。注意现代成熟的JWT库如java-jwt、pyjwt默认会拒绝algnone的Token。因此这个漏洞多见于开发者自行实现JWT解析验证逻辑或错误配置了第三方库的情况。在测试时也可以尝试将alg改为None、NONE、nOnE等大小写变种以绕过一些简单的字符串过滤。修复建议在服务端验证JWT时必须显式指定允许的算法列表并且拒绝任何不在列表中的算法尤其是none。以Python的PyJWT库为例正确的做法是import jwt # 错误做法不指定算法库可能会使用头部声明的算法易受攻击 # decoded jwt.decode(token, your-secret-key) # 正确做法使用algorithms参数明确指定可接受的算法 decoded jwt.decode( token, your-secret-key, algorithms[HS256] # 明确只接受HS256算法 )这样即使攻击者传入alg: none的Tokenjwt.decode函数也会因为none不在algorithms列表中而直接抛出InvalidAlgorithmError异常。2.2 手法二密钥混淆攻击HS256与RS256这种攻击手法比“none”攻击更隐蔽需要对非对称加密和对称加密有基本的理解。JWT常用的签名算法有HS256HMAC with SHA-256对称加密和RS256RSA Signature with SHA-256非对称加密。漏洞成因深度分析HS256使用同一个密钥进行签名和验证。密钥必须保密。RS256使用私钥签名公钥验证。私钥保密公钥可以公开。漏洞产生于服务端代码错误地允许算法被篡改。假设一个应用本应使用RS256其验证逻辑是从Token头部读取alg如果是RS256就用预置的公钥去验证签名。攻击者可以尝试以下步骤将头部alg从RS256改为HS256。服务端代码有缺陷它看到alg: HS256后错误地使用了原本用于验证RS256的公钥作为HMAC的对称密钥来验证签名。攻击者只需要知道这个公钥通常是公开的就可以用HS256算法和这个公钥作为密钥伪造一个有效的签名。关键在于服务端在验证时没有将算法与密钥进行绑定。用于RS256验证的公钥被错误地用于HS256的验证逻辑。真实环境利用场景与实操利用此漏洞的前提是攻击者能够获取到应用用于验证RS256签名的公钥。公钥有时会存放在/jwks.json、/.well-known/jwks.json等端点或者直接硬编码在前端JavaScript代码中。信息收集寻找应用的公钥。检查前端JS、已知的JWKS端点、甚至应用文档。获取原始Token正常登录获取一个有效的RS256签名的JWT。篡改与伪造修改载荷如将user: victim改为user: admin。将头部alg改为HS256。使用获取到的公钥作为HMAC的密钥对新的头部和载荷进行HS256签名。生成新的Token。重放测试用伪造的Token替换原Token发送请求。修复建议服务端在验证JWT时绝对不能依赖客户端可控制的头部alg字段来决定验证逻辑和所使用的密钥。正确做法是应用在配置验证器时就固定好算法和对应的密钥。如果使用HS256则配置一个保密的对称密钥并固定使用HS256算法验证。如果使用RS256则配置公钥并固定使用RS256算法验证。 同样以PyJWT为例修复方式就是始终在decode时明确指定算法# 对于HS256服务 decoded jwt.decode(token, strong-secret-key, algorithms[HS256]) # 对于RS256服务 decoded jwt.decode(token, public_key, algorithms[RS256])这样无论攻击者如何修改Token头部的alg服务端都只用自己的预设算法和密钥去验证从根本上杜绝了密钥混淆的可能。2.3 手法三弱密钥爆破与密钥泄露这种手法更偏向于传统的密码学攻击针对的是使用对称算法如HS256的JWT。如果签名密钥强度不够或者密钥意外泄露那么整个认证体系就会崩塌。漏洞成因深度分析HS256的安全性完全依赖于密钥的强度。密钥泄露的途径可能包括弱密钥开发者使用简单、常见的字符串作为密钥如secret、password、123456、应用名等。这些密钥可以被字典攻击轻松破解。密钥硬编码将密钥写在客户端代码、配置文件或代码仓库中导致密钥可能被反编译、源码泄露或通过目录遍历获取。密钥生成算法有缺陷使用不安全的随机数生成器生成密钥导致密钥可预测。真实环境利用场景与实操在渗透测试中面对一个HS256签名的JWT密钥爆破是常规手段。识别算法解码JWT头部确认alg为HS256。准备字典收集常见的JWT弱密钥字典。网络上有很多开源字典包含成千上万个常见密钥。也可以根据目标应用名称、域名、公司名等生成定制字典。使用工具爆破推荐使用hashcat或jwt-tool。使用jwt-tooljwt-tool是专为JWT安全测试设计的工具非常方便。python3 jwt_tool.py JWT_TOKEN -C -d /path/to/wordlist.txt使用hashcathashcat性能强大模式16500对应JWT。# 将JWT格式转换为hashcat接受的格式token::signature echo -n JWT_TOKEN jwt.hash hashcat -m 16500 jwt.hash /path/to/wordlist.txt利用泄露密钥一旦爆破出密钥你就可以用这个密钥为任意载荷生成合法的签名完全控制Token内容。实操心得爆破的成功率取决于字典的质量。除了通用弱密钥字典一定要尝试与目标相关的词汇组合。有时查看网页源代码、JavaScript文件甚至错误信息可能会发现密钥的线索。在CTF靶场中弱密钥往往设置得很明显但在真实场景密钥可能有一定复杂度需要结合其他信息泄露漏洞如Git源码泄露、配置文件泄露来获取。修复建议使用强密钥对于HS256密钥必须是足够长如32字节以上、随机的密码学安全字符串。可以使用安全的随机数生成器来生成。密钥管理绝对不要将密钥硬编码在客户端或公开的代码仓库中。应该使用安全的密钥管理系统如云服务商的密钥管理服务或在部署时通过环境变量注入。定期轮换密钥制定密钥轮换策略。但要注意轮换会导致所有已签发的Token立即失效需要配合合理的Token过期时间和刷新机制。考虑升级算法对于安全性要求极高的场景应考虑使用RS256等非对称算法。即使公钥泄露没有私钥也无法伪造签名从根本上解决了对称密钥泄露的风险。2.4 手法四KID参数路径遍历与注入kidKey ID是JWT头部的一个可选参数用于指示在验证签名时应该使用哪个密钥。这在服务端维护了多个密钥例如用于密钥轮换或不同租户时非常有用。然而如果服务端处理kid参数的方式不安全就会引入严重漏洞。漏洞成因深度分析服务端通常会有一个密钥库JWKS根据kid来查找对应的密钥。漏洞出现在以下情况路径遍历如果kid参数被直接用于拼接文件路径例如/keys/kid攻击者可以注入路径遍历序列如../../../../etc/passwd让服务端读取非预期的文件作为密钥。如果这个文件内容是已知的如/dev/null是空/proc/self/environ包含环境变量攻击者就可能用这个“密钥”来伪造签名。SQL注入如果kid被直接用于数据库查询可能引发SQL注入进而导致密钥泄露或其他数据破坏。重定向攻击如果kid是一个URL服务端可能从这个URL获取密钥。攻击者可以控制这个URL指向自己服务器上的一个密钥文件从而完全控制签名验证。真实环境利用场景与实操假设我们发现一个JWT的头部包含kid: key-123。测试路径遍历尝试修改kid为../../../../etc/passwd。然后我们需要用/etc/passwd文件的内容作为HMAC密钥来为篡改后的载荷生成签名。由于/etc/passwd是公开可读的我们知道它的内容。我们可以用这个内容作为密钥使用HS256算法生成签名构造一个“合法”的Token。工具辅助jwt-tool可以自动化这个过程。python3 jwt_tool.py JWT_TOKEN -I -hc kid -hv ../../../../etc/passwd -S hs256 -k /etc/passwd这个命令会注入kid并用指定的文件作为密钥重新签名。测试其他敏感文件尝试/proc/self/environ可能泄露环境变量其中包含密钥、/dev/null空密钥等。测试URL重定向尝试将kid改为http://attacker.com/key.jwk观察服务端是否会发起网络请求可通过自己服务器日志查看。如果会则攻击者可以托管一个自定义的JWKS完全掌控验证密钥。修复建议严格校验kid将kid视为用户输入进行严格的校验。只允许特定的、预定义的键名白名单并且禁止任何可能包含路径遍历字符../、\等或URL协议http://的值。间接映射不要直接用kid拼接路径或查询。应该维护一个安全的映射表将允许的kid映射到对应的密钥对象或安全存储路径。限制JWKS URL如果支持从URL获取JWKS必须将URL限制在可信的、预配置的域名列表内并验证HTTPS证书。使用标准库尽可能使用经过安全审计的标准JWT库它们通常对这类参数有更安全的默认处理方式。2.5 手法五未经验证的头部与载荷篡改即使签名验证本身是坚固的如果应用在验证签名之后错误地信任了Token头部或载荷中的某些字段也可能导致逻辑漏洞。这与JWT本身的密码学安全无关而是应用业务逻辑的缺陷。漏洞成因深度分析JWT签名验证只能保证Token自签发后未被篡改。但它不能保证Token里的信息在当前的业务上下文里是有效或合适的。常见的逻辑缺陷包括信任cty或typ头cty表示内容类型typ表示令牌类型。如果服务端代码根据这些字段来决定如何处理Token而攻击者可以篡改它们就可能触发不同的、有缺陷的解析逻辑。密钥ID混淆类似于kid头部可能有jku、x5u等参数指向包含密钥的URL。如果应用在验证签名后又根据这些参数去执行其他操作如记录日志、查询数据库而参数被篡改可能导致SSRF或日志注入。载荷逻辑绕过签名验证通过后应用从载荷中读取user_id、role等信息进行授权。但如果授权逻辑有缺陷例如只检查了role是否为admin却没有检查user_id是否与当前会话绑定攻击者就可以使用一个属于低权限用户但被篡改了role字段的Token前提是你能获得这个Token的签名这通常意味着你需要该用户的密钥或破解之。真实环境利用场景与实操这种漏洞的利用更依赖于对目标应用业务逻辑的理解。分析Token与业务逻辑首先你需要一个或多个有效Token并理解应用如何使用它们。通过Burp Suite重放不同权限用户的请求观察请求参数、响应差异。测试头部参数影响在确保签名有效的前提下即你需要有正确的密钥或使用原Token尝试修改头部字段。修改typ从JWT到JWTXML或其他值观察响应是否变化是否可能触发XML解析器导致XXE修改cty看是否会改变服务端对载荷内容的解析方式测试授权逻辑这是更常见的场景。假设你有一个普通用户A的Token其中user_id: 1001, role: user。你还有一个管理员用户B的Token你无法获得其中user_id: 1, role: admin。如果应用在验证签名后仅根据role字段判断管理员权限那么你可以尝试将用户A的Token中的role改为admin然后用用户A的密钥重新签名如果你能爆破或泄露用户A的密钥。但在很多情况下你无法做到。 更实际的场景是水平越权。如果应用在修改资源时只从Token的user_id字段判断所有权而不从数据库复核该Token对应的真实用户是否拥有该资源那么攻击者就可以通过修改请求中的资源ID如/api/user/1001/profile改为/api/user/1002/profile配合自己的合法Token来访问或修改他人数据。这里的漏洞不在JWT而在业务接口的授权检查不完整。修复建议签名验证后不信任可控字段理解jku、x5u、kid等字段仅在签名验证阶段有意义。验证通过后业务逻辑不应再依赖这些可能来自客户端的字段。完整的授权检查这是最重要的原则。JWT中的声明Claims只能作为初始的、快捷的用户身份和权限提示。在进行任何敏感操作尤其是写操作或访问他人数据时服务端必须进行完整的、基于数据库真实状态的授权检查。垂直越权防御不仅检查role还要将Token中的user_id与操作所涉及的用户身份进行比对。水平越权防御对于数据访问必须校验当前Token对应的用户是否拥有其请求操作的目标数据的所有权。例如在/api/orders/123接口中需要检查订单123是否属于当前用户。使用标准声明优先使用JWT标准声明如sub,iss,exp并充分理解其含义。对于自定义声明要有清晰的文档和严格的校验逻辑。3. 从靶场到实战渗透测试中的JWT审计流程掌握了具体的攻击手法我们需要将其融入到一个系统化的渗透测试流程中。以下是我在实际工作中总结的一套针对JWT的审计方法它融合了自动化工具扫描和手动深度测试。3.1 信息收集与Token识别第一步永远是发现目标。JWT可能出现在多个地方HTTP头部最常见的是Authorization: Bearer token。Cookie有时会存储在Cookie中名称可能是auth_token、jwt、session等。URL参数较少见但可能存在如?tokenJWT。Post Body在部分API设计中Token可能通过JSON body传递。在Burp Suite中你可以使用“Search”功能在整个项目范围内搜索正则表达式[A-Za-z0-9_-]\.[A-Za-z0-9_-]\.[A-Za-z0-9_-]*来快速定位JWT。或者使用jwt-heartbreaker等Burp插件进行自动识别和标记。3.2 自动化初步扫描与手动验证不要完全依赖工具但工具可以极大提高效率。将捕获到的JWT发送到Burp的JWT Editor插件或jwt_tool进行初步测试。算法测试工具会自动测试none算法、将RS256改为HS256等常见问题。敏感信息扫描解码载荷查看是否包含敏感信息如邮箱、手机号、内部ID、过长的权限列表等。这本身可能就是一种信息泄露。过期时间检查检查exp声明。是否存在exp是否设置得过长如数年这增加了Token泄露后的风险窗口。工具结果研判自动化工具可能会报出“疑似漏洞”。切勿直接相信。必须手动复现理解其报出漏洞的原理和利用条件。例如工具提示“密钥混淆可能”你需要手动检查是否有公钥泄露并尝试构造POC。3.3 深度手动测试清单在自动化扫描后按照以下清单进行深度手动测试签名验证移除测试直接删除签名部分第三个点之后的所有内容或者将签名改成任意字符串看服务端是否还会接受。这能判断服务端是否真的在执行签名验证。弱密钥爆破针对HS256 Token使用综合字典进行爆破。别忘了结合目标信息的定制字典。参数注入测试如果头部有kid、jku、x5u等参数系统性地测试路径遍历、SSRF和SQL注入。kid:../../../etc/passwd../../../../windows/win.inikey-123 OR 11jku:http://attacker-controlled.example.com/jwks.json业务逻辑测试声明篡改在能重新签名的情况下尝试修改role、user_id、email等声明测试垂直和水平越权。Token重复使用注销或修改密码后旧Token是否立即失效多端一致性在Web端登录后获得的JWT能否直接在移动端API使用反之亦然这有助于理解Token的签发和验证是否统一。密钥管理评估尝试寻找密钥泄露的迹象。检查前端JS源码、Git泄露、目录遍历读取配置文件、错误信息等。3.4 问题排查与漏洞确认技巧在测试过程中你可能会遇到各种响应如何判断漏洞是否存在响应状态码差异对比正常Token、篡改后Token、无Token三种情况下的响应码。如果篡改后返回200或302成功状态而签名无效的Token返回401/403那很可能存在漏洞。但如果都返回同样的错误页面可能需要进一步分析响应内容。响应时间差异有时漏洞存在但服务端会统一返回错误信息。此时观察响应时间可能有奇效。例如在测试密钥混淆时如果服务端错误地尝试用公钥进行HMAC运算这个计算过程可能比直接返回“无效签名”要慢几十毫秒。使用Burp Suite的Repeater模块多次发送请求并对比响应时间。错误信息泄露仔细阅读所有错误响应。错误信息中可能泄露使用的库类型、密钥的线索如“无效的密钥格式”甚至堆栈跟踪。旁路信息观察登录、刷新Token等接口的行为。有时密钥可能通过其他不安全的端点泄露。实操心得真实环境的渗透测试往往没有CTF靶场那么“标准”。服务端可能有WAF、有复杂的负载均衡、有自定义的错误处理。你的Payload可能需要编码、可能需要分多次请求、可能需要结合其他漏洞如XSS先窃取Token。保持耐心像拼图一样将各种信息组合起来才能最终确认漏洞的存在和可利用性。4. 面向开发者的JWT安全加固指南作为渗透测试人员我们的目标不仅是发现漏洞更是帮助客户修复风险。以下是一份可以直接交付给开发团队的安全加固建议。4.1 安全配置清单选择强算法并固定使用首选RS256等非对称算法。私钥安全存储于服务端公钥可用于验证即使公钥泄露也无法伪造Token。如果必须使用HS256确保密钥是密码学安全的随机字符串长度32字节并通过安全的方式管理如环境变量、密钥管理服务。绝对禁止使用none算法。在代码中显式声明允许的算法列表。使用经过审计的成熟库避免自己手写JWT解析和验证代码。使用社区活跃、经过安全审计的官方库如Java:java-jwt/jjwtPython:PyJWTNode.js:jsonwebtokenGo:golang-jwt/jwt定期更新这些库到最新版本以修复已知漏洞。严格校验声明过期时间必须校验exp并设置合理的短有效期如15-30分钟。生效时间校验nbfNot Before。签发者校验iss确保Token来自可信的签发方。受众校验aud确保Token是发给本应用的。在库的验证函数中设置这些校验参数。安全的密钥/密钥ID管理对于kid实施严格的白名单校验防止路径遍历和注入。对于JWKS URL只允许配置在可信列表内的HTTPS端点。私钥和HS256的对称密钥必须视为最高机密绝不能出现在客户端代码、版本库或配置文件中。4.2 生命周期管理与纵深防御实现Token吊销机制JWT本身是无状态的吊销困难。可以通过维护一个短小的“吊销列表”存储已注销但未过期的Token ID或在服务端维护一个“版本号”声明来实现简易吊销。用户注销或修改密码时递增其版本号验证Token时检查版本是否匹配。采用短有效期与刷新令牌模式访问令牌设置短有效期如15分钟配合一个长有效期的刷新令牌来获取新的访问令牌。这样即使访问令牌泄露攻击窗口也很小。刷新令牌必须安全存储如HttpOnly Cookie且其使用频率低泄露风险小。记录与监控记录所有JWT验证失败的事件尤其是签名无效、算法不匹配、kid异常等并设置告警。这些日志是发现攻击行为的重要线索。业务层二次授权这是最重要的纵深防御措施。JWT验证是认证不是授权。在执行业务逻辑前必须根据当前用户的真实身份可从数据库查询和操作目标进行完整的权限检查。4.3 常见错误配置与修复代码示例错误示例1依赖头部alg# 危险依赖客户端控制的alg def dangerous_verify(token): decoded jwt.decode(token, verifyFalse) # 先解码不看签名 alg decoded[header][alg] if alg HS256: key SECRET_KEY elif alg RS256: key PUBLIC_KEY # 这里用动态的alg和key去验证存在密钥混淆风险 return jwt.decode(token, key, algorithms[alg])修复后# 安全固定算法和密钥 def safe_verify_hs256(token): # 明确指定算法和密钥忽略头部alg try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) return payload except jwt.InvalidTokenError: return None def safe_verify_rs256(token): # 明确指定算法和公钥 try: payload jwt.decode(token, PUBLIC_KEY, algorithms[RS256]) return payload except jwt.InvalidTokenError: return None错误示例2未校验标准声明payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) # 直接使用payload未检查是否过期 user_id payload[user_id]修复后try: payload jwt.decode( token, SECRET_KEY, algorithms[HS256], options{verify_exp: True}, # 启用exp校验 issueryour-issuer-name, # 校验签发者 audienceyour-audience # 校验受众 ) user_id payload[user_id] except jwt.ExpiredSignatureError: # 处理Token过期 return Token expired except jwt.InvalidTokenError: # 处理其他无效Token return Invalid token从CTFHub的靶场到真实的网络攻防JWT的安全问题清晰地展示了“魔鬼在细节中”这一道理。一个强大的加密协议其安全性最终取决于开发者如何实现和配置它。对于攻击者而言理解每一种手法的原理和适用场景能让你在渗透测试中快速定位薄弱点对于防御者而言遵循安全最佳实践对JWT的整个生命周期保持敬畏是构建稳固认证体系的基石。在实际项目中我习惯在项目初期就将这些安全配置作为代码审查清单的一部分从源头减少漏洞的产生。毕竟修复一个上线后发现的认证漏洞其成本远高于在开发阶段就将其规避。