资讯动态

Kali下JWT弱密钥破解实战:Docker靶标+Burp Suite全流程

发布时间:2026/10/17 12:36:10 来源:尧图企业网站定制
干安全测试这一行碰到JWT鉴权的频率比想象中高。登录成功返回一长串token后续请求带上它就能通过但很多新手对token里的签名到底怎么校验、为什么会有弱密钥问题其实没真正搞明白。Kali环境下调JWT弱密钥破解最省事的路线就是在本地用Docker起一个可控靶标再通过Burp Suite完成抓包、重放和爆破验证。这里记录一下我自己跑通的一套完整流程环境准备、靶标服务编写、Docker部署、代理配置、Burp抓包、弱密钥爆破和最终伪造验证适合安全测试新手也适合需要快速复现问题的工程师。老规矩所有操作都在你自己搭的本地靶标环境里做别拿去对别人的线上系统试。1. 先搞明白JWT弱密钥为什么这么容易被“猜”出来1.1 JWT其实就是三段明文签名只负责“防篡改”JWT长得很唬人拆开看就是三段由点号分隔的字符串分别是Header、Payload和Signature。Header里写着签名算法和类型Payload里放着用户名、角色、过期时间这些业务数据Signature则是服务端用来确认前两段没被动过手脚的校验值。很多人容易忽略一个事实Header和Payload并不是加密的它们只是做了Base64URL编码。把这段token丢进Burp Decoder或者随便一个在线解析工具直接就能看到明文内容。所以永远不要往JWT里塞密码、身份证号这类敏感信息这是最基本的一条。既然前两段是明文那Signature存在的意义就只有一个证明token来自合法的签发方并且内容在传输过程中没有被篡改。服务端收到请求后会用自己保存的密钥重新计算一遍签名再和token里的签名做比对。一致就放行不一致就直接拒绝。这个流程说白了就是“验签”。1.2 弱密钥的命门出在HMAC签名上JWT最常见的一种签名算法是HS256它本质上就是HMAC-SHA256。计算签名时需要一个密钥这个密钥同时用于签名和验签相当于整个认证体系里最核心的那把锁。问题在于很多系统在实现时图省事直接把密钥写成了secret、123456、admin、password这类字符串甚至还有把公司名、项目名当密钥用的。HMAC签名有一个特点只要给我一个合法的JWT我就可以在不知道密钥的情况下做离线枚举。因为JWT的三个部分都摆在那里我只需要把Header和Payload拼接起来再用猜测的密钥去算一遍HMAC看结果是否和Signature一致。整个枚举过程在本地完成不需要向目标服务器发任何请求速度快、隐蔽性强也不会触发服务端的日志告警。可以这么理解服务端像是给保险箱配了一把锁但钥匙胚子太简单攻击者根本不需要撬锁工具只需要拿一批常见胚子挨个试总有一把能插进去。弱密钥问题在HS256场景下被放大就是因为离线枚举的成本实在太低了。1.3 为什么先选Docker部署再上Burp SuiteKali本身是一个集成度很高的渗透测试发行版工具链齐全但直接拿它去测真实业务系统的JWT端点风险不可控。最稳妥的方式是先用Docker在本地起一个靶标服务把漏洞环境固定下来然后再用Burp Suite做手工测试和爆破验证。Docker带来的好处很直接环境干净、起停方便、复现一致。爆破过程中哪怕把服务搞挂重新docker start一下就好完全不影响Kali本体。更重要的是靶标可以故意配置成弱密钥方便反复练习这在真实业务系统里几乎不可能遇到。先有靶标再谈后续的Burp实战排查问题时也不用担心误伤别人的环境。2. 搭建自己的JWT靶标Docker部署全流程2.1 准备Kali环境与DockerKali通常不会默认安装Docker需要手动装一下。打开终端先更新软件源然后安装Docker软件包sudo apt update sudo apt install -y docker.io sudo systemctl enable --now docker安装完成后用docker version看一下客户端和服务端版本确认daemon正常运行。如果当前用户不在docker组里后面执行docker命令时可能需要加sudo或者干脆像我一样在Kali的root环境里操作少不少麻烦。2.2 写一个故意使用弱密钥的JWT服务靶标服务我用Python写选Flask加PyJWT这两个库逻辑简单、依赖少。核心接口有两个/login负责用固定密钥签发JWT/protected负责校验Authorization头里的token并返回用户信息。密钥故意设成123456就是为了后续爆破用的。from flask import Flask, request, jsonify import jwt import datetime app Flask(__name__) SECRET_KEY 123456 app.route(/login, methods[POST]) def login(): data request.get_json() or {} username data.get(username, admin) role admin if username admin else user payload { username: username, role: role, exp: datetime.datetime.utcnow() datetime.timedelta(hours1) } token jwt.encode(payload, SECRET_KEY, algorithmHS256) return jsonify({token: token}) app.route(/protected, methods[GET]) def protected(): auth_header request.headers.get(Authorization, ) token auth_header.replace(Bearer , ) try: data jwt.decode(token, SECRET_KEY, algorithms[HS256]) return jsonify({msg: ok, user: data}) except jwt.ExpiredSignatureError: return jsonify({msg: token expired}), 401 except jwt.InvalidTokenError: return jsonify({msg: invalid token}), 401 if __name__ __main__: app.run(host0.0.0.0, port5000)注意jwt.encode在不同版本的PyJWT里返回类型可能略有差异新版本返回的是字符串可以直接作为token返回。如果你用的是老版本发现返回bytes类型在jsonify之前加一个.decode(utf-8)即可。这个坑不算大但第一次跑的时候容易懵。依赖文件requirements.txt里写两行内容flask pyjwt2.3 用Dockerfile构建并启动靶标有了服务代码和依赖文件接下来写Dockerfile。基础镜像用一个精简的Python 3.11镜像就够JWT靶标本身不涉及数据库和复杂组件镜像越简洁构建越快出问题的概率也越低。FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . EXPOSE 5000 CMD [python, jwt_lab.py]文件都准备好后在同一个目录下执行构建和启动命令docker build -t jwt-lab . docker run -d -p 5000:5000 --name jwt-lab jwt-lab如果本地端口5000被占用可以把-p 5000:5000改成-p 5001:5000之类的映射端口后面Burp测试时记得跟着改。容器启动后用docker ps确认状态如果STATUS列是Up说明靶标已经起来了。2.4 用curl完成第一轮功能验证靶标起来后先别急着开Burp用curl确认接口可用。向/login发一个POST请求拿到token再带token访问/protected看返回是否正常curl -X POST http://127.0.0.1:5000/login \ -H Content-Type: application/json \ -d {username:admin,password:admin}正常情况下会返回一个JSON对象里面的token就是后续用来测试的样本。把这个token复制出来手动加Authorization头去请求受保护接口curl http://127.0.0.1:5000/protected \ -H Authorization: Bearer 你拿到的token返回{msg:ok,user:{...}}就说明验签逻辑正常。到这里整个靶标环境已经处于可测试状态后面所有Burp操作都在这个本地容器上完成。3. Burp Suite实战抓包、重放、爆破三件套3.1 先把浏览器代理和CA证书搞定Burp Suite默认监听本机8080端口Kali里打开Burp后把浏览器的代理设置为127.0.0.1:8080即可。Firefox可以直接在设置里加一个手动代理或者用Burp自带的内置浏览器后者更方便省得配证书。本地靶标走的是HTTP不涉及HTTPS证书但如果你后面想测别的HTTPS站点记得从http://burp下载CA证书并导入浏览器信任列表。代理配置不对的话最典型的现象是浏览器打不开页面或者Burp的HTTP History里一片空白这时先查代理和监听端口。3.2 抓到登录请求拿到JWT样本打开Burp Proxy的Intercept功能也可以不开启拦截直接通过HTTP History观察。浏览器里访问靶标的登录接口或者在Burp里直接构造一个POST请求发给http://127.0.0.1:5000/login这时左侧HTTP History会出现对应的流量记录响应里就是服务端签发的JWT。把这个token从响应里复制出来切到Burp Decoder选择Base64-URL解码分别解Header和Payload。你会看到Header里的alg写的是HS256Payload里有username、role和exp。这一步的重点是确认签名算法和payload结构因为后续爆破的目标就是这段签名的密钥。3.3 先离线破解密钥再回Burp里验证很多人习惯直接拿Burp Intruder对/protected接口发一堆请求把token的Signature部分替换成不同的值。这种做法其实很低效因为Intruder只是替换字符串它不会帮你计算HMAC签名替换后的token基本都会验签失败。要真正爆破HS256的弱密钥正确的姿势是先离线枚举把每个候选密钥算成完整的token再用Burp批量验证。先准备一个弱密钥字典比如123456 secret password admin jwt test qwerty接着用Python脚本批量生成候选token。脚本的思路很简单固定Header和Payload遍历字典里的每个密钥分别计算签名输出完整token到文本文件import jwt import datetime wordlist [ 123456, secret, password, admin, jwt, test, qwerty ] base_payload { username: admin, role: user, exp: datetime.datetime.utcnow() datetime.timedelta(hours1) } with open(candidates.txt, w) as f: for secret in wordlist: token jwt.encode(base_payload, secret, algorithmHS256) f.write(token \n)运行完这个脚本candidates.txt里就是一堆用不同密钥签出来的合法token。现在再回到Burp抓一个GET /protected的请求把Authorization头里的整个JWT用§标记为payload positionAttack Type选SniperPayload Load从candidates.txt读取发送请求后观察响应。因为候选token里的密钥各不相同只有用正确密钥签出来的那个token才能通过服务端验签响应状态码会从401变成200响应体内容也明显不同。我用的是一个小字典实际测试中可以把Kali系统自带的弱密钥字典文件按行提取或者自己从常见密码中整理一份。关键是思路先离线生成候选token再交给Burp做黑盒验证这样既省时又能直观地在Burp里看到命中结果。这一阶段也可以单独用Hashcat等工具跑JWT模式拿到密钥后手动伪造token。不过本文重点是Burp Suite所以不展开讲Hashcat参数预生成候选token的方案在常规靶标测试中完全够用。3.4 找到弱密钥后在Repeater里伪造高权限token假设爆破之后确认密钥是123456接下来就是最有意思的部分在Burp Repeater里伪造一个管理员token。刚才candidates.txt里的payload是role: user现在把payload改成role: admin再用同样的密钥重新签名业务权限就彻底不一样了。如果图省事可以直接用Burp的JWT相关扩展来改签比如在扩展里输入密钥自动重新签名。但我个人的习惯是遇到简单场景就写个两三行的Python脚本改payload再签名输出新tokenimport jwt import datetime payload { username: admin, role: admin, exp: datetime.datetime.utcnow() datetime.timedelta(hours1) } token jwt.encode(payload, 123456, algorithmHS256) print(token)把输出复制到Burp Repeater里替换Authorization头的JWT再次发送GET /protected响应直接返回{msg:ok,user:{username:admin,role:admin}}。到这一步JWT弱密钥从爆破到利用的闭环就走通了。3.5 别忘了检查Intruder的响应判断条件用Intruder批量验证时不要只看状态码。有些服务端实现里token过期和token非法都返回401但响应体的msg字段不一样有些接口对权限不足和校验失败的返回逻辑也各不相同。我一般会在Intruder的Settings里加一条Grep-Match规则匹配成功时的关键字比如msg: ok然后在结果列表里按Match高亮排序命中项一目了然。如果响应体比较长单靠肉眼扫很累也可以加上Response length排序。爆破成功后那条响应长度明显不同于其他401响应的记录通常就是需要重点看的。4. 常见问题与排查技巧实录4.1 Docker容器起不来端口冲突或权限问题最常见的是端口被占用。在Kali里跑其他Web服务时5000端口很容易被某个Python开发服务器占掉。遇到这种情况先docker ps -a看容器状态再用ss -lntp | grep 5000确认占用进程。最简单的解决办法是换宿主端口映射比如docker run -d -p 5001:5000 --name jwt-lab jwt-lab后续Burp请求改成http://127.0.0.1:5001即可。如果是权限问题docker命令报permission denied把当前用户加入docker组再重新登录sudo usermod -aG docker $USER4.2 Intruder批量验证时全部401是不是爆破失败了先别急着怀疑字典。把candidates.txt里的token复制到Repeater里手动发送如果也401最可能的原因是token里的exp过期了。我写的那个靶标里token有效期只有1小时而预生成脚本如果是在测试开始时跑的等到Intruder批量发送时可能已经过期。解决办法是重新生成token或者把exp时间设长一点。另一个容易踩的坑是Intruder的payload position选错了。如果只选中token里的Signature段而不是整个JWT那无论换什么payload都过不了验签因为Header和Payload没变但Signature被替换成了一段不匹配的字符串。正确做法是替换整个JWT。4.3 要不要升级到Turbo Intruder普通Intruder在低并发下对付这种本地靶标够用但如果你打算跑一个几十万词的字典普通Intruder的速度会让人崩溃。Turbo Intruder这类基于Python的扩展可以显著提升发包效率还能自定义爆破逻辑。不过我一般不在这个场景里用它因为离线生成候选token的环节才是性能瓶颈真正发送到Burp验证的只是少量候选没必要用涡轮增压。如果确实要跑大字典离线爆破直接用Hashcat这类工具会快得多。在我的流程里Burp更多承担的是流量观察、请求构造和结果验证的角色而不是纯暴力发包的工具。工具选型没有绝对标准能快速解决问题的就是好方案。4.4 服务端不只是弱密钥算法压制也要注意JWT安全坑不止弱密钥一个。有些实现会在jwt.decode时不指定algorithms导致攻击者把Header里的alg改成none甚至把HS256换成RS256来混淆验签逻辑。虽然本文的靶标是标准的HS256弱密钥但你在真实授权测试中遇到JWT相关问题时一定要结合具体实现去判断漏洞类型。密钥太弱可以爆破算法校验不严可以绕过exp不校验也可以重放这些东西往往是组合出现的。防御侧也一样密钥强度只是基础服务端还必须显式限制允许的算法列表校验exp等标准声明并且避免在token里存放敏感信息。弱密钥爆破流程反过来用就是一次很好的基线检查如果你的业务系统也用了JWT不妨拿着这份字典自测一下看能不能被爆破。5. 这套流程还能怎么延伸5.1 从本地靶标过渡到授权测试清单本地跑通之后这套思路可以整理成一张授权测试小清单先判断目标是否使用JWT再解析token结构确认算法接着观察接口对篡改token的响应然后离线枚举弱密钥最后在Burp里完成伪造验证和权限提升测试。每一步都有对应的Burp操作也都有明确的判断依据非常适合写进Web应用安全测试的执行手册里。5.2 把爆破结果接入自动化验证如果你经常需要查验一批系统的JWT密钥强度可以把第3节里的Python脚本扩展成一个小工具输入一个JWT样本和字典文件自动输出匹配到的密钥再生成高权限token。配合Burp的API甚至可以做到扫描到JWT后自动拉取样本、自动跑字典、自动回注验证。这个方向不复杂但能省下大量重复劳动。我自己在实际操作里有一个习惯不管时间多紧都会先用离线脚本确认密钥再回Burp里做最终验证。这个顺序一旦反过来很容易陷入“换了无数payload、看了一堆401”的低效循环。工具只是手段能够把每个环节拆开、分别验证才是这套流程真正值钱的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑