资讯详情

PyJWTInspector:图形化JWT检测与弱密钥签名校验工具

📅 2026/11/4 4:19:20 | 华诺云谱 👁 阅读
PyJWTInspector:图形化JWT检测与弱密钥签名校验工具
做安全测试的同学大概都遇到过这种场面抓包工具里躺着一串eyJhbGciOi...开头的字符串你隐约觉得它有戏但要真去验证就得开终端、装依赖、手写解码脚本、再拼一段发包代码来回折腾半小时热情全耗在环境上。我这次要聊的PyJWTInspector就是为这个场景写的一个图形化 JWT 检测与验证工具——把 JWT 的结构解析、算法识别、签名校验、声明篡改、弱密钥字典比对、请求回放这些零散动作收进一个窗口里点几下就能跑完一轮。它不替代 Burp 这类抓包主力定位更像是一把随手可用的瑞士军刀拿到 Token 之后先在这里把它的骨骼看清把能验证的点快速验证一遍再决定要不要深挖。适合做 Web 渗透测试、接口安全评估的从业者也适合正在学 JWT 机制、想在授权靶场里动手的新人。下面我把自己从 0 到 1 搭这个工具的完整思路、关键代码、踩过的坑一条条摊开讲。1. 为什么值得专门做一个 JWT 检测台1.1 JWT 的三段式结构藏着多少信任假设JWT 的物理结构极简单header.payload.signature三段之间用点号隔开前两段是 Base64URL 编码的 JSON第三段是签名。简单到很多人第一眼会觉得这不就是个带签名的 cookie 吗。恰恰是这种简单掩盖了它背后一整套需要开发者自己负责的信任假设。服务端拿到 Token 之后要做的判断至少有四件头部的alg字段声明的算法我到底认不认签名用的密钥我是不是正确加载了kid、jku、x5u这类头部参数我要不要照单全收载荷里的exp、nbf、iss、aud我有没有真的逐条校验。这四件事里任意一件处理得糙就可能出问题。举个最常见的很多库在调用解码函数时如果开发者没有显式指定algorithms参数早期版本的某些实现会把alg: none当成合法值放行签名段直接留空即可通过。这不是库的恶意设计而是向后兼容 宽松默认留下的历史包袱。再比如kid字段设计初衷是让服务端在多密钥场景下快速定位该用哪把钥匙可一旦被拼进 SQL 查询或者文件路径注入面就打开了。所以做这个工具的第一个动机很朴素JWT 的问题点高度集中在头部字段的信任边界上而这些字段人眼看不见、需要解码才能读。一个能把三段拆开、把每个字段摊在桌面上、并且顺手给出这个字段存在哪些常见处理缺陷提示的界面能省掉大量重复劳动。1.2 命令行时代的三个真实痛点我最早是纯脚本流一个decode.py负责拆包一个verify.py负责试签名一个replay.py负责把改过的 Token 塞回请求。跑了大半年痛点越来越清晰。第一是上下文切换成本高。抓包工具里复制 Token切终端粘贴跑脚本看输出再切回抓包工具改请求。一趟下来十几秒但一天几十趟就是十几分钟更麻烦的是注意力被打断容易漏细节。第二是中间状态无法留存。脚本跑完就没了改过的 payload、试过的密钥、返回的状态码全靠脑子记。等要写报告的时候得重新跑一遍。第三是参数组合爆炸。光算法层面就有none、HS256 与 RS256 混淆、密钥字典比对、签名段置空这么几种玩法每次手改脚本里的变量改错一个字符就得排查半天。图形化界面解决的正是这三点状态留在界面上不用记参数用控件切不用改代码操作路径短到几乎零切换。这也是我选择 PyQt5 而不是写个网页版的原因——本地跑、零部署、直接读写本地文件处理 Token 这种敏感数据时不用担心往哪儿传。1.3 工具定位与必须守住的边界这里必须把话说在前面。这个工具的名字里带利用两个字但它的实际定位是检测与验证。它做的是把 Token 拆开给你看、用本地字典去比对签名密钥、把改动后的 Token 发到你在授权范围内指定的目标上、把响应记录下来。它不做的不扫描、不遍历、不对任何未授权目标发起请求。我在代码里做了硬性约束所有发包模块必须由用户手动填写目标地址且启动时会弹一次确认框勾选我确认已在授权范围内操作才能继续。这个设计不是走过场它决定了工具的性质——你是拿着它去测自己的系统、公司内部靶场、或者明确授权的评估项目而不是别的。凡是给不出授权凭据的目标一律不碰这是一条没有讨论余地的线。同样地工具默认不保存任何请求响应原文到磁盘日志只留在内存里导出报告时需要手动点。Token 本身往往就是有效凭据落盘即风险。2. 整体架构与技术选型2.1 四层拆分解析、载荷、签名、回放工具内部我按职责切成四层每层之间用纯 Python 的数据结构传递不互相依赖 GUI。解析层负责把字符串切成三段做 Base64URL 补齐、UTF-8 解码、JSON 解析产出header和payload两个 dict外加原始签名字节。这一层必须零外部依赖只用标准库因为它是所有后续逻辑的基础不能因为某个第三方库的版本变动就崩掉。载荷层负责声明校验exp是否过期、nbf是否还没生效、iat是否在未来、iss/aud/sub的值长什么样、有没有重复键。这一层还要识别一些典型特征比如载荷里是否含明文密码、是否含内部 IP、是否有超长过期时间。签名层负责算法识别和本地验签。它要判断头部声明的算法属于哪一族HMAC、RSA、ECDSA然后走对应分支HMAC 走字典比对RSA/ECDSA 走公钥加载后的验签同时标记出声明算法与实际使用的密钥类型不匹配这种可疑情况。回放层负责组装请求、发送、记录响应。这一层被刻意做得最薄——只有requests一个依赖超时、重定向、代理都从界面参数读绝不硬编码。四层分开的好处是测试友好。解析层的每个函数都能单独跑单测签名层的字典比对能脱离界面做基准测试回放层可以用本地起的空壳服务验证。2.2 为什么是 PyQt5 而不是 Web 前端三个理由。一是部署成本。PyInstaller 打包成单文件之后对方拿到就能双击运行不需要装 Python、不需要配环境。做安全工具的人都知道工具能不能被同事用起来往往就差在这一个门槛上。二是本地数据不出机。Token 本质是凭据走浏览器就意味着可能被扩展、被缓存、被同步而本地 GUI 进程内的数据流动是完全可控的。三是原生控件够用。这个工具的交互复杂度不高两个文本框、一个树形控件、一个表格、几个按钮PyQt5 的QTreeWidget和QTableWidget直接对上需求不值得为它引入一套前端工程。至于 PyQt5 和 PySide6 的选择我最后用了 PyQt5原因是打包体积和兼容性在若干 Python 版本上更稳而且社区里现成的打包踩坑记录更多出问题好搜。如果你从零开始、没有历史包袱PySide6 的 LGPL 协议约束更宽松也完全可行代码迁移量很小主要是把PyQt5换成PySide6信号槽写法基本一致。2.3 目录结构与依赖清单pyjwtinspector/ ├── main.py # 入口创建 QApplication ├── core/ │ ├── parser.py # 解析层 │ ├── claims.py # 载荷层 │ ├── signer.py # 签名层 │ └── replayer.py # 回放层 ├── ui/ │ ├── main_window.py # 主窗口 │ ├── panels.py # 左中右三栏 │ └── workers.py # QThread 工作线程 ├── data/ │ └── wordlist.txt # 默认弱密钥字典 └── resources/ └── app.ico依赖只有三个PyQt5、requests、cryptography。注意我没有用官方的PyJWT库来做验签。原因后面会细说简单讲是我需要的是可控的、能暴露中间过程的验签而不是一个黑盒的decode调用。cryptography用来加载 RSA/EC 公钥并做验签这部分自己写反而更透明。pip install PyQt5 requests cryptography提示cryptography在部分平台需要编译轮子如果pip装不上先升级pip再试或者直接装预编译的 wheel别在编译上耗时间。3. 核心模块的关键实现细节3.1 手写 JWT 解析器Base64URL 的三个坑标准库的base64.urlsafe_b64decode有个经典问题它要求输入长度是 4 的倍数而 JWT 各段是去掉了尾部填充等号的。所以你直接拿 Token 的第二段扔进去大概率报Incorrect padding。正确的处理是先补齐import base64 import json def b64url_decode(seg: str) - bytes: # JWT 段去掉了 填充先补回来 pad 4 - len(seg) % 4 if pad ! 4: seg * pad return base64.urlsafe_b64decode(seg) def parse_jwt(token: str): token token.strip() # 兼容 Bearer 前缀实际抓包经常直接复制整行 if token.lower().startswith(bearer ): token token[7:] parts token.split(.) if len(parts) ! 3: raise ValueError(f段数异常期望 3 段实际 {len(parts)} 段) header json.loads(b64url_decode(parts[0])) payload json.loads(b64url_decode(parts[1])) return header, payload, parts[2]这段看着简单但三个坑我都踩过。坑一是段数判断。有些场景下 Token 只有两段签名段为空即none算法此时split出来是 2 段如果严格断言 3 段就会误判成非法 Token。正确做法是允许 2 段把签名段视为空字符串然后在签名层单独标记。坑二是 UTF-8 解码。JSON 里含中文时base64解出来是 bytes必须显式.decode(utf-8)json.loads对 bytes 的支持在不同 Python 版本上行为不一致。坑三是 URL 安全的变体。极少数实现会把和/混进来虽然不合规范但真实环境里确实存在所以解码时最好加一层兜底遇到异常先做一次字符替换再重试。解析结果我不用纯文本展示而是塞进QTreeWidget。理由很实在JWT 头部和载荷都是嵌套 JSONsub里塞个对象、scope里塞个数组纯文本格式化之后层级关系全靠缩进眼睛累。树形控件可以逐层展开还能对每个节点加个可疑标记——比如alg节点标红、exp节点标黄一眼就能扫到重点。3.2 算法识别与混淆检测的判定逻辑算法这块是整个工具的判断核心。我做了两层检查。第一层是声明值是否在合理集合内。把header[alg]取出来跟一个白名单比对HS256/384/512、RS256/384/512、ES256/384、PS256/384/512。不在集合里的直接标为非标准算法。特别地none、None、NONE、空字符串这几种变体全部归到空算法分类下因为服务端如果对大小写做了宽松匹配任意一种写法都可能被放行。第二层是声明算法与实际签名长度是否自洽。这是我觉得最有价值的启发式判断。HMAC-SHA256 的签名是 32 字节Base64URL 编码之后大约是 43 个字符RSA-2048 的签名是 256 字节编码后约 342 个字符ECDSA P-256 的签名是 64 字节约 86 个字符。这三者的长度差异非常显著。于是逻辑就出来了如果头部声明HS256但签名段长度接近 342那几乎可以断定服务端实际在用 RSA 验签头部声明是假的——这正是算法混淆场景的典型特征。反过来如果声明RS256但签名只有 43 个字符那服务端可能实际在用 HMAC此时用公钥当 HMAC 密钥去比对就有意义。LEN_HINT { HS256: (43, 44), HS384: (64, 64), HS512: (86, 86), RS256: (342, 344), RS384: (342, 344), RS512: (342, 344), ES256: (86, 88), ES384: (128, 130), } def check_alg_consistency(header, sig_seg): alg header.get(alg, ) expect LEN_HINT.get(alg) if not expect: return unknown, None low, high expect actual len(sig_seg) if low actual high: return consistent, actual # 长度不符给出最可能的实际算法族 for name, (l, h) in LEN_HINT.items(): if l actual h: return mismatch, name return mismatch, unknown需要说清楚的是这是启发式判断不是定论。密钥长度可以自定义RSA 也可以是 1024 或 4096 位长度区间会有偏移。所以界面上我给的措辞是疑似算法族而不是确定是。真正的确认要靠发回给服务端看响应——这一点后面第 5 节会讲。3.3 字典比对引擎为什么用本地计算而不是调库弱密钥比对这块市面上常见的做法是循环调用jwt.decode(token, key, algorithms[alg])捕获异常判断是否命中。能跑但有两个问题一是异常驱动每次失败都要走一遍完整的异常构造几十万条字典跑下来开销不小二是黑盒你没法知道差一点命中的情况。我改成直接用hmac做本地计算import hmac import hashlib ALG_MAP { HS256: hashlib.sha256, HS384: hashlib.sha384, HS512: hashlib.sha512, } def try_hmac_keys(token: str, wordlist, alg: str, progress_cbNone): parts token.split(.) signing_input f{parts[0]}.{parts[1]}.encode() target b64url_decode(parts[2]) digestmod ALG_MAP.get(alg) if digestmod is None: return None total len(wordlist) for i, key in enumerate(wordlist): k key.encode() sig hmac.new(k, signing_input, digestmod).digest() if hmac.compare_digest(sig, target): return key if progress_cb and i % 500 0: progress_cb(i, total) return None几个关键点值得展开。用hmac.compare_digest而不是虽然这里不涉及网络时序侧信道但保持恒定时间比较是习惯也避免在某些极端情况下被优化掉。signing_input只取前两段原文而不是重新编码这点极其重要——Base64URL 编码不是唯一的同一份 JSON 换个键顺序编码结果就不同重编码会导致签名对不上。进度回调加在主循环里每 500 次触发一次避免频繁刷新界面反而拖慢速度。性能上实测单核 Python 跑 HMAC-SHA256 大概每秒 30 万到 50 万次取决于密钥长度和机器一个 10 万条的字典几秒钟就能跑完。如果字典更大可以开多进程分片但要注意进程间传 Token 和结果的序列化开销我实测下来 50 万条以内单进程 进度条体验最好再往上才有必要上multiprocessing。关于字典本身工具内置的wordlist.txt只放了约两千条高频弱口令覆盖secretpasswordchangeme公司名年份这类组合。实战里可以换自己的字典界面留了文件选择入口。字典质量远比数量重要——一个针对目标业务定制的五千条字典命中率往往吊打通用百万字典。3.4 声明篡改与重签的数据流设计改 payload 再重签是这个工具用得最多的功能。流程看着直观实现上有几处必须理清。第一步是改什么。界面上 payload 树是可直接编辑的双击某个值改掉回车生效。底层维护一份payload_dict每次编辑触发一次深拷贝更新而不是直接改树节点——树只是视图数据源必须是 dict否则嵌套结构的同步会乱。第二步是怎么改。这里有个小设计工具提供常用篡改模板比如把role从user改成admin、把sub改成另一个已知用户名、把exp往后推一年、删掉aud字段。模板不是让工具替你想而是把你会反复做的那几个动作变成一键省掉手敲。第三步是怎么重签。分三种情况。知道密钥的直接用hmac或cryptography重算签名none算法的签名段留空用私钥签的界面提供私钥文件加载入口cryptography读 PEM 之后走对应签名算法。def resign(token: str, header: dict, payload: dict, algo: str, keyNone): h b64url_encode(json.dumps(header, separators(,, :)).encode()) p b64url_encode(json.dumps(payload, separators(,, :)).encode()) signing_input f{h}.{p}.encode() if algo.lower() none: return f{h}.{p}. if algo.startswith(HS): sig hmac.new(key.encode(), signing_input, ALG_MAP[algo]).digest() else: sig private_key_sign(key, signing_input, algo) return f{h}.{p}.{b64url_encode(sig)}separators(,, :)这个参数别省。默认的json.dumps会在逗号和冒号后加空格编码出来体积变大不说某些严格校验的服务端会因为它跟原始格式不一致而产生意料之外的行为。3.5 回放层的克制设计回放模块我刻意写得笨只支持指定 URL、指定方法、指定头部、指定 Body 模板{{TOKEN}}占位符会被替换成当前编辑好的 Token。不支持批量、不支持并发、不支持自动遍历。理由很明确这个工具的定位是验证单个假设不是扫描。你有一个具体的怀疑比如把 role 改成 admin 能不能过就用它发一次看响应。批量遍历属于另一类工具的职责混进来会让边界模糊。def replay(url, method, headers, body, token, timeout10): h {k: v.replace({{TOKEN}}, token) for k, v in headers.items()} b body.replace({{TOKEN}}, token) if body else None resp requests.request(method, url, headersh, datab, timeouttimeout, allow_redirectsFalse) return { status: resp.status_code, length: len(resp.content), headers: dict(resp.headers), body_preview: resp.text[:2000], }allow_redirectsFalse是刻意的。登录类接口经常用 302 跳转来表示成功如果自动跟随最终拿到的可能是首页 HTML状态码 200长度巨大反而判断不出结果。关掉重定向你看到的才是第一手响应。body_preview截断到 2000 字符也是刻意的防止某个接口返回几 MB 的 JSON 把界面卡死。真要细看日志区有查看完整响应按钮。4. 界面搭建与交互上的取舍4.1 三栏布局信息密度与操作路径的平衡主窗口我用QSplitter切成三栏可拖动调整宽度。左栏是输入区一个多行文本框放 Token下面一排按钮——粘贴、解析、清空。文本框做了实时校验输入合法的三段式结构时边框变绿非法时变红并给出提示。这个即时反馈很重要能挡掉大部分复制的时候多带了个空格或换行的低级问题。中栏是解析结果区上下两个QTreeWidget上面是 header下面是 payload中间夹一条声明校验的摘要行过期时间、算法一致性结论。树形控件的每个键值对是一个两列的 item值可编辑。右栏是操作区用QTabWidget分成四个页签——算法检测、密钥比对、声明篡改、请求回放。每个页签只放跟它相关的控件避免一屏塞满按钮。这个布局是改了三个版本才定下来的。第一版是上下结构Token 在上、结果在下结果区一长就得滚改 payload 的时候看不到 header。第二版是把所有操作塞进一个工具栏按钮十几个找起来费劲。三栏 页签的组合是在一屏能看到关键信息和操作分组清晰之间找到的平衡点。顺带提一句配色。整个界面走浅色底 深灰字只有三处用色解析成功的绿、可疑项的橙、高危项的暗红。颜色只用来传递判断不做装饰。做工具的人都懂界面花哨不解决问题能让人三秒内定位到哪里不对劲才有价值。4.2 QThread 与信号槽别让主线程干重活这是 PyQt 新手最容易翻车的地方。字典比对动辄几秒到几十秒如果直接在按钮的槽函数里跑主线程被占住界面直接假死用户点关闭都没反应。正确做法是把耗时任务搬到QThread里通过信号回传进度和结果from PyQt5.QtCore import QThread, pyqtSignal class CrackWorker(QThread): progress pyqtSignal(int, int) finished_ok pyqtSignal(object) failed pyqtSignal(str) def __init__(self, token, wordlist, alg): super().__init__() self.token, self.wordlist, self.alg token, wordlist, alg def run(self): try: hit try_hmac_keys( self.token, self.wordlist, self.alg, progress_cblambda i, t: self.progress.emit(i, t) ) self.finished_ok.emit(hit) except Exception as e: self.failed.emit(str(e))三个细节。信号必须是类属性写成pyqtSignal而不能在__init__里动态加否则连接不上。run()里绝对不能碰任何 UI 控件只能emit信号UI 更新全部放在主线程的槽函数里做。别忘了在工作线程结束时把按钮重新启用否则用户点了一次之后按钮一直是灰的会以为卡死了。还有一个隐性坑用户点了开始跑到一半想取消。我加了self._stop False标志在字典循环里每次检查stop()方法把它置 True。这个取消做不到立即但能在几十毫秒内响应体验上够用。4.3 日志区与结果表格的分工界面底部我留了一条固定高度的日志区QTextEdit只读带自动滚动。里面记录的是动作流水什么时候解析了什么、用了多长的字典、耗时多少、命中了没有、发包返回了什么状态码。右栏的密钥比对页签里则是一个QTableWidget记录的是候选结果试过的密钥、是否命中、命中时的算法。表格支持导出 CSV方便把一次测试的过程整理进报告。这两者的分工是刻意的。日志是时间线回答我做了什么表格是结果集回答找到了什么。混在一起会让报告整理变得很痛苦。导出按钮我放在了表格上方而不是菜单里因为这是高频动作藏太深等于没有。5. 从拿到 Token 到出结果的完整实操5.1 环境准备与首次运行先把依赖装好然后直接跑源码。git clone 你的仓库地址 cd pyjwtinspector python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt python main.py首次启动会弹一个确认框勾选授权声明之后主窗口才出现。这个流程我坚持保留哪怕它多了一步点击。环境上有几点提醒。Python 版本建议 3.9 及以上3.8 也能跑但cryptography的新版本会挑版本。Linux 下如果 PyQt5 报could not load the Qt platform plugin xcb装一下系统级的libxcb-xinerama0之类的基础库这是老问题了跟工具本身无关。macOS 的 M1/M2 芯片上cryptography和PyQt5都有对应的 arm64 wheel正常pip install就行别去手动编译。5.2 一次完整检测流程记录下面是我在一个自建靶场环境里的实跑记录靶场是本地起的、故意留了几处配置缺陷的示例服务专门用来验证工具。流程按实际操作顺序写。第一步解析。从靶场登录接口拿到 Token粘进左栏点解析。中栏立刻出来两棵树。header 是{alg:HS256,typ:JWT}payload 里有sub、role、iat、exp。摘要行显示签名长度 43 字符与 HS256 一致绿色。第二步看声明。摘要行同时提示exp距今还有 7200 秒正常。role值是user。这一步的收获是确定了攻击面HMAC 算法、有角色字段、有效期合理。第三步算法层试探。切到算法检测页签点构造空算法变体。工具会生成四个变体alg: none 空签名、alg: None、alg: NONE、alg: 。每个变体配一个发送按钮填上靶场接口地址逐个发。结果全部返回 401说明靶场的库正确拒绝了空算法。这一轮排除。第四步算法混淆试探。点构造混淆变体把 header 里的alg改成RS256签名段保留原 HMAC 签名。发出去返回 401。再反向构造声明HS256但拿靶场的 RSA 公钥当 HMAC 密钥去签。这一步需要先在靶场拿到公钥靶场提供了 JWKS 端点填进公钥文件输入框生成 Token 后发送返回 401。说明靶场在验签时对算法做了强绑定。第二轮排除。第五步弱密钥比对。切到密钥比对页签选中内置字典点开始。进度条走到 37% 的时候停住了表格里多了一行命中密钥dev-secret-2023算法 HS256耗时 0.8 秒。这是个典型的开发环境遗留密钥很明显是靶场故意留的。第六步篡改重签。拿到密钥之后回到中栏双击 payload 树里的role改成admin回车。切到声明篡改页签密钥框自动带上了刚才命中的值点重签。新 Token 生成粘回请求发送。返回 200响应体里带出了管理员接口的数据。到这里一个完整的弱密钥导致权限提升链路就验证完了。第七步整理。点表格上方的导出 CSV把密钥比对记录导出日志区全选复制跟 CSV 一起扔进报告草稿。整个过程从拿到 Token 到出结论实测四分钟左右其中大半时间花在读响应上。5.3 打包成单文件分发给同事用的时候python main.py这个要求还是太高。PyInstaller 打个单文件pyinstaller --noconfirm --onefile --windowed \ --name PyJWTInspector \ --icon resources/app.ico \ --add-data data/wordlist.txt:data \ main.py三个注意点。--add-data里的分隔符在 Windows 上是;在 Linux/macOS 上是:写错了打包能成功但运行时找不到字典文件。代码里读字典的路径必须用sys._MEIPASS做兼容因为单文件模式下资源被解压到临时目录import sys, os def resource_path(rel): base getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base, rel)--windowed会隐藏控制台好处是双击不弹黑框坏处是出错时你看不到堆栈。调试阶段先去掉这个参数跑通了再加。打出来的单文件在 Windows 上大约 40 到 60 MB主要体积来自 Qt 的动态库这个没办法除非换更轻的 GUI 框架但那些框架的控件丰富度撑不起这个界面。5.4 报告导出的一点设计CSV 导出看着简单但字段设计上我想了一下时间戳、Token 摘要、算法、密钥候选、是否命中、耗时、目标地址、响应状态。Token 摘要只存前 20 个字符加省略号不存全文——报告可能会流转完整 Token 落进去就是凭据泄露。日志导出用纯文本按时间线排列。这两份东西加起来基本能还原一次检测的完整过程写报告的时候不用凭记忆补细节。6. 常见问题与排查速查6.1 解析与编码类问题症状Incorrect padding报错。九成是没做补齐按 3.1 的b64url_decode处理即可。剩下一成是 Token 本身被截断了检查一下复制的时候是不是少了尾巴。症状UnicodeDecodeError。payload 里含非 ASCII 字符解码时忘了.decode(utf-8)。注意json.loads对 bytes 的处理在不同版本有差异统一先 decode 成 str 最稳。症状解析出来的 JSON 键顺序跟原文不一样。正常现象json.loads到 dict 之后顺序由插入顺序决定跟原文无关。但如果你要重签必须用原始的前两段字符串做签名输入不能重新序列化——这一点在第 3.3 节强调过是踩过最疼的坑。症状Token 明明是有效的但解析报 JSON 错误。大概率是复制的时候混进了换行符或者全角字符。工具里加了strip()和不可见字符过滤仍然报错的话用十六进制看一眼原始字节。6.2 签名与算法类问题症状字典跑完没命中但你知道密钥很短。先确认算法对不对。头部声明 HS256但你用 HS512 去算永远对不上。其次确认字典的编码——如果字典文件是 GBK 存的读进来不转 UTF-8算出来的哈希完全是另一回事。症状重签之后服务端返回 401但密钥确定是对的。检查三处一是separators参数有没有加多余的空格会改变签名输入二是 header 里是不是被工具自动加了字段比如typ有些服务端会严格比对 header 内容三是 payload 里的时间字段——把exp改成过去的时间服务端校验过期就拒了。症状RSA 签名一直失败。检查私钥格式。cryptography读 PEM 时-----BEGIN RSA PRIVATE KEY-----和-----BEGIN PRIVATE KEY-----是两种不同格式前者是 PKCS#1后者是 PKCS#8加载函数不一样。用load_pem_private_key通常能兼容但加密的私钥需要提供密码。症状算法一致性判断说疑似 RS256但服务端其实就是 HS256。这是启发式的固有局限签名长度只是线索不是证据。以实际发包结果为准别把工具的判断当结论。6.3 环境与打包类问题症状打包后运行时提示找不到wordlist.txt。--add-data的路径没配对或者代码里没用sys._MEIPASS。按 5.3 的resource_path改。症状打包体积特别大。PyQt5 会把整个 Qt 打进去可以试试--exclude-module掉用不到的模块比如PyQt5.QtWebEngine能省不少。别指望压到 10 MB 以内。症状Windows 上报毒。PyInstaller 打出来的单文件被误报是常见现象尤其是安全类工具。解决办法一是加签名证书二是改用--onedir模式分发误报率低一些三是把源码一起提供让对方自己构建。症状macOS 上双击提示无法打开因为无法验证开发者。右键打开或者去系统设置里放行。这是 Gatekeeper 的机制跟工具本身无关。6.4 一张速查表现象最可能原因处理方式Incorrect paddingBase64URL 缺填充补齐到 4 的倍数解析出乱码未按 UTF-8 解码显式.decode(utf-8)重签后验签失败重新序列化了前两段用原始字符串做签名输入字典不命中算法或字典编码不匹配核对alg字典转 UTF-8RSA 私钥加载失败PEM 格式或密码问题区分 PKCS#1/PKCS#8提供密码打包后找不到资源路径未适配_MEIPASS用resource_path包装界面卡住无响应耗时任务跑在主线程搬到QThread 信号槽重定向后判断不了结果自动跟随了 302allow_redirectsFalse7. 边界、坑与一点个人体会7.1 工具越顺手越要清楚自己在测什么这一点我想单独拎出来说。图形化带来的最大副作用是操作变得太容易——点几下就发出去了很容易让人忘记每一次点击背后都是一次真实的请求。我给自己定了两条规矩也写进了工具的设计里目标地址必须手填不提供任何形式的自动发现每次发包前日志区打印完整的目标和 Token 摘要让操作留痕。技术上还有个容易忽略的点requests默认会读取环境变量里的代理配置。在某些内网环境里这会导致请求走向意料之外的出口。我在工具里加了显式的proxies{http: None, https: None}默认值需要走代理时手动在界面勾选并填写。这条是纯经验文档里不会写但排查请求明明发了却没有任何响应的时候能救命。7.2 三个我踩过、文档里查不到的坑坑一QTreeWidget的 item 编辑信号会递归触发。我在itemChanged槽里更新底层 dict更新完又要刷新树节点显示结果刷新动作再次触发itemChanged死循环直到栈溢出。解决办法是加一个self._updating标志刷新前置位、刷完复位槽函数开头判断这个标志直接 return。坑二hmac.new的密钥长度没有下限检查。理论上 HMAC 支持任意长度密钥但极短的密钥比如 1 个字符在某些服务端实现里会被拒绝导致你本地算出来能对上、发过去却不通。我在字典里过滤掉了长度小于 4 的候选看似减少了覆盖面实际减少的是无效尝试。坑三打包后requests的证书校验报错。单文件模式下certifi的证书文件路径会指向临时目录某些情况下读不到。稳妥的做法是显式指定verifycertifi.where()或者在界面提供一个跳过证书校验的开关——但后者一定要默认关闭并且在日志里显式打警告避免在真实评估中掩盖掉证书问题。7.3 后续可以往下扩的方向如果你打算基于这个思路继续做有几个方向我觉得值得投入。一是把 JWKS 端点的自动解析做扎实现在只支持手动贴公钥实际上很多系统的公钥是可通过标准端点获取的自动拉取并缓存能省不少事。二是加一个历史对比功能把同一目标多次测试的 Token 结构做 diff能快速看出服务端改了什么配置。三是把字典比对换成可插拔的规则引擎现在只有 HMAC 弱密钥一种规则未来可以挂上kid路径特征匹配、jku协议校验这类纯静态检查不需要发包就能给出风险提示。至于 GUI 本身我暂时不打算换框架。PyQt5 在这个体量下够用、稳定、打包成熟换框架带来的收益远小于迁移成本。工具的价值在于把判断逻辑沉淀下来界面只是壳。最后分享一个我在实际使用里的习惯每次测完一个目标我会把这次的字典命中结果和几个典型 Token 结构脱敏后存进一个本地笔记里。测得多了之后你会发现同一类业务系统、同一套开发框架留下的密钥模式和字段命名往往高度相似。这份笔记比任何通用字典都值钱它记录的是你对一类系统的理解而不是一堆字符串。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑