JWT 这东西平时在业务系统里就是个“门禁卡”后端一验签、一解包就知道你是谁、能进哪个房间。可在靶场里它就是一套出题人精心布置的机关表面看三段字符串长得差不多实际坑位一个比一个深。我近期把某套 JWT 综合靶场从头到尾刷了一遍从最基础的弱密钥爆破到明显偏脑筋急转弯的算法混淆再到平时很少有人注意的“签名校验缺失”算是把这类题目的套路摸了个七七八八。这篇文章就按我当时解题的顺序来写把每一类题型的判断方法、实操步骤和容易踩的坑都整理出来适合正在刷 Web 安全题、准备攻防演练或者单纯想搞清楚 JWT 到底哪里脆弱的同学参考。1. 为什么 JWT 会成为靶场出题重灾区1.1 JWT 在真实系统里的普适性先说一下我为什么觉得 JWT 是靶场绕不开的考点。现在稍微新一点的 Web 系统登录之后返回一个 JWT 作为会话凭证这个模式基本是标配。前端把 token 放在本地存储里每次请求带上后端验一下签名就放行既不需要服务端存 session又天然“无状态”横向扩容也方便。正因为它应用太广安全问题一旦出现影响面就非常大。靶场出题人自然也会盯着这类“高频且高价值”的技术点。一个 JWT 题目做得好能把编码、签名、密钥管理、服务端校验逻辑全串起来考察面非常广而且从静态代码分析到动态请求测试都能覆盖。对刷题的人来说做一道 JWT 题产出的东西也远远不止一个 flag而是对认证体系整体安全性的理解。1.2 靶场出题人的常见思路我把刷过的题目复盘了一下发现出题人的思路基本围绕几个维度展开密钥相关的密钥弱、密钥泄露、密钥可预测。算法相关的算法混淆、不安全的算法声明、none 算法。校验相关的服务端压根没验签、只解包不验签、验签了但异常处理不正确。业务逻辑相关的修改用户身份、伪造管理员角色、篡改过期时间、越权访问。这四类里面“校验相关”和“业务逻辑相关”往往是同一道题里同时出现的。比如你伪造了一个管理员 token但后端根本不验签那这道题既考了你对 token 结构的理解也考了服务端实现缺陷的利用。理解出题人的套路之后刷题效率就会高很多看到一个 token 你能第一时间判断该往哪个方向试。2. JWT 三段式结构还没改 Payload 之前先理解签名是怎么出来的2.1 Header、Payload、Signature 各管什么JWT 长什么样呢就是三个用点号分隔的字符串整体类似这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.sQZuJhPmo28Vg3ZSsHhJmDkX9bE5cDcl9T8DNjkvkyA第一部分是 Header一般藏着算法声明第二部分是 Payload承载具体的业务声明第三部分是 Signature由“头部 载荷 密钥”按约定算法算出来。Header 最典型的例子是{alg:HS256,typ:JWT}这里的 alg 声明了签名算法typ 基本可以忽略大部分实现里就是纯标识。Payload 里常见的字段有 username、role、user_id、exp、iat、iss 等这些字段往往就是题目里要改的目标。2.2 签名算法分类HS 系列和 RS/ES 系列签名算法大致分成两类对称算法 HS256/HS384/HS512加密和解密用的是同一个密钥。服务端拿到 token用自己保存的密钥重新算一遍签名跟 token 里带的签名比对一样就通过。问题是这个密钥一旦泄露或者选得太简单攻击者就能自己伪造 token。非对称算法 RS256/ES256 等签名用私钥验签用公钥。服务端只拿公钥验签私钥保管在更安全的地方。理论上就算公钥公开攻击者也没法伪造签名因为只有私钥能签名。理解这一层区别非常重要因为后面说的“算法混淆攻击”就是故意让服务端把“非对称验签”误当成“对称验签”来用。2.3 签名生成过程的底层逻辑以 HS256 为例签名的计算过程可以写成signature HMAC_SHA256(secret, base64url(header) . base64url(payload))也就是说把 Header 和 Payload 分别做 base64url 编码中间用点号拼起来再用密钥做一次 HMAC-SHA256 运算得到的结果再做 base64url 编码就是签名。RS256 就复杂一些它是用 RSA 私钥对整个“header.payload”字符串做签名验签的时候用公钥。这个过程的重点在于整个签名校验的核心输入是“header 里写的算法”和“服务端保存的密钥”。很多新手不理解为什么改了 payload 后面签名就失效了。原因很简单签名是拿原始 payload 算出来的只要 payload 动一个字节重新计算的签名就完全不同。除非你已经拿到了密钥或者服务端压根不校验签名否则盲改 payload 基本没用。2.4 base64url 编码的常见细节JWT 用的编码不是普通 base64而是 base64url。区别在于普通 base64 里会有、/、这几个字符而 base64url 把换成-把/换成_结尾的通常也会去掉。我见过不少人在改 payload 的时候直接拿在线 base64 工具编码完就拼进 token 里结果发现服务端报错或者验签不过。原因就是编码方式不一致。你在手动改 token 的时候一定要确认用的是 base64url 且不带填充符号否则很容易在题目里绕圈子浪费时间。3. 高频题型拆解从弱密钥爆破到算法混淆3.1 弱密钥爆破拿到 token 先试这一招刷 JWT 题时看到 HS256 的 token第一反应就是猜密钥。很多靶场为了防止题目太难会把密钥设成常见弱口令比如secret、password、123456、admin之类。也有的会把密钥放在字典里等着你爆破。我的判断顺序是先看 header 里 alg 是不是 HS256如果是说明是共用密钥体系爆破可行性高。再用常见弱口令字典直接试。如果常见字典没跑出来再考虑从题目环境里找线索比如网页源码、JS 文件、注释、接口返回等地方可能藏着密钥。爆破工具方面hashcat 可以直接解 JWT 的 HS256 模式hashcat -m 16500 jwt.txt wordlist.txtjwt.txt 里放完整的 JWT 字符串wordlist.txt 放字典。跑出来之后就可以用得到的密钥伪造任意身份了。这里说个坑有些靶场用的密钥是随机长字符串那基本不是靠爆破能解决的你就得换个思路比如去找服务端配置文件、Git 仓库泄露、备份文件之类的信息泄露点。3.2 算法混淆RS256 怎么被 HS256 带偏的算法混淆是我刷这套题时觉得最绕的一种题型也是面试和攻防里很喜欢考的点。它的核心逻辑是这样的服务端本来用的 RS256 验签也就是用公钥验证签名结果攻击者把 Header 里的算法改成 HS256同时用公钥作为 HMAC 的密钥去生成新签名。如果服务端支持根据 Header 动态选择算法并且没有限制算法白名单那么服务端就会用公钥去当 HS256 的密钥来验签——这时候攻击者已经用这个“公钥”签了一个 token验签自然就通过了。这个攻击成立的两个前提是服务端验签时信任 Header 里的 alg 字段。攻击者能拿到公钥。公钥通常藏在靶场某个接口里或者以.pem文件、JWK 格式直接暴露。实操上你可以先找公钥然后构造一个 Payload把 alg 改成 HS256再用公钥字符串作为 HMAC 密钥生成签名。最终得到的 token 放进请求里如果服务端不再报签名错误说明算法混淆攻击成功了。注意公钥作为 HMAC 密钥时具体用的字符串格式要和服务端代码一致。有的场景直接用-----BEGIN PUBLIC KEY-----整段有的场景用的是去掉头和换行后的 base64 字符串这个细节我在后面的排坑部分会专门说。3.3 签名校验缺失最“朴实”但最实用的一类题签名校验缺失听起来很低级但在实战里并不少见。比如有些开发者在写代码时只解析了 token 的 Payload根本没验证签名或者已经验证签名但在异常分支里直接返回了 Payload 而非报错。碰到这种题解题步骤极其简单把 JWT 复制下来。解码 Payload 部分。把 username、role、user_id 改掉。重新 base64url 编码拼回去签名部分原封不动甚至随便填一串字符。这时候你可能会想“签名都不对服务端怎么会认”可现实就是服务端根本没验签它只解 Payload自然认不出你已经动过手脚了。判断这类题的方法也很简单拿一个明显篡改过的 token 去请求如果服务端没有报签名相关错误而是直接正常响应或者只报“用户不存在”这类业务错误那大概率就是没验签。3.4 密钥泄露与硬编码题面线索往往就在眼前还有一类题密钥并不是靠爆破解出来的而是直接藏在你能看到的地方。常见藏法网页源码里的 JS 注释。接口返回里的调试信息。静态资源文件里的配置备份。响应头里的自定义字段。前端打包文件里的环境变量。刷题的时候千万不要一头扎进去爆破。先把靶场提供的所有页面、接口、静态文件过一遍看看有没有和 key、secret、token 相关的字眼。就拿我之前刷的一道题来说密钥直接写在登录接口的响应注释里开发者加了个// keytestkey123之类的调试信息后面被带到生产环境了。这种题考察的就是信息收集能力和细心程度。拿到密钥之后无论 HS256 还是 RS256你都能直接构造任意 token剩下的事情就是伪造管理员身份然后取 flag。3.5 none 算法与过期时间绕过理解服务端异常处理none 算法这种题比较老现在很多 JWT 库已经默认禁止了但靶场里为了教学还是会保留。原理是 Header 里写入{alg:none,typ:JWT}签名部分留空如果服务端解析时没有对“空签名”做校验就能直接通过。处理办法是把 Header 改为 none。Payload 改成想要的身份。签名部分干脆不写变成header.payload.或者header.payload视服务端实现而定。过期时间绕过也属于这一类。有的靶场给你一个 token 但已经过期如果你想继续用就先把 exp 改成未来的时间戳再重新签名。如果服务端没有认真校验 exp或者说异常处理不严谨依然会通过。还有一种情况是 exp 字段缺失服务端可能直接当成永不过期处理。4. 实操落地解码、爆破、伪造、验证一条龙4.1 准备一个趁手的实验环境刷题前我习惯先把工具备齐。本地装好 Python 3 环境装一个 PyJWT 库再准备一个常见的 JWT 字典和 hashcat如果要用。不装也行但后面有些自动化的伪造和批量测试还是会用到。安装 PyJWT 的命令很简单pip install pyjwt版本不同API 有区别后面我会提到新老版本的差异。还有一个非常好用的命令行工具叫 jwt_tool它把解码、爆破、算法混淆、none 攻击这些都集成在一起了适合快速验证思路。不过我建议你先把原理弄清楚再依赖工具不然工具跑不出来你都不知道问题出在哪。4.2 解码分析先看结构再定策略拿到一个 JWT第一步永远是“拆开看”。用 PyJWT 可以直接解码但不验签import jwt token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.sQZuJhPmo28Vg3ZSsHhJmDkX9bE5cDcl9T8DNjkvkyA data jwt.decode(token, options{verify_signature: False}) print(data)这样不需要密钥就能看到 Payload 里的内容。你的任务就是根据这些内容判断要改什么是改 username 还是改 role还是把 user_id 改成某个已知的管理员 ID。同时看 Header 里的 alg如果是 HS256准备爆破如果是 RS256先找公钥如果完全没有签名进入无验签路线。4.3 弱密钥爆破与伪造示例假设你已经判断目标是 HS256而且密钥就是字典里的某个词。先用 hashcat 爆破得到密钥testkey123下一步就是直接伪造一个管理员 tokenimport jwt payload { username: admin, role: admin, user_id: 1 } key testkey123 token jwt.encode(payload, key, algorithmHS256) print(token)这里要注意旧版 PyJWT 的jwt.encode(payload, key, algorithmHS256)能直接用新版则要求传入algorithms列表或者使用algorithm参数写法大体兼容。生成之后拿到靶场接口里替换原来的 token 进行测试。4.4 算法混淆的模拟过程RS256 转 HS256 的题我一般先把公钥保存成public_key.pem然后用 Python 脚本伪造 tokenimport jwt public_key open(public_key.pem, r).read() payload { username: admin, role: admin } token jwt.encode(payload, public_key, algorithmHS256) print(token)这段代码的本质是把公钥内容当作 HS256 的对称密钥来用。如果服务端没有正确配置算法白名单就会用同一个公钥去验签结果自然能通过。这里有个细节有些靶场的公钥是直接给的一段字符串而不是 PEM 格式文件你需要手动拼成统一格式再试。4.5 把伪造好的 token 放到请求里伪造只是第一步真正出结果要看服务端认不认。请求时通常用 Authorization 头Authorization: Bearer 你的token也有的靶场把 token 放在 Cookie 里字段名可能是 token、jwt、session 之类的你得先看登录接口的返回和请求包。用 Burp Suite 或者浏览器开发者工具改一下请求替换成新 token再访问需要管理员权限的接口。返回 200 并且拿到 flag这道题就算通关了。4.6 通用自动化脚本的思路刷的题多了之后你会发现很多步骤是重复的于是我写了一个很小的工具函数方便快速测试不同思路import jwt def create_token(payload, key, algorithm): if algorithm none: header {alg: none, typ: JWT} encoded_header jwt.utils.base64url_encode(jwt.utils.bytes_to_string(jwt.utils.json.dumps(header).encode())) # 这里只演示思路none 场景通常需要手工拼字符串 return None return jwt.encode(payload, key, algorithmalgorithm)当然这只是个框架具体使用时还要根据字典爆破的结果和目标服务端的响应来做调整。写脚本的目的不是炫技而是把反复操作的部分收拢起来减少人工失误。5. 排坑手册我刷 JWT 题时踩过的雷5.1 改了 Payload 却没重算签名这是新手最常见的错误。很多人解码 Payload 后直接把修改后的内容 base64 一下拼回去忘了签名是拿原 Payload 算的。服务端一验签立刻就发现签名不匹配。正确做法是在没有密钥的情况下就只考虑“无验签”型题目如果判断需要验签通过就必须先用密钥或公钥重新算签名。5.2 公钥格式不统一导致伪造失败我在做算法混淆题的时候被公钥格式坑过一次。从接口拿到的公钥是-----BEGIN PUBLIC KEY-----开头、-----END PUBLIC KEY-----结尾的完整 PEM 文件但靶场代码里使用的是去掉换行的紧凑格式。我直接用完整 PEM 去签名结果服务端验签一直失败后来才发现它内部把换行去掉了。建议多准备几个格式版本完整 PEM。去掉头和尾的 base64。去掉所有换行后的紧凑串。逐个试一遍总能撞对。5.3 看错了服务端的校验逻辑有些题目表面上是 JWT 题但核心考点并不在签名。比如服务端虽然验签但它验签之后还会拿着 Payload 里的 username 去数据库查一次真实角色这时候你光把 role 改成 admin 是没用的得把 username 也改成数据库里真实管理员的名字。遇到这种情况我的经验是先看接口响应。如果伪造 token 后报“用户不存在”之类的错误说明服务端拿 Payload 里的身份去查库了如果报“权限不足”说明身份校验和角色校验是分开的可能还藏着一个查询或者接口逻辑要处理。5.4 exp 时间戳的单位换算JWT 里的过期时间 exp 是 Unix 时间戳单位是秒。有一次我为了绕过过期时间限制直接把当前时间戳不乘 1000 填进去结果服务端判断 token 已经过期反复报错。后来才发现自己把它当成了毫秒。记住一个规律如果当前时间是1700000000十位数直接当秒填。如果当前时间是1700000000000十三位数那是毫秒除非服务端特殊处理否则要除以 1000 再填。5.5 PyJWT 新老版本接口差异以前写jwt.decode(token, verifyFalse)还能用新版本已经强制要求options{verify_signature: False}这种写法。伪造 token 时新版本要求明确指定 algorithm 参数不填会直接报错。建议刷题前固定一个虚拟环境省得被版本差异干扰。5.6 别忽略“签名校验失败”和“解析失败”的语义区别服务端返回错误时你要区分是签名校验失败还是 JWT 结构解析失败。前者说明服务端确实验签了如果攻击方向对那就是密钥或者算法问题后者说明服务端可能连格式都不认或者你拼接的字符串有问题。不要看到 401 就以为是验签失败先详细看一下错误信息。6. 解完题之后真实业务里如何补上这些漏洞6.1 算法层面白名单控制靶场里刷到的所有 JWT 漏洞映射到真实系统里第一道防线就是算法白名单。服务端在验证 token 时不能盲目信任 Header 里的 alg 字段要固定允许的算法列表。比如后端只允许 RS256那遇到 Header 里写着 HS256 的 token直接拒绝而不是真的去用公钥做 HMAC 验签。很多成熟 JWT 库已经默认实现了算法白名单但如果代码里写成“动态获取 alg”再传给库函数绕过白名单的问题就又出现了。代码评审里这一点特别值得关注。6.2 密钥管理弱密钥是万恶之源我相信刷完 JWT 题的同学对“密钥不能乱设”这件事都会印象极深。一个 HS256 token密钥设成secret就等于把门禁卡制作权限交给了所有人。生产环境一定要用足够长度的随机密钥并且定期轮换。密钥轮换机制要考虑旧 token 的兼容性但不能因为兼容就永久保留旧密钥。6.3 校验逻辑先验签再解析服务端的校验顺序应该是“先完整验签再解析 Payload再做业务判断”。很多漏洞都是因为先解析了 Payload或者解析失败时错误地走到了默认放行分支。这里有个反模式很常见开发者在异常捕获里返回了 Payload 而不是错误导致签名校验成了摆设。建议写一个统一的认证中间件负责解析和验签所有业务接口都只从这个中间件的上下文里读取用户身份而不是各自去解析 token。6.4 日志与监控看到异常请求能溯源真实攻防里攻击者伪造 token 的行为不会每次都成功失败尝试也会留下痕迹。服务端应该记录 JWT 验签失败、alg 异常、token 过期等关键事件。我个人的经验是把“验签失败”单独记录成一个告警项比记录所有请求靠谱得多。因为正常业务里验签失败的比例非常低一旦突然升高大概率有人在打你的认证接口。6.5 失效与续期别让 token 一劳永逸最后多说一句JWT 是无状态 token签发之后服务端很难主动让它立刻失效。业务上如果涉及封号、踢人、权限变更单纯靠 JWT 的过期时间往往不够实时。可以考虑引入 token 版本号、用户状态校验或者把关键权限从 token 里拿出来再查库。这一点靶场题里遇不到但真实系统必须面对。刷完这套 JWT 靶场我最大的体会就是JWT 本身并不复杂复杂的永远是使用它的上下文。无论是爆破、算法混淆还是无验签利用都是在和服务端代码的“懒惰”和“信任”博弈。把这些题做明白再回头看自己负责的系统你会不自觉地想检查一下算法白名单、密钥长度和异常处理分支。这种“解题之后的后怕”大概就是刷靶场最值钱的收获。
阅读完成 · 觉得有帮助?