JWT安全漏洞深度解析:从算法绕过到密钥混淆攻击
1. 项目概述从JWT的本质谈起最近在和一些做安全测试的朋友交流时发现一个挺有意思的现象很多刚入行的同学对JWTJSON Web Token的理解还停留在“它是一个用来做登录验证的Token”这个层面。一旦在渗透测试或代码审计中遇到基于JWT的认证体系往往就不知道从何下手了。这让我想起几年前自己第一次遇到JWT漏洞时的情景也是对着那一串由点分隔的、看起来像乱码的字符串发愣。所以今天我想结合自己这些年踩过的坑和积累的经验系统地聊聊JWT特别是它那几种看似简单、实则非常有效的“绕过”方法。这篇文章不是教你去攻击别人的系统而是让你彻底理解JWT的运作机制和潜在风险点。无论是作为开发者加固自己的系统还是作为安全人员评估风险知其然并知其所以然都是第一步。JWT本质上是一个开放标准RFC 7519它定义了一种紧凑且自包含的方式用于在各方之间作为JSON对象安全地传输信息。你经常会在API调用、单点登录SSO等场景里见到它。它最典型的模样就是header.payload.signature这样由三部分组成的字符串用点号.连接。很多朋友觉得它安全是因为最后那个签名部分——服务器用密钥对头部和载荷进行签名验证时再核对篡改了不就失效了吗理论上是的但工程实践中的“魔鬼”往往藏在细节里。对签名的验证逻辑是否严格密钥是否足够强壮且保密算法声明是否被信任这些环节的任何一个疏漏都可能让看似坚固的JWT防线形同虚设。接下来我们就深入这些细节看看攻击者或者说我们作为防御者需要警惕的通常会从哪些角度尝试“绕过”JWT的验证。2. JWT的结构与验证机制深度解析要谈绕过必须先彻底理解它的正常验证流程。一个标准的JWT由三部分组成我们拆开来看。2.1 头部Header、载荷Payload与签名Signature的奥秘头部通常是一个JSON对象经过Base64Url编码。它最少会包含两个信息令牌的类型typ通常是JWT和所使用的签名算法alg比如HS256HMAC SHA-256或RS256RSA SHA-256。{ alg: HS256, typ: JWT }编码后就成了eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9。这里第一个关键点就出现了alg参数是声明式的。也就是说JWT自己告诉验证方“请用HS256算法来验我”。这埋下了一个信任隐患如果验证方完全相信这个声明攻击者就可以篡改它。载荷部分包含了所谓的“声明”Claims也就是我们要传递的信息。声明分三种预定义的、公共的以及私有的。像iss签发者、exp过期时间、sub主题这些都是预定义声明。载荷同样会被Base64Url编码。{ sub: 1234567890, name: John Doe, admin: true, iat: 1516239022 }编码后是第二段字符串。非常重要的一点Base64Url编码是可逆的且没有加密性。任何人拿到JWT都可以轻松解码出头部和载荷的明文内容。所以绝对不要在JWT的载荷中存放任何敏感信息比如密码、密钥等。签名部分是安全的核心。它的生成方式取决于头部声明的算法。对于HS256签名是这样产生的HMACSHA256( base64UrlEncode(header) . base64UrlEncode(payload), secret )验证时服务器会用相同的密钥secret和算法对收到的头部和载荷重新计算一次签名然后与JWT自带的签名进行比对。如果一致说明令牌未被篡改且是由知道密钥的一方签发的。2.2 服务器端的验证逻辑链一个健壮的验证流程绝不仅仅是校验签名。它应该是一条完整的逻辑链结构检查首先检查令牌是否由三部分组成用点号分隔。解码与解析Base64Url解码头部和载荷解析JSON。算法验证检查头部声明的算法alg是否在服务器信任的算法列表中。这是很多漏洞的源头。签名验证使用对应的密钥和算法进行签名计算与比对。声明校验检查关键声明如exp是否过期、nbf是否未生效、iss签发者是否可信等。业务逻辑校验将解码出的用户ID等信息与数据库或会话状态进行比对确保令牌未被吊销或重复使用。很多开发框架提供的JWT库会帮你完成第4步签名验证但第3步和第5步的严格程度以及第6步的实现很大程度上取决于开发者自己。攻击者的所有“绕过”手法几乎都是针对这条验证链上的某个薄弱环节发起的。3. 核心绕过方法原理与实战拆解理解了正常流程我们就可以切换到攻击者视角看看有哪些路径可以尝试突破。以下方法按照常见性和危害性排序。3.1 算法操纵攻击将签名算法改为“none”这是最经典、也最容易被忽视的一种绕过方法。其原理极度简单JWT规范中alg字段可以设置为none表示此令牌不进行签名验证。在JWT发展的早期一些库的实现为了兼容规范确实允许alg: none的存在用于某些不需要签名的调试或内部场景。攻击步骤截获一个正常的JWT令牌。解码其头部将alg字段的值从HS256或RS256修改为none。修改载荷部分例如将user从guest改为admin。由于算法是none签名部分就应该是空。所以我们需要删除原来的签名但保留第二个点号形成header.payload.的格式或者直接将签名部分置空有些解析器要求三段结构完整。将伪造的令牌发送给服务器。为什么能成功如果服务器的验证逻辑存在缺陷它可能只做了以下事情解码头部看到alg: “none”。认为这是一个无签名的令牌于是跳过签名验证步骤。直接信任并解析了载荷中的内容。实操心得与加固加固方法服务器端必须维护一个明确的、受信任的算法白名单如[“HS256”, “RS256”]。任何不在白名单内的alg值包括none都必须立即拒绝。现代主流的JWT库如jsonwebtokenfor Node.js,PyJWTfor Python,java-jwtfor Java默认都会拒绝none算法但如果你使用的是老旧版本或自定义实现务必检查。测试技巧在安全测试时可以尝试发送alg: None、alg: NONE、alg: nOnE等变体因为大小写处理不当也可能导致绕过。3.2 密钥混淆攻击当RS256遇到HS256这种攻击手法比“none”攻击更隐蔽需要结合具体的应用场景来理解。它利用了非对称加密RS256和对称加密HS256在验证逻辑上的差异。原理剖析RS256非对称服务器用私钥private key签名用公钥public key验证。公钥通常可以公开。HS256对称服务器用一个秘密的secret进行签名和验证。这个secret必须绝对保密。攻击场景假设一个应用原本设计使用RS256。它的验证逻辑是从JWT头部读取alg如果是RS256就用对应的公钥去验证签名。攻击步骤攻击者设法获取到应用的RSA公钥这有时并不难公钥可能硬编码在客户端、暴露在API端点如/auth/jwks.json等。攻击者伪造一个JWT将头部改为alg: HS256。关键一步攻击者将获取到的RSA公钥当作HS256算法所需的对称密钥secret来计算签名。服务器收到这个伪造的JWT解码头部看到alg: HS256。于是它调用HS256的验证逻辑使用配置中用于HS256的密钥我们称之为server_secret去验证签名。但是如果服务器的代码写得不够健壮它可能会错误地使用验证RS256的公钥去作为HS256的验证密钥。更常见的情况是服务器配置了一个全局的密钥字符串既用于HS256的secret又作为RS256的public key来源。如果这个密钥字符串恰好就是公钥本身那么验证就会通过因为签名正是用这个公钥被当作HS256的secret签发的。简单来说就是诱导服务器用非对称的公钥去执行对称签名的验证并且因为签名就是用那个公钥生成的所以验证通过。实操心得与加固加固方法绝对不要在不同算法间复用密钥材料。为RS256和HS256使用完全独立且无关联的密钥。在代码中明确区分public_key用于验证RS256和hmac_secret用于验证HS256。一个更安全的做法是根据JWT头部解码出的alg值从不同的配置源获取对应的验证密钥。测试技巧在测试时如果你发现目标使用JWT且可能涉及算法可以尝试将alg从RS256改为HS256并用你能找到的任何可能字符串如“secret”、“public key pem内容”作为密钥去伪造签名进行测试。3.3 弱密钥爆破与签名验证缺失这种方法更偏向于“暴力”破解但针对开发不当的系统依然非常有效。弱密钥爆破对于使用HS256等对称算法的JWT其安全性完全依赖于密钥secret的强度。如果开发者使用了弱密钥如secret、password、123456或者密钥与公司名、项目名、域名相关那么攻击者就可以通过字典爆破的方式尝试用这些常见密钥去验证JWT的签名。如果某个密钥能成功验证签名那么攻击者就掌握了签发合法令牌的能力。工具与实操可以使用像hashcat这样的工具进行爆破。首先需要将JWT和候选密钥列表准备好。虽然在线也有JWT破解工具但强烈不建议在非授权测试中使用在线工具提交他人的JWT因为这存在隐私和安全风险。本地化工具如jwt-tool是更好的选择。签名验证缺失这是一种更低级的错误但确实存在。即服务器的验证逻辑完全忽略了签名部分只解码并信任了头部和载荷的内容。这等价于使用alg: none但成因不同。通常发生在开发者自己手动实现JWT解析但只做了Base64解码忘了做签名校验或者错误地认为“能成功解码就是有效的”。加固方法对于对称加密必须使用强随机生成的、足够长的密钥如32字节的随机字符串。对于非对称加密确保私钥的妥善保管。无论使用哪种算法都必须严格实现签名验证步骤并将其作为验证流程中不可跳过的核心环节。3.4 声明篡改与逻辑漏洞KID、JKU、X5U的滥用JWT标准定义了一些特殊的头部参数用于指示验证密钥的来源。如果这些参数被服务器信任且未加严格过滤就会成为严重的漏洞点。kid密钥ID攻击kid是一个可选的头部参数提示服务器应该使用哪个密钥来验证此令牌。它本意是在服务器有多个密钥时方便轮转。漏洞在于如果服务器根据kid值从某个地方如文件系统、数据库动态加载密钥而kid参数用户可控就可能造成路径遍历或SQL注入。路径遍历例如kid参数被拼接成文件路径/keys/kid。攻击者可以传入kid: ../../../../etc/passwd诱导服务器使用/etc/passwd文件的内容作为验证密钥。如果签名是用这个文件的内容生成的验证就会通过。SQL注入如果kid被用于数据库查询可能引发SQL注入进而操纵密钥查询结果。jkuJWK Set URL攻击jku头部参数可以指定一个URL该URL返回一个JWK Set一组JSON格式的公开密钥。服务器理论上应该去这个URL获取公钥来验证签名。如果服务器盲目信任jku指向的任意URL攻击者就可以托管一个包含自己公钥的恶意JWK Set然后用对应的私钥签发任意令牌实现完全合法的伪造。x5uX.509证书链URL攻击与jku类似但指向的是X.509证书链。风险同理。加固方法对于kid必须进行严格的过滤和校验确保其值在白名单内或无法用于路径遍历和注入。对于jku和x5u最佳实践是彻底禁用除非有非常严格的内部需求。如果必须使用则必须将允许的URL限定在一个预先配置好的、受信任的白名单内并确保通过HTTPS访问同时要对返回的密钥内容进行有效性验证。4. 实战演练从发现到利用的完整过程光说不练假把式。我们假设一个简单的靶场场景来串联一下上述的几种方法。请注意这仅用于教育目的请在合法授权的环境中进行测试。场景你正在测试一个Web应用https://vuln-app.com。通过抓包你发现登录后的API请求头中携带了JWTAuthorization: Bearer your_jwt_token。4.1 信息收集与初步分析解码令牌将JWT复制到jwt.io的调试器或使用命令行工具echo token | cut -d . -f 1,2 | xargs -I {} sh -c echo {} | base64 -d | jq进行解码。假设你看到Header:{alg: HS256, typ: JWT}Payload:{user: guest, exp: 1743456000}观察行为尝试修改Payload中user为admin然后重新编码只修改Base64部分不动签名发送请求。预期结果是失败因为签名无效。这确认了服务器在验证签名。4.2 尝试“none”算法绕过将Header修改为{alg: none, typ: JWT}并Base64Url编码。将Payload修改为{user: admin, exp: 一个未来的时间戳}并编码。将签名部分完全移除或者置空即第三部分为空字符串。构造令牌eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4iLCJleHAiOjE5OTk5OTk5OTl9.注意最后有个点。用这个新令牌替换原请求中的令牌发送。如果返回成功说明存在none算法绕过漏洞。4.3 尝试密钥混淆攻击如果alg是RS256如果解码发现Header是{alg: RS256, ...}尝试将其改为{alg: HS256, ...}。寻找公钥。检查网页源码、JS文件、移动端应用反编译、或尝试访问/.well-known/jwks.json/auth/keys等常见端点。假设你找到了PEM格式的公钥。使用这个公钥字符串作为secret用HS256算法对新的Header和Payloaduser: admin进行签名。可以使用jwt-tool或编写简单脚本完成。# 使用 jwt-tool 示例 python3 jwt_tool.py original_token -T -S hs256 -k \-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh...\n-----END PUBLIC KEY-----\工具会生成一个伪造的、alg为HS256的令牌。用这个令牌去请求。如果服务器错误地使用RS256的公钥作为HS256的密钥来验证请求就会成功。4.4 检查敏感声明与注入点仔细检查解码后的Payload和Header寻找除了标准声明iss,sub,aud,exp,nbf,iat,jti之外的自定义声明。特别关注那些看起来像是ID、路径或配置参数的声明。尝试修改它们观察应用逻辑是否会发生变化。例如一个role声明可能直接控制前端菜单或后端权限。检查Header中是否有kid,jku,x5u等参数。如果存在按照第3.4节的方法进行测试。在整个过程中使用Burp Suite的Repeater模块会非常方便可以方便地修改和重放请求。5. 开发者防御指南构建健壮的JWT验证体系作为开发者如何避免自己的系统成为上述攻击的受害者以下是一份详细的加固清单。5.1 验证逻辑的黄金法则强制算法白名单在验证开始时就从JWT头部提取alg。只允许你明确打算使用的算法例如[“HS256”, “RS256”]。立即拒绝none以及任何不在列表中的算法。不要依赖库的默认行为显式配置它。密钥管理隔离为不同的算法使用完全独立且无关联的密钥。绝对不要将RS256的公钥用作HS256的密钥反之亦然。密钥尤其是对称密钥和私钥必须作为高敏感机密信息管理使用安全的密钥管理系统。完整声明校验过期时间exp必须校验且服务器时间必须同步NTP。生效时间nbf如果存在需校验。签发者iss和受众aud如果应用是多租户或面向多个客户端必须校验确保令牌是发给你的。令牌IDjti可用于实现令牌吊销列表。虽然JWT本身是无状态的但你可以将jti存入Redis或数据库的黑名单用于在用户登出或密码修改后立即失效特定令牌。签名验证不可绕过这是底线。无论alg是什么签名验证步骤必须执行且必须使用与算法匹配的正确密钥。5.2 对特殊头部参数的严格管控慎用甚至禁用动态密钥指示器除非有强烈的、安全的内部需求否则应禁用jku和x5u。如果必须使用URL必须严格限定于内网或预设的白名单域名并对返回的密钥进行完整性、有效性校验。安全处理kid将kid视为不可信的用户输入。避免将其直接拼接成文件路径或数据库查询语句。最好使用映射表Map或白名单机制将有限的、预定义的kid值映射到对应的密钥。5.3 其他安全最佳实践使用强密钥与定期轮转对称密钥长度要足够HS256至少32字节随机字符。非对称密钥强度要足RSA 2048位以上。建立密钥轮转机制但要注意新旧令牌的平滑过渡。令牌存储与传输安全前端不要存储在localStorage中以免遭受XSS攻击窃取。推荐使用HttpOnly、Secure、SameSiteStrict的Cookie来存储但这会牺牲一些API友好的特性。折中方案是存储在内存JavaScript变量中但页面刷新会丢失。需要权衡。传输始终使用HTTPS。设置合理的过期时间访问令牌Access Token过期时间宜短如15-30分钟配合刷新令牌Refresh Token使用。刷新令牌过期时间可较长但需单独安全存储和校验。依赖库的选择与更新使用社区活跃、经过安全审计的知名JWT库并保持更新。避免自己重复造轮子尤其是密码学相关的部分。6. 常见问题排查与疑难解答在实际开发和测试中你可能会遇到以下问题Q1我修改了Payload但签名验证失败了这是不是说明我的应用是安全的A不一定。这只是通过了最基本的签名验证。你还需要测试none算法、密钥混淆、弱密钥、以及kid等参数注入。签名验证通过只是第一道防线。Q2我使用了最新的java-jwt库还需要担心这些绕过吗A主流现代库的默认配置通常能防范none算法和明显的算法混淆。但是安全最终取决于开发者如何配置和使用这些库。如果你错误地将公钥配置成了HS256的密钥或者自己实现了部分验证逻辑风险依然存在。永远不要完全信任默认值要阅读文档理解配置项。Q3JWT的Payload可以被看到那怎么保护用户隐私AJWT的载荷Payload是Base64编码不是加密。绝对不要在其中存放密码、信用卡号、身份证号等敏感信息。如果需要传输敏感信息应该先加密敏感字段再将密文放入Payload或者考虑在传输层使用TLS并将敏感信息存储在服务器端JWT中只放一个不可猜测的引用ID。Q4用户登出后如何让JWT立即失效A由于JWT是无状态的服务器无法直接作废一个已签发的令牌。常见的解决方案有短期令牌刷新令牌Access Token有效期很短如5分钟登出时在服务端将Refresh Token加入黑名单即可。令牌黑名单用户登出或修改密码时将该令牌的jti存入一个短期的黑名单缓存如Redis有效期略长于Token本身。每次验证令牌时除了常规检查再查一下黑名单。这引入了状态但实现了即时失效。修改密钥紧急情况下如密钥泄露可以立即轮转密钥使所有旧令牌失效。但这会影响所有用户是核选项。Q5在微服务架构中每个服务都要验证JWT吗A是的这是“零信任”原则的体现。每个接收到JWT的服务都应该独立验证其签名和声明的有效性。可以将验证逻辑封装成公共库或Sidecar如Envoy过滤器来统一处理但验证动作必须发生。API网关可以进行初次验证但内部服务不应无条件信任网关传递的用户信息除非有非常可靠的内部通道和附加签名。理解JWT的绕过方法最终目的是为了构建更安全的系统。安全是一个持续的过程而非一劳永逸的状态。定期审查你的JWT实现关注依赖库的更新进行适当的安全测试才能让你的令牌真正成为守护系统的可靠卫士。