
说起JWTJSON Web Token弱密钥很多人第一时间想到的是那些“HS256对称密钥别写死在代码里”的安全建议但真到了实战里从拿到一个JWT到成功伪造认证令牌中间隔着环境搭建、算法识别、字典爆破、签名替换一大串细节。最近我在Kali环境里把这条链路完整跑了一遍从Docker拉起靶标到Burp Suite重放利用整个过程中踩了不少坑也把每一步的取舍都理清了。这篇文章就当成一次完整的手把手复盘把每个环节的“为什么这么做”也一并讲透。这篇文章适合三类人看一是做渗透测试或安全评估的朋友需要在授权测试里快速验证JWT类漏洞二是前后端开发同学想搞清楚为什么密钥短就容易出事以及攻击者到底怎么利用三是CTF玩家这类题目在Web方向几乎年年出现。整个流程不依赖特定的靶场平台环境全部由Docker自建保证可复现。开始之前必须强调一句本文所有操作只面向自建靶标、CTF赛题或已获得书面授权的测试环境不要拿这些手法去碰任何未经授权的系统。安全测试的根本目的是帮人补墙不是破门。1. 为什么瞄准JWT弱密钥这条路1.1 JWT三段落结构与HS256的原理JWT说白了就是一个经过签名的JSON对象由三部分组成用点号分隔eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoic3VwZXJhZG1pbiJ9.8JpGj2Nn6sP6dQ4xVg8WvV1ZxkYVHQ8iZFJ9KJxK0qY第一段是Header说明签名算法和令牌类型第二段是Payload存放用户信息、过期时间、角色等声明第三段是签名用来保证前两段内容没有被篡改。以最常用的HS256来说签名算法是HMAC-SHA256它是对称的——同一个密钥既用来签名也用来验证。服务器的逻辑是用密钥对“Header.Payload”这段内容计算HMAC得到的结果和客户端令牌里的第三段比对一致就认为是合法的。现在关键问题来了HMAC的计算只需要密钥而攻击者手里已经握着完整的JWT。他可以离线拿任意字符串当密钥把前两段拿去跑HMAC比对输出签名是否一致。只要密钥本身足够简单就能在本地无限次尝试服务器几乎没有感知连日志都不会多一条。这就像你捡到了一把锁没人拦着你你可以回家慢慢试钥匙——锁本身不会报警也不限制你试错的次数。1.2 弱密钥为什么能成为突破口很多系统在开发期图省事直接写一个类似secret、123456、admin、secret123这样的字符串当签名密钥上生产环境的时候忘了换。更常见的情况是密钥虽然看起来不简单但本质上是一句英文短语、一个生日、一个公司名加年份这种都逃不过字典攻击。举个例子密钥secret123的熵大概也就30 bit左右而HMAC-SHA256本身没有任何防爆破机制。GPU跑SHA256的算力是以十亿次每秒为单位的配合rockyou这种大型字典十几分钟内就能试完几千万个候选词。要是钥匙是admin888这种那更快。正因为JWT的签名是可以在本地独立校验的弱密钥问题从“服务器配置隐患”直接变成了“离线可破解漏洞”。这也解释了为什么业界反复强调HS256的密钥必须用高熵随机数长度至少32字节。下面整个实验流程就是围绕这个短板展开的。1.3 这套流程适合谁需要准备什么这套流程不需要你从零搭建显存怪兽服务器大部分工作在CPU上就能完成只不过GPU会快很多。前置条件并不苛刻Kali环境虚拟机或物理机均可我用的版本里自带Burp Suite Community和hashcat省去不少安装功夫Docker可用用于拉起靶标应用一份常用密码字典Kali自带的/usr/share/wordlists/rockyou.txt就够用基础的HTTP知识知道请求头、响应状态码是什么就行如果你还没装Docker一条apt install docker.io就能搞定然后记得把当前用户加进docker组并重启终端。Kali本身是滚动更新Docker兼容性一般没什么问题。2. Docker快速搭建带JWT鉴权的靶标环境2.1 靶标应用的设计与镜像选择我选择用Flask来写靶标原因是PyJWT库支持HS256开箱即用代码量极小读起来一目了然适合拿来做教学演示。镜像直接基于python:3.11-slim这是官方精简版体积小、拉取快不会因为镜像过大把整个实验时间耗在下载上。靶标的功能设计成两个接口一个/login用写死的账号密码换JWT一个/admin校验请求头里的JWT通过后返回管理数据。为了演示方便我把密钥直接写死在代码里值设为secret123这是有意为之的弱密钥。生产环境千万别这么干后面第七节会专门说加固方案。需要说明的是我没有用某些现成的“带漏洞靶场镜像”原因很简单现成镜像的漏洞点往往是固定的密钥、路径、接口逻辑都不透明出问题了你很难判断是靶标的问题还是自己操作的问题。自己写一个每一步都清清楚楚。2.2 Dockerfile与启动配置先创建项目目录放三个文件app.py、Dockerfile、requirements.txt。app.py的核心代码如下from flask import Flask, request, jsonify import jwt import datetime app Flask(__name__) SECRET secret123 # 仅限实验室演示用的弱密钥 app.route(/login, methods[POST]) def login(): data request.get_json() if data.get(username) admin and data.get(password) admin123: token jwt.encode( { user: admin, role: user, exp: datetime.datetime.utcnow() datetime.timedelta(hours1), }, SECRET, algorithmHS256, ) # jwt.encode 返回字符串直接返回即可 return jsonify({token: token}) return jsonify({error: 认证失败}), 401 app.route(/admin, methods[GET]) def admin(): auth request.headers.get(Authorization, ) token auth.replace(Bearer , ) try: data jwt.decode(token, SECRET, algorithms[HS256]) except jwt.ExpiredSignatureError: return jsonify({error: 令牌过期}), 401 except jwt.InvalidTokenError: return jsonify({error: 无效令牌}), 401 if data.get(role) superadmin: return jsonify({message: 破解成功你已拿到管理员权限, flag: JWT-WEAK-SECRET-0427}) return jsonify({error: 权限不足当前角色: str(data.get(role))}), 403 if __name__ __main__: app.run(host0.0.0.0, port5000)接口逻辑里特意做了一层角色区分正常登录拿到的role是user访问/admin会返回403只有把role改成superadmin才能通过。这样后面破解完密钥之后就有清晰的利用前后对比。requirements.txt就两行flask pyjwtDockerfile这样写FROM python:3.11-slim WORKDIR /app COPY app.py . COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt EXPOSE 5000 CMD [python, app.py]构建和启动docker build -t jwt-demo . docker run -d -p 5000:5000 --name jwt-demo jwt-demo参数解释一下-d表示后台运行-p 5000:5000把容器内5000端口映射到宿主机的5000端口--name给容器起名方便后续管理。如果端口被占用把左边的5000改成别的比如-p 5001:5000后面访问时用对应端口即可。2.3 联通性检查与应用验证容器起来之后先确认它在跑docker ps | grep jwt-demo curl -X POST http://127.0.0.1:5000/login \ -H Content-Type: application/json \ -d {username:admin,password:admin123}正常会返回一个JSON里面带一串JWT。把JWT放到Authorization头里去访问admin接口curl http://127.0.0.1:5000/admin \ -H Authorization: Bearer 刚拿到的token此时应该返回403提示“权限不足当前角色: user”。这个结果很重要它意味着服务器确实在校验角色字段我们的攻击目标非常明确把role从user篡改成superadmin同时让令牌通过签名校验。到这里整个靶标环境和预期行为已经对齐了下一步进入抓包分析阶段。3. Burp Suite抓包与JWT令牌结构拆解3.1 拦截带Authorization头的请求Kali自带Burp Suite打开后先在Proxy选项卡里的Intercept打开拦截开关。浏览器侧需要把代理指到Burp的监听地址默认是127.0.0.1:8080。我用的是Firefox装个FoxyProxy之类的代理切换插件比较省事或者直接在Firefox网络设置里手动填代理也行。配置好代理后浏览器访问http://127.0.0.1:5000/login先用Burp拦截登录请求把登录表单改成POST请求提交usernameadminpasswordadmin123放行后响应里就带着JWT。再访问/admin这次带上了Authorization头Burp里能看到完整的请求GET /admin HTTP/1.1 Host: 127.0.0.1:5000 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoidXNlciIsImV4cCI6MTcx...把这个请求选中右键Send to Repeater后面伪造令牌都在这里操作。3.2 手工解码与三段结构分析JWT用的是Base64URL编码和标准Base64的区别在于URL安全的Base64把换成-把/换成_而且去掉了末尾的填充。所以解码的时候要用base64命令配合-d先把URL安全的字符替换回来。把Token复制到终端里分段解码TOKENeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoidXNlciIsImV4cCI6MTcx... echo $TOKEN | cut -d. -f1 | tr _- / | base64 -d echo $TOKEN | cut -d. -f2 | tr _- / | base64 -d第一段解出来是{alg:HS256,typ:JWT}第二段解出来是{user:admin,role:user,exp:171...}第三段是签名没法直接解出明文它是用密钥对“第一段.第二段”做HMAC-SHA256之后的结果。这一步做完攻击面的两条路就很清晰了算法声明是HS256意味着可以尝试离线爆破密钥Payload里的role字段是我们篡改的目标。如果算法声明是none那连密钥都不用猜如果Header里写的是RS256却拿的是个公钥那就还有算法混淆利用的可能。这些后面都会提到。3.3 攻击面判定哪种算法能破解JWT算法层面的判断直接决定后面用什么工具Header中的alg签名机制是否可离线破解HS256/HS384/HS512HMAC 对称密钥可以只需爆破密钥RS256/RS384/RS512RSA 私钥签名不行私钥在服务器手里ES256ECDSA 私钥签名不行私钥在服务器手里none无签名直接改Payload即可注意Header里的alg是明文可篡改的。如果服务器校验时没有对算法白名单做硬约束攻击者可以把RS256改成HS256然后用服务器端的RSA公钥当HMAC密钥去签名这就是经典的算法混淆攻击。不过这篇文章的主线是弱密钥爆破算法混淆在第六节我会专门展开。就当前靶标而言Header明确写着HS256服务器端代码里也写死了只用HS256验证那么唯一的突破口就是密钥强度。进入下一节开始爆破。4. 弱密钥爆破hashcat与jwt_tool双路线4.1 工具选型对比爆破JWT密钥的常见工具有三款hashcat、jwt_tool、John the Ripper。我分别说下它们的特点和适用场景。hashcat目前公认效率最高的密码恢复工具支持GPU加速JWT专用模式是16500。适合大规模字典、规则攻击和掩码攻击缺点是命令参数多新手容易在格式上卡壳。jwt_tool专攻JWT的安全测试工具对应的爆破参数是-C -d加字典它内部会做完整的密钥猜测和签名比对输出友好还能顺带做算法混淆、Payload篡改测试。缺点是纯Python实现速度远不如hashcat。John the Ripper有个jwt2john.py转换脚本把JWT转成John能识别的格式适合习惯John操作流的人。速度居中但配置流程多一步。我的建议是实验和学习阶段用jwt_tool因为它的输出信息量足、易读真到实战且机器有GPU用hashcat速度是数量级的差距。下面两条路线我都跑一遍。4.2 hashcat模式16500实战先用hashcat爆破。把完整的JWT保存为文件注意文件里只需要一个token不要加任何多余字符echo -n 完整的JWT jwt.txt然后确认rockyou字典存在。Kali里rockyou默认是压缩包要先解压ls /usr/share/wordlists/ sudo gunzip /usr/share/wordlists/rockyou.txt.gz执行爆破命令hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt --force解析一下这个命令-m 16500指定JWT破解模式--force用于在OpenCL环境检测异常时强行运行有GPU报错也常见不加这个会中断。如果一切正常爆破过程会滚动显示每秒尝试的密钥数。我在这台只有CPU的虚拟机上跑速度大约每秒几十万次rockyou里的一千多万个词条也就几分钟的事。GPU环境下每秒十亿次以上也不稀奇。爆破结果会显示类似这样的信息Session..........: hashcat Status...........: Cracked Hash Target......: eyJhbGciOiJIUzI1NiIs... Recovered........: 1/1 Candidates.#1....: secret123 - secret123看到Status: Cracked密钥就是secret123。如果字典跑完没结果说明密钥不在基础字典里这时候要升级策略# 用规则引擎衍生更多变体 hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt -r /usr/share/hashcat/rules/best64.rule # 或者针对短密钥用掩码爆破8位纯小写字母 hashcat -m 16500 jwt.txt -a 3 ?l?l?l?l?l?l?l?l掩码攻击里?l代表小写字母?u代表大写字母?d代表数字。对8位以内的弱密码组合空间完全在可接受范围内。4.3 jwt_tool字典爆破与结果验证第二条路线用jwt_tool。Kali里大概率没有预装先从项目仓库克隆下来git clone jwt_tool项目地址 cd jwt_tool pip3 install -r requirements.txt用法很直观直接把token传进去-C是crack模式-d指定字典python3 jwt_tool.py 完整的JWT -C -d /usr/share/wordlists/rockyou.txtjwt_tool会先解析token的结构打印出Header和Payload的明文然后逐词带入HMAC运算比对签名。跑完后如果命中会直接输出[] secret123 is the CORRECT key!它还支持-M参数指定攻击模式-M hs256可以限定只测HS系列可以配合其他测试一起用。爆破出密钥之后建议先手动验证一下确认这个密钥是真的而不是误报。用Python一行就能验echo -n header.payload | openssl dgst -sha256 -hmac secret123 -binary | openssl base64 -url把输出和JWT第三段对照一致就实锤了。这一步很有必要因为有些场景下字典里恰好有多个词都能通过某种签名格式的巧合匹配手动验证能排除假阳性。5. Burp Suite伪造令牌与重放利用5.1 安装并配置JWT Editor插件拿到密钥之后就该把知识变成“战力”了。Burp Suite里最顺手的JWT操作插件是JWT Editor在BApp Store里直接搜就能装。装完在Burp顶部菜单栏会多出JWT Editor的标签页。这个插件做的事情很简单它能把请求头里的JWT自动解码成可编辑的JSON表单改完以后直接用你配置好的密钥重新签名。相当于把“解码、篡改、重签、替换回Authorization头”这一串手工操作合成了一个动作。先添加签名密钥进入JWT Editor标签页点击New Key选择HMAC-SHA256类型把secret123填进去密钥格式选Text字符串而不是Base64。这里要注意如果密钥本身是二进制或Base64存的选错格式会导致签名结果不一致是个很容易踩的坑。5.2 修改Payload并重新签名回到Repeater把之前抓到的/admin请求翻出来。在请求编辑区域选中整个JWT右键选择“Send to JWT Editor”。JWT Editor里会把Token的三个部分拆开显示。定位到Payload段找到role字段把值从user改成superadmin。改的时候注意JSON格式别把逗号引号弄丢了。如果想把user字段也一起改成别的随你签名校验看的是整体内容只要密钥对就行。改完之后点Sign按钮选择刚添加的HMAC密钥插件会自动用新密钥对修改后的Header.Payload重新计算签名生成一个新的Token。然后点Copy JWT把新Token复制回Repeater的Authorization头里替换掉原来的值。有个细节值得说下JWT的exp字段是Unix时间戳如果修改操作时间太久原Token过期了服务器会返回401“令牌过期”。解决方案是在Payload里重新算一个当前时间加一小时的exp或者在Payload里把exp删掉但部分服务端会强制校验这个字段所以更稳妥的做法是设置一个合理的未来时间戳。Kali终端里执行date -d 1 hour %s就能拿到。5.3 重放验证从破解到利用的完整闭环替换完Token后在Repeater里直接点Send。这时候服务器的视角是收到了一个签名完全合法的JWT因为密钥确实是secret123Payload里的角色是superadmin没有理由拒绝。正常响应会返回200内容类似{message:破解成功你已拿到管理员权限,flag:JWT-WEAK-SECRET-0427}对比一下利用前用user角色Token访问同一个接口拿到的403整个攻击链就闭环了拿到合法JWT → 识别HS256 → 爆破弱密钥 → 篡改角色声明 → 重新签名 → 服务端完全信任。这一步也顺带说明了为什么JWT的Payload绝对不能存信任边界不可触碰的数据更不能把角色、权限这类敏感声明交给客户端自报家门。Token虽然签名防篡改但算法和密钥强度一旦失守签名就变成了一层纸。6. 实战中的典型坑点与排查经验6.1 高频问题速查这条路我第一次完整走下来至少踩了四五个坑。挑高频的整理成表遇到问题先对着查现象可能原因解决方式hashcat报Token Length Exception文件里token前后有空白字符、换行或者多个token用cat -A jwt.txt检查不可见字符确保只含一个完整tokenhashcat跑完0/1未破解字典没覆盖到真实密钥换规则攻击、掩码攻击或加大字典jwt_tool报Missing header/payloadtoken被浏览器或终端截断手动复制完整token用echo $TOKEN签名后服务器仍报Invalid Token密钥格式选错或签名算法没选对检查JWT Editor里Key类型是否选HMAC-SHA256、格式是否Text报ExpiredSignatureErrorexp时间戳过期重新生成未来时间戳或去掉exp字段后看服务端是否强制校验Burp拦截不到流量浏览器代理未生效或代理端口不对检查Burp监听地址与浏览器代理设置最直接的验证是用浏览器访问http://127.0.0.1:8080看是否出现Burp证书页还有一个很容易忽略的问题/login接口的账号密码不是重点很多人卡在爆破完密钥后不知道怎么验证密钥对不对。这时候最简单的验证方式是用PyJWT本地签名一个测试Token再解码全程离线不要依赖靶标环境import jwt token jwt.encode({user:admin,role:superadmin}, secret123, algorithmHS256) print(token) print(jwt.decode(token, secret123, algorithms[HS256]))能正常编码再解码说明密钥和算法匹配。这个思路在靶标挂了、网络不通的时候特别管用。6.2 算法混淆与none攻击的联动思路弱密钥爆破不是JWT唯一的突破口实际测试时我习惯把算法层的问题一起测掉因为很多靶标会把多个漏洞点叠在一起。第一个是alg: none。把Header里的alg改成nonePayload随意构造第三段签名直接留空某些服务端库在版本较老或配置不当的时候会照单全收。用jwt_tool一句话就能测python3 jwt_tool.py JWT -X a第二个是RS256转HS256混淆。如果应用正常用RS256私钥签名、公钥验证但验证代码里没有限定algorithms白名单你可以把Header改成HS256然后用泄露的公钥内容当作HMAC密钥对新的Header.Payload签名。服务端如果用公钥去当HS256的密钥验证你会发现它一样能验过。这个手法的关键是要能拿到公钥通常在应用配置、API文档或GitHub仓库里能找到。测试时我会按这个顺序来先摸算法再试none再试算法混淆最后才是弱密钥爆破。弱密钥爆破放在最后是因为它耗时相对长而前两个手法如果成立秒级就能出结果。另外收个尾爆破完拿到密钥不代表完事我习惯顺手验证密钥是否被多处重用——有些开发会把JWT密钥和API密钥、数据库密码用同一个字符串这种重用一旦被攻破影响面会成倍放大。当然这需要结合目标环境的授权范围来评估别越界。7. 安全加固怎么让这类攻击失效作为安全测试人员拿到结果之后更重要的是告诉开发者怎么修。针对JWT弱密钥链路修复建议按优先级排列。第一密钥强度是根本。HS256的密钥必须使用随机生成的高熵值长度至少32字节推荐用下面这种方式生成并存入环境变量或密钥管理服务openssl rand -base64 48生成的类似8HwQmK3R...这种字符串别写进代码仓库别出现在日志里更别贴到配置中心明文存储。同时要做密钥轮换机制一旦怀疑泄露能快速吊销并重新签发。第二校验端硬约束算法白名单。PyJWT里jwt.decode(token, key, algorithms[HS256])明确指定只允许HS256别用algorithms不传或使用不受控的列表。同理如果是RS256体系要拒绝alg被篡改成HS256或none。这是拦截算法混淆最直接的手段。第三Payload里别放权限决策数据。角色、权限、用户ID这类信息应该存服务端会话里JWT里可以带但服务端必须二次校验不能直接信任客户端声明的角色。更稳妥的做法是把JWT当作无状态票据使用权限判断落到数据库或权限服务上。第四过期时间与刷新策略。exp设置短一些建议15分钟到1小时。刷新Token用独立的密钥体系别和访问Token复用同一个HS256密钥。最后一个建议是给开发团队做一次内部演练把本文这套流程在测试环境里完整跑一遍让每个人都亲眼看到“secret123”是怎么在三分钟内变成管理员令牌的。很多时候安全建议讲一百遍不如现场演示一遍来得有效。我自己做过类似的内部培训效果远好于PPT宣讲。JWT本身不是坏技术坏的是实现时的随意。密钥是签名的信任根信任根不牢整个认证体系都是沙上堡垒。这篇复盘里所有手法和工具最终目的都是为了让这个道理变得可感知、可验证。你在自己的靶标里跑通之后不妨再试试更长的随机密钥然后观察爆破工具失效的过程——那种“打不进去”的感觉才是安全该有的常态。