
1. 从一个看起来没毛病的登录接口说起JWTJSON Web Token这几年几乎成了前后端分离项目的标配。不管是做单页应用、移动端 API还是微服务之间的身份传递很多团队都会选择用 JWT 来承载用户身份信息。原因很直接它自带签名、可以自包含声明、服务端不用存 Session横向扩展的时候省心。但我在实际测试和代码审计里发现一个很普遍的现象很多人把 JWT 当成加密过的 Token来理解觉得只要用了 JWT里面的内容就安全了、改不了了。这个认知偏差恰恰是签名绕过类漏洞的温床。这篇文章围绕 Burp 靶场里的 JWT 签名绕过场景展开。我会把 JWT 的结构、签名机制、常见的几类绕过手法、Burp 里的实操流程以及防御侧到底该怎么写代码完整地拆一遍。适合已经了解 HTTP 抓包基础、想深入理解 JWT 安全问题的开发者和安全测试人员。哪怕你之前只听说过 JWT 这三个字母跟着走一遍也能建立起完整的认知。先说结论JWT 的签名不是加密它只保证完整性不保证机密性。Payload 部分是 Base64URL 编码的明文任何人拿到 Token 都能解出里面的内容。而签名绕过本质上就是想办法让服务端验签通过或者干脆不验签从而伪造出一个身份合法的 Token。2. JWT 的三段式结构与签名验证的真实逻辑2.1 Header、Payload、Signature 各自装了什么一个标准的 JWT 长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6InVzZXIifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c用两个点号分成三段第一段 Header描述这个 Token 用什么算法签名、类型是什么。解码后通常是{alg:HS256,typ:JWT}。第二段 Payload真正承载业务数据的地方比如用户 ID、角色、过期时间。解码后可能是{sub:1234567890,name:John Doe,role:user}。第三段 Signature对前两段做签名运算得到的结果。这里有个关键点必须强调前两段只是 Base64URL 编码不是加密。你在浏览器控制台敲一行atob就能把 Payload 解出来。所以任何把敏感信息密码、身份证号、内部密钥塞进 Payload 的做法都是错的。2.2 签名到底是怎么算出来的以最常用的 HS256HMAC-SHA256为例签名的计算过程是signature HMAC-SHA256( base64UrlEncode(header) . base64UrlEncode(payload), secret )服务端收到 Token 后会用同样的方式重新算一遍签名然后和 Token 里带的第三段做比对。一致就认为 Token 没被篡改不一致就拒绝。注意这里的措辞——没被篡改。签名验证的是完整性它回答的问题是这个 Token 是不是我签发的、内容有没有被改过而不是这个 Token 里的内容别人看不看得懂。RS256RSA-SHA256则是非对称的服务端用私钥签名用公钥验签。公钥可以公开分发私钥必须严格保管。这个区别在后面的算法混淆攻击里会变得非常重要。2.3 为什么验签这一步最容易出问题我审过不少 JWT 相关的代码出问题的地方高度集中在验签环节而不是签名生成环节。常见的坑有这么几类用了某个库的解码函数但那个函数默认不验签只是把 Payload 解出来。开发者以为自己在验签其实没有。验签逻辑里对alg字段做了信任直接拿 Header 里声明的算法去验导致攻击者可以指定算法。对none算法没有做拦截。密钥用了弱口令能被字典跑出来。这些问题的共同点是代码能跑通功能看起来正常但安全边界是破的。这也是为什么 JWT 签名绕过在靶场和真实项目里都这么常见。3. 靶场里最常见的四类签名绕过手法拆解3.1 alg 设为 none最古老也最容易被忽略的绕过JWT 规范里定义了一个none算法表示不签名。它原本的用途是给那些已经通过其他方式保证了完整性的场景用的比如 Token 在受信任的信道里传输。问题在于很多 JWT 库在实现的时候如果 Header 里写{alg:none}验签逻辑会直接跳过签名校验认为 Token 合法。攻击者只要把 Header 改成none把 Payload 里的role改成admin然后把第三段签名删掉或者留一个空字符串就能伪造出一个管理员 Token。具体操作上构造出来的 Token 长这样eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIn0.注意最后那个点号后面是空的。有些实现要求末尾必须有个点有些则连点都不要这个得根据目标行为去试。提示none的大小写在不同库里处理不一致有的只认小写none有的对None、NONE也放行。测试的时候几种写法都试一遍。3.2 算法混淆把 RS256 降级成 HS256这一类绕过更隐蔽也更高级。前提是服务端原本用的是 RS256非对称公钥是公开的。攻击逻辑是这样的服务端验签时如果它信任了 Header 里的alg字段攻击者就可以把alg从RS256改成HS256。这时候服务端会用 HS256 的验签逻辑来处理而 HS256 需要的是一个密钥。如果服务端的实现有缺陷它可能会把原本的 RSA 公钥当成 HMAC 的密钥来用。而公钥往往是公开的——可能放在/jwks.json、/.well-known/jwks.json或者干脆硬编码在前端代码里。攻击者拿到公钥后用它作为 HMAC 密钥重新签名就能构造出通过验证的 Token。这个攻击能成立的核心原因是服务端没有把算法固定死而是跟着 Token 走。正确的做法是服务端在验签时明确指定我只接受 RS256忽略 Header 里的声明。3.3 弱密钥爆破HS256 的密钥能被猜出来HS256 是对称算法签名和验签用的是同一个密钥。如果这个密钥是弱口令——比如secret、123456、password、公司名拼音——那攻击者完全可以离线爆破。流程是拿一个已知合法的 Token用字典里的候选密钥逐个计算签名比对是否和 Token 里的第三段一致。一致就说明密钥猜对了接下来就能随意伪造 Token。常用的工具是 hashcat模式 16500 专门针对 JWThashcat -a 0 -m 16500 jwt.txt wordlist.txt其中jwt.txt里放完整的 JWT 字符串。跑起来之后如果字典里有正确密钥很快就能出结果。我在实际项目里见过用your-256-bit-secret这种示例代码里的默认值直接上生产的也见过用项目名当密钥的。这类问题在靶场里是必考项在真实环境里也一点都不罕见。3.4 kid 参数注入从头部字段找突破口kidKey ID是 Header 里的一个可选字段用来告诉服务端用哪个密钥来验签。当服务端支持多密钥比如密钥轮换时会靠这个字段去查对应的密钥。如果服务端对kid的处理不严谨就可能出问题。常见的两种路径穿越kid被拼进文件路径去读取密钥文件攻击者用../../dev/null之类的路径让服务端读到一个空文件或可控内容从而用已知的密钥签名。SQL 注入kid被拼进 SQL 查询去数据库取密钥攻击者通过注入让查询返回一个自己知道的值。这类问题的根因不在 JWT 本身而在于服务端把 Header 里的字段当成了可信输入。JWT 的 Header 和 Payload 一样都是攻击者完全可控的。4. 在 Burp 里把绕过流程跑通4.1 环境准备与靶场定位Burp Suite 社区版就够用主要用到的是 Proxy抓包和 Repeater重放改包两个模块。如果你用的是专业版Scanner 能帮你自动发现一些 JWT 相关问题但手工分析仍然不可替代。靶场方面PortSwigger 官方的 Web Security Academy 里有一整个 JWT 专题从none算法到算法混淆、弱密钥、kid 注入都有对应的实验环境。每个实验都有明确的目标比如以 admin 身份删除用户 carlos做完能拿到很直观的反馈。配置上把浏览器代理指向 Burp 的监听端口默认 8080装好 Burp 的 CA 证书确保 HTTPS 流量能正常解密。这一步是基础但经常有人卡在证书没装对上。4.2 用 Repeater 手工改 Token 的完整流程假设你已经登录靶场抓到了一个带 JWT 的请求。典型的样子是GET /my-account HTTP/1.1 Host: target Cookie: sessioneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...把它发到 Repeater右键 → Send to Repeater然后按下面的步骤操作解码 Payload把 Token 中间那段复制出来用 Burp 自带的 Decoder 做 Base64 解码看清楚里面有哪些字段。重点关注sub、role、username、exp这些。构造伪造 Payload把role改成admin或者把sub改成目标用户的 ID。改完再 Base64URL 编码回去。处理签名根据你要测的绕过类型决定。测none就把 Header 的alg改成none并删掉签名测弱密钥就先用 hashcat 跑出密钥再重新签名测算法混淆就改alg并用自己的公钥签名。重放请求在 Repeater 里点 Send观察响应。如果返回了 admin 才能看到的内容说明绕过成功。这里有个实操细节Base64URL 编码和标准 Base64 不完全一样它把换成-、/换成_并且去掉末尾的。手工改的时候如果编码错了服务端会直接报格式错误容易误判成绕过失败。Burp 的 Decoder 里可以选 Base64URL 模式省得自己算。4.3 用 JWT Editor 插件提升效率手工改 Token 改多了会很烦尤其是要反复试不同算法的时候。Burp 的 BApp Store 里有个JWT Editor插件装上之后能直接在 Repeater 里对 JWT 做可视化编辑。它的几个实用功能自动识别请求里的 JWT 并高亮一键切换算法包括none内置密钥管理可以导入 RSA 公私钥、HMAC 密钥支持对 Payload 做结构化编辑不用手动 Base64我一般的工作流是先用插件快速试几种常见绕过确认方向之后再手工构造精确的 Payload。插件负责提速手工负责精确。4.4 弱密钥爆破的实操细节爆破这一步单独说一下因为坑比较多。首先你需要一个已知合法的 JWT。从登录响应里抓一个就行。把它单独存成一个文件注意不要带多余的换行或空格。然后选字典。rockyou.txt 是基础但针对 JWT 场景我建议再补一些常见的技术词汇secret、jwt、token、key、admin、password以及项目名、公司名的各种变体。hashcat 命令hashcat -a 0 -m 16500 jwt.txt rockyou.txt --force跑完之后用hashcat -m 16500 jwt.txt --show查看结果。拿到密钥后就可以用任意工具比如 Python 的 PyJWT重新签名import jwt payload {sub: administrator, role: admin} token jwt.encode(payload, cracked_secret, algorithmHS256) print(token)注意PyJWT 新版本返回的是字符串老版本返回 bytes如果打印出来带b...前缀记得 decode 一下。5. 从靶场到真实项目几个容易踩的认知坑5.1 我用了 HTTPSToken 就安全了HTTPS 保护的是传输过程防止中间人窃听。但 JWT 签名绕过攻击的是服务端的验签逻辑跟传输层没关系。攻击者完全可以自己构造一个 Token 直接发给服务端根本不需要截获你的流量。这个认知坑很普遍。我见过有团队觉得我们全站 HTTPSJWT 不会有问题结果none算法一测就穿。5.2 Payload 里不放敏感信息就行了不放敏感信息是对的但这只解决了机密性问题没解决完整性问题。就算 Payload 里只有一个user_id攻击者把它改成别人的 ID照样能越权访问。签名绕过攻击的目标从来不是读取 Payload而是篡改 Payload。5.3 用了成熟的 JWT 库就不会有漏洞库本身可能没漏洞但用法很容易出问题。比如调用了jwt.decode(token, options{verify_signature: False})这是 PyJWT 里明确关闭验签的写法但很多人复制粘贴的时候没注意。用了jwt.decode(token, key)但没指定algorithms参数老版本 PyJWT 会接受 Header 里声明的任意算法。自己手写验签逻辑结果漏了none的判断。库是工具用错了照样出事。这也是为什么靶场练习有价值——它逼你去理解每一步在干什么而不是无脑调 API。5.4 关于token 续签的一个常见误区热词里提到了 JWT 实现 token 续签这里顺带说一句。常见的续签方案是双 Tokenaccess token 短期有效比如 15 分钟refresh token 长期有效比如 7 天。access token 过期后用 refresh token 换新的。这个方案本身没问题但要注意refresh token 的验签逻辑同样不能有绕过。我见过有的实现里access token 验签很严格refresh token 却用了简化逻辑结果从 refresh 接口绕过去了。安全强度取决于最弱的那一环。6. 防御侧验签代码到底该怎么写6.1 固定算法永远不信任 Header 里的 alg这是最重要的一条。服务端在验签时必须明确指定自己接受的算法而不是读 Header 里的alg。PyJWT 的正确写法import jwt try: payload jwt.decode( token, public_key, algorithms[RS256], # 明确指定只接受 RS256 options{require: [exp, sub]} ) except jwt.InvalidTokenError: # 拒绝 passalgorithms参数是一个白名单列表只有列表里的算法才会被接受。这样即使攻击者把 Header 改成none或HS256也会直接抛异常。Node.js 的 jsonwebtoken 库同理jwt.verify(token, publicKey, { algorithms: [RS256] }, (err, decoded) { if (err) { // 拒绝 } });6.2 显式拒绝 none 算法虽然固定算法白名单已经能挡住none但多一层显式检查不亏。有些库在特定配置下可能对none有特殊处理显式判断一下更稳妥header jwt.get_unverified_header(token) if header.get(alg, ).lower() none: raise ValueError(none algorithm not allowed)注意这里用的是get_unverified_header它只解码 Header 不验签专门用来做前置检查。6.3 密钥管理别用弱口令别硬编码HS256 的密钥建议至少 256 位随机值用secrets.token_urlsafe(32)之类的函数生成。绝对不要用示例代码里的默认值项目名、公司名、域名短于 16 位的字符串提交到代码仓库里的明文密钥密钥应该通过环境变量或密钥管理服务注入并且支持轮换。轮换的时候配合kid字段但要确保kid的处理是安全的见下一节。6.4 kid 参数的安全处理如果确实要用kid处理原则是不要把kid直接拼进文件路径或 SQL 语句用白名单映射kid只能取预定义的几个值对应到内存里的密钥对象如果必须查数据库用参数化查询一个安全的映射示例KEY_STORE { key-2024-01: load_key(key-2024-01), key-2024-02: load_key(key-2024-02), } def get_key(kid): if kid not in KEY_STORE: raise ValueError(unknown kid) return KEY_STORE[kid]这样即使攻击者传了../../etc/passwd也只会得到一个unknown kid的异常。6.5 加上 exp、iat、nbf 的校验签名绕过之外时间相关的声明也要校验。exp过期时间必须检查nbf生效时间和iat签发时间建议也检查。PyJWT 默认会校验exp但如果你手动解析 Payload 就很容易漏掉。另外时钟偏移要留一点余量比如 30 秒否则分布式系统里容易出现刚签发就过期的误判。7. 我踩过的几个具体坑和排查思路7.1 改了 Token 但请求一直 401怎么定位这是最常见的困惑。我的排查顺序是先确认 Token 格式对不对。把改完的 Token 丢进 jwt.io 或者本地解码看三段是不是都能正常解出来。Base64URL 编码错了是最常见的原因。看响应头里有没有线索。有些服务端会在WWW-Authenticate里返回具体的失败原因比如invalid signature、invalid algorithm、token expired。这几个词能直接告诉你卡在哪一步。确认改的字段真的被服务端用了。有的系统 Payload 里有role字段但服务端实际是从数据库查角色Payload 里的role只是个展示用的冗余字段。这种情况下改role当然没用得改sub或者user_id。确认签名部分处理对了。测none的时候有的服务端要求末尾保留点号有的不要。两种都试。7.2 算法混淆测试时公钥拿不到怎么办公钥通常在几个地方/.well-known/jwks.json或/jwks.json登录页面的 JS 文件里有些前端会内联公钥做本地校验如果靶场提供了公钥下载直接用拿到 JWKS 格式的公钥后需要转成 PEM 格式才能给 PyJWT 用。可以用jwcrypto或者在线工具转。这一步稍微有点繁琐但转一次就能反复用。7.3 爆破跑不出来不代表密钥安全hashcat 跑不出结果可能只是字典不够好不代表密钥真的强。我一般会先用小字典快速试一轮几秒钟没结果再上大字典。如果大字典也没结果可以试试规则变形-r参数比如在单词后面加数字、首字母大写等。但反过来作为防御方不能因为字典跑不出来就认为密钥安全。攻击者的算力和字典是不断进化的唯一可靠的做法是用足够长的随机密钥。7.4 靶场环境和真实环境的差异靶场的实验环境是精心设计的每个漏洞都有明确的触发路径。真实环境里你可能遇到服务端同时支持多种算法但只在特定接口上Token 存在 Cookie 里而不是 Authorization 头里有 WAF 对明显的none关键字做拦截这些差异意味着靶场练熟之后真实测试还需要更多的信息收集和试探。但底层原理是一样的靶场帮你建立的是看到 JWT 就知道该测哪几个点的条件反射。8. 把 JWT 安全检查做成清单最后分享一个我自己用的检查清单每次遇到 JWT 相关的接口就过一遍检查项具体动作风险等级算法固定改 Header 的 alg 为 none/HS256看是否被拒高弱密钥用 hashcat 跑常见字典高kid 注入传路径穿越和 SQL 注入 payload中过期校验改 exp 为过去时间看是否仍有效中敏感信息解码 Payload看是否含密码等中签名校验改 Payload 不改签名看是否被拒高双 Token检查 refresh 接口的验签逻辑中这个清单不复杂但覆盖了绝大多数 JWT 相关的坑。我个人的习惯是只要项目里用了 JWT上线前至少把算法固定和弱密钥这两项过一遍因为这两类问题的出现频率实在太高了。JWT 本身是个好设计问题从来不在它而在于用的人有没有真正理解签名验证的是完整性不是机密性这句话。把这句话刻在脑子里很多坑自然就避开了。