JWT安全攻防实战:从算法混淆到弱密钥破解的漏洞利用与防御

📅 2026/6/23 0:54:01
JWT安全攻防实战:从算法混淆到弱密钥破解的漏洞利用与防御
1. 项目概述从零开始理解JWT安全攻防的实战价值如果你是一名刚接触网络安全的新手或者是一名正在开发Web应用的后端工程师那么“JWT安全”这个话题你迟早会碰到而且它比你想象的要重要得多。JWT全称JSON Web Token现在几乎成了现代Web应用身份认证的“标配”。从单页应用到微服务架构你都能看到它的身影。它轻量、自包含设计初衷是为了解决分布式系统中的认证问题。但就像任何一把锋利的工具如果使用不当它也可能反过来伤到自己。我见过太多项目开发团队兴冲冲地接入了JWT实现了“无状态”认证却在安全审计时被爆出一堆高危漏洞轻则数据泄露重则整个认证体系被绕过。这篇文章我们不谈枯燥的理论也不做泛泛而谈的科普。我的目标很直接带你从零开始通过一个个真实的、可复现的漏洞场景亲手去“攻击”有缺陷的JWT实现从而真正理解其背后的安全原理。为什么“零基础入门到精通”这个说法在这里成立因为JWT的安全漏洞其利用手法往往不依赖于高深的密码学知识更多是对协议规范的理解偏差、开发者的配置疏忽和逻辑缺陷的利用。我们将从最基础的JWT结构讲起逐步深入到签名绕过、算法混淆、密钥破解、信息泄露等核心漏洞的实战利用。你会发现很多漏洞的复现只需要一个浏览器、一个简单的解码工具或者几行Python脚本。对于网络安全学习者而言JWT安全是一个绝佳的切入点。它涉及Web安全、密码学应用、协议分析等多个维度攻防过程清晰结果反馈即时非常适合用来建立对安全研究的兴趣和信心。而对于开发者这更是一份必不可少的“避坑指南”。了解攻击者如何思考是你写出更安全代码的第一步。接下来我们就直接进入正题拆解这个看似简单的小令牌背后究竟隐藏着多少风险。2. JWT核心机制与常见安全隐患深度解析在动手“攻击”之前我们必须先彻底理解“靶子”的结构和运行机制。JWT本质上是一个经过编码的字符串通常被放在HTTP请求的Authorization: Bearer token头中。这个字符串由三部分组成用点号.分隔Header.Payload.Signature。Header头部通常包含两个字段typ类型固定为JWT和alg签名算法如HS256、RS256、none等。它经过Base64Url编码后形成第一部分。这里就埋下了第一个隐患这个头部是客户端完全可控且可解码的。攻击者可以轻易地看到当前Token使用的算法。Payload负载包含了所谓的“声明”Claims也就是你需要传递的信息比如用户IDsub、过期时间exp、签发者iss等。它同样经过Base64Url编码。需要明确的是编码不等于加密。任何拿到Token的人都可以轻松将其解码并读取其中的全部内容。因此绝对不要在Payload中存放任何敏感信息如密码、信用卡号等。Signature签名是整个JWT安全性的基石。签名的生成方式取决于头部声明的算法alg。对于HMAC类算法如HS256签名公式是HMACSHA256(base64UrlEncode(header) “.” base64UrlEncode(payload), secret_key)。对于RSA类算法如RS256则是使用私钥签名公钥验证。签名的目的是确保Token在传输过程中未被篡改。服务器在收到Token后会使用相同的密钥或公钥按照头部声明的算法重新计算签名并与Token中的签名部分进行比对。如果一致则认为Token可信。理解了这三部分我们就可以系统地梳理JWT的常见攻击面了。几乎所有漏洞都源于对上述机制的误解或错误实现签名验证缺失这是最致命也最低级的错误。服务器收到Token后根本没有验证签名就直接相信了Payload里的内容。这意味着攻击者可以随意修改Payload比如把普通用户改成管理员然后提交给服务器服务器会照单全收。弱密钥问题对于HS256这类对称加密算法签名和验证使用同一个密钥secret_key。如果这个密钥强度不够如secret、password123或者甚至被硬编码在客户端攻击者就可以通过暴力破解或字典攻击获取密钥从而签发任意有效的Token。算法混淆攻击Algorithm Confusion这是JWT安全中最经典、最高频的漏洞之一。其根源在于JWT的验证库需要根据Token头部自带的alg字段来决定使用何种算法进行验证。如果服务器代码逻辑有缺陷攻击者就可以通过篡改alg值诱使服务器使用错误的验证逻辑。例如将alg从RS256非对称改为HS256对称。我们会在实战环节详细拆解这个过程。“none”算法攻击JWT规范允许alg值为none表示无签名。一些早期的库为了兼容性可能会接受这种Token。如果服务器未正确配置或过滤攻击者就可以提交一个将alg设为none的Token并去掉签名部分从而绕过所有签名验证。密钥文件泄露在使用RS256等非对称算法时如果用于签名的私钥文件如.pem文件意外泄露例如通过Git仓库、备份文件、目录遍历漏洞攻击者就可以用其签发任意高权限的Token。信息泄露与篡改由于Payload是明文编码如果其中包含了会话标识、内部ID等可用于后续攻击的信息就可能造成信息泄露。此外如果应用程序逻辑错误地依赖于Payload中某个可被用户预测或修改的字段如jti也可能导致问题。注意一个非常关键的认知是JWT本身不是一种加密机制而是一种签名和验证机制。它的主要作用是防篡改Integrity而非保密性Confidentiality。Payload里的所有信息对持有者都是公开的。3. 实战环境搭建与基础工具链准备“纸上得来终觉浅绝知此事要躬行。” 学习安全尤其是漏洞利用最有效的方法就是亲手操作。我们不依赖任何复杂的商业靶场而是用最轻量的方式快速搭建一个用于学习和测试的本地环境。这里我推荐两种方式使用Docker快速拉起一个存在漏洞的测试应用或者自己用Python Flask/Node.js Express写一个简单的、故意留有漏洞的JWT验证服务。对于零基础的朋友我强烈建议从Docker开始。你不需要关心语言环境依赖一条命令就能获得一个完整的、可交互的测试靶场。网络上有很多开源的学习项目例如https://github.com/ticarpi/jwt_tool项目本身就附带了一些测试用例或者专门针对Web安全训练的靶场镜像。你可以搜索“jwt lab docker”来找到相关资源。使用命令docker pull 靶场镜像名和docker run -p 8080:80 镜像名即可在本地运行。如果你希望更深入地理解后端验证逻辑自己写一个漏洞服务是更好的选择。这里我用Python Flask给出一个极简的、存在严重漏洞的示例请你绝对不要将其用于任何生产环境from flask import Flask, request, jsonify import jwt import base64 import json app Flask(__name__) # 这是一个写死的、强度很弱的密钥这是我们的“靶子”。 SECRET_KEY ‘supersecret’ # 漏洞点1弱密钥 PUBLIC_KEY ‘‘ # 我们暂时留空后续模拟算法混淆时会用到 users { ‘admin‘: ‘adminpass‘, ‘user‘: ‘userpass‘ } app.route(‘/login‘, methods[‘POST‘]) def login(): data request.json username data.get(‘username‘) password data.get(‘password‘) if username in users and users[username] password: # 使用HS256算法生成Token payload {‘user‘: username, ‘role‘: ‘admin‘ if username ‘admin‘ else ‘user‘} token jwt.encode(payload, SECRET_KEY, algorithm‘HS256‘) return jsonify({‘token‘: token}) else: return jsonify({‘error‘: ‘Invalid credentials‘}), 401 app.route(‘/admin‘, methods[‘GET‘]) def admin(): auth_header request.headers.get(‘Authorization‘) if not auth_header or not auth_header.startswith(‘Bearer ‘): return jsonify({‘error‘: ‘Token missing‘}), 401 token auth_header.split(‘ ‘)[1] try: # 漏洞点2验证逻辑依赖于Token头部自带的alg未强制指定预期算法 decoded jwt.decode(token, SECRET_KEY, algorithms[‘HS256‘, ‘RS256‘, ‘none‘]) # 允许了多种算法 if decoded.get(‘role‘) ‘admin‘: return jsonify({‘message‘: f‘Welcome admin {decoded[“user“]}!‘}) else: return jsonify({‘error‘: ‘Insufficient privileges‘}), 403 except jwt.InvalidTokenError as e: return jsonify({‘error‘: ‘Invalid token‘}), 401 if __name__ ‘__main__‘: app.run(debugTrue) # 漏洞点3生产环境切勿开启debug模式这个简单的应用有两个端点/login用于登录获取Token/admin用于验证Token并访问管理员功能。它已经故意埋下了几个漏洞弱密钥、验证时允许的算法列表过于宽泛包含了RS256和none。接下来是攻击者工具链也就是我们需要的“武器”浏览器开发者工具主要用于拦截和修改HTTP请求这是测试Web接口的必备工具。Chrome或Firefox的F12网络标签Network就足够了。JWT解码/调试网站例如jwt.io。这是一个非常直观的在线工具你可以将Token粘贴进去它自动解码Header和Payload并允许你编辑它们实时观察编码后的变化。但切记绝对不要在任何在线工具中处理真实的、生产环境的敏感Token。命令行工具jwt-tool这是一个功能强大的Python工具专门用于JWT安全测试。它可以暴力破解弱密钥、测试算法混淆、扫描常见漏洞等。安装很简单pip install jwt-tool。之后在终端使用jwt-tool your_token即可开始分析。Pythonpyjwt库用于编写自定义的攻击和验证脚本。pip install pyjwt哈希破解工具hashcat或john当我们需要对捕获的HS256 Token进行密钥暴力破解时这些工具效率远高于自己写的脚本。准备好环境和工具后我们的“黑客”实验室就搭建完毕了。接下来我们将逐一击破那些常见的漏洞。4. 核心漏洞利用手法实战演练现在我们进入最核心的实战环节。我将带领你像攻击者一样思考利用我们刚才搭建的漏洞环境和工具逐个验证并利用这些JWT安全漏洞。4.1 场景一签名验证缺失与Payload篡改这是最理想的情况对攻击者而言。我们首先通过正常登录/login接口获取一个普通用户user的Token。假设得到的Token是eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsInJvbGUiOiJ1c2VyIn0.xxxxxx签名部分用x代替。我们使用jwt.io或简单的Python脚本将其解码import base64 import json token “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsInJvbGUiOiJ1c2VyIn0.xxxxxx“ parts token.split(‘.‘) header json.loads(base64.urlsafe_b64decode(parts[0] ‘‘).decode()) payload json.loads(base64.urlsafe_b64decode(parts[1] ‘‘).decode()) print(“Header:“, header) print(“Payload:“, payload)输出会显示Payload中包含{“user“: “user“, “role“: “user“}。攻击操作如果服务器不验证签名我们直接修改Payload即可。将“role“: “user“改为“role“: “admin“。然后我们需要将修改后的Header和Payload重新进行Base64Url编码。注意因为签名验证被绕过我们不需要计算新的签名甚至可以直接把原签名部分去掉或者留空或者随便填一串字符。使用Python生成新的Tokenimport base64 import json new_header {“alg“: “HS256“, “typ“: “JWT“} new_payload {“user“: “user“, “role“: “admin“} # 篡改关键字段 # Base64Url编码 encoded_header base64.urlsafe_b64encode(json.dumps(new_header).encode()).decode().rstrip(‘‘) encoded_payload base64.urlsafe_b64encode(json.dumps(new_payload).encode()).decode().rstrip(‘‘) # 拼接成一个无签名或假签名的Token forged_token f“{encoded_header}.{encoded_payload}.“ # 签名部分为空 # 或者 forged_token f“{encoded_header}.{encoded_payload}.anything“ print(“伪造的Token:“, forged_token)将这个forged_token放入Authorization: Bearer头中发送给/admin端点。如果漏洞存在服务器将直接解码Payload看到role是admin从而授予管理员权限。实操心得在实际测试中签名完全缺失的情况比较少见。更常见的是“签名验证逻辑可被绕过”。例如某些库的验证函数在签名错误时返回的异常类型可能被全局异常处理捕获并错误地认为是“Token格式错误”而非“签名无效”从而意外放行。测试时除了直接删除签名也可以尝试提交一个格式正确但签名错误的Token观察服务器响应差异。4.2 场景二弱密钥暴力破解这次我们假设服务器进行了签名验证但使用的是弱密钥。我们同样先获取一个合法Token。攻击的目标是破解出这个密钥SECRET_KEY有了密钥我们就能为任何Payload生成合法的签名。使用jwt-tool进行破解 在终端中运行jwt-tool eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsInJvbGUiOiJ1c2VyIn0.xxxxxx -C -d /path/to/wordlist.txt参数解释-C表示破解Crack-d指定字典文件。jwt-tool会自动尝试字典中的每一个词作为密钥去验证Token的签名。一旦验证通过就会输出密钥。字典的选择至关重要。你可以从网上下载常见的弱口令字典如rockyou.txt也可以根据目标情况自定义。在我们的示例中密钥是supersecret这属于极其常见的弱密码很容易被破解。手动验证与利用一旦获取密钥supersecret我们就可以使用pyjwt库签发任意Tokenimport jwt secret ‘supersecret‘ malicious_payload {“user“: “attacker“, “role“: “admin“, “exp“: 9999999999} # 甚至可以设置一个遥远的过期时间 new_valid_token jwt.encode(malicious_payload, secret, algorithm‘HS256‘) print(“新签发的管理员Token:“, new_valid_token)这个新Token拥有完全合法的签名服务器将无条件信任。注意事项暴力破解的成功率取决于密钥强度和字典质量。对于HS256密钥长度建议至少32字节的随机字符串。开发中绝对要避免使用字典单词、短字符串、或与项目相关的简单词汇如项目名123作为密钥。密钥应作为高度机密的配置项如环境变量管理而非写在代码里。4.3 场景三算法混淆攻击Algorithm Confusion—— 重头戏这是JWT安全中最精妙也最危险的漏洞之一。我们回顾一下漏洞代码jwt.decode(token, SECRET_KEY, algorithms[‘HS256‘, ‘RS256‘, ‘none‘])。注意第二个参数是SECRET_KEY一个字符串密钥而允许的算法列表中包含了RS256一种非对称算法需要公钥/私钥对。攻击原理正常流程服务器使用HS256和SECRET_KEY签发Token。验证时库函数看到Token头部的alg是HS256就会用SECRET_KEY作为密钥去验证签名。混淆攻击攻击者伪造一个Token将其头部的alg改为RS256。但签名部分并不是用真正的RSA私钥签名的而是用SECRET_KEY作为“RSA公钥”通过HS256的签名算法计算出来的。关键漏洞当验证库收到这个Token时它看到algRS256就会尝试使用提供的密钥也就是代码中的SECRET_KEY作为RSA公钥去验证签名。在有些库的实现中如果提供的密钥是一个字符串它可能会被直接当作PEM格式的公钥材料去解析。而用HS256算法对同一段数据header.payload进行签名得到的结果与用SECRET_KEY这个字符串当作“公钥”去进行RS256验证在某些条件下可能会被错误地验证通过。实战步骤首先我们需要获取用于签名的SECRET_KEY。在这个例子中我们假设已经通过其他方式如信息泄露、弱密钥破解获得了它即supersecret。使用jwt-tool可以一键完成算法混淆攻击的Token生成jwt-tool eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsInJvbGUiOiJ1c2VyIn0.xxxxxx -X k -pk “supersecret“ -pc “role“ -pv “admin“参数解释-X k指定进行“密钥混淆”攻击-X 代表exploitk代表key confusion。-pk “supersecret“指定我们已知的对称密钥即SECRET_KEY。-pc “role“ -pv “admin“指定要修改的Payload字段及其新值。工具会输出一个伪造的Token其alg被改为RS256或PS256等签名是基于supersecret用HS256方式生成的。这个Token提交给我们的漏洞服务器后服务器会用SECRET_KEY字符串当作RSA公钥去验证RS256签名从而导致验证逻辑混乱可能误判签名有效。为什么这会成功根源在于验证库的API使用错误。安全的做法是在验证时必须明确指定预期使用的算法而不是接受一个列表。对于使用非对称算法的场景应该提供公钥而非一个通用的密钥字符串进行验证。正确的代码应该类似于# 正确做法明确指定算法并使用正确的密钥类型 try: # 如果是HS256提供密钥字符串 decoded jwt.decode(token, SECRET_KEY, algorithms[‘HS256‘]) except jwt.InvalidTokenError: # 如果是RS256提供公钥字符串或路径 with open(‘public.pem‘, ‘r‘) as f: public_key f.read() decoded jwt.decode(token, public_key, algorithms[‘RS256‘])而漏洞代码将对称密钥用于非对称算法的验证上下文造成了逻辑混淆。4.4 场景四“none”算法攻击与Kid参数注入“none”算法攻击相对简单。如果服务器配置的允许算法列表中包含了none攻击者可以直接构造一个alg为none的Token并省略签名部分即最后一部分为空。JWT格式是header.payload.注意最后有一个点号。使用jwt-tool可以快速生成jwt-tool original_token -X a -pc “role“ -pv “admin“参数-X a即代表进行algnone攻击。Kid参数注入是另一个重要攻击面。kidKey ID是JWT头部的一个可选参数用于指示使用哪个密钥来验证签名。例如在一个多租户系统中不同的租户可能使用不同的密钥服务器根据kid来查找对应的密钥。如果kid参数未经过滤直接用于拼接文件路径或数据库查询就可能造成严重漏洞。路径遍历如果kid被直接用于拼接文件路径如key_file “/keys/“ kid “.pem“攻击者可以设置kid“../../../etc/passwd“可能导致服务器读取敏感文件。在验证签名时服务器会用这个敏感文件的内容作为密钥来验证如果攻击者能预测或控制这个文件的部分内容如/dev/null内容为空/dev/zero内容为无数0就可能构造出有效的签名。SQL注入如果kid被直接拼接到SQL语句中查询密钥则可能造成SQL注入进而泄露密钥或其他数据。SSRF服务器端请求伪造如果kid是一个URL服务器可能会去这个URL获取密钥。攻击者可以将其指向自己控制的服务器从而可能触发服务器向内部网络发起请求探测内网服务。测试这类漏洞需要仔细检查JWT头部尝试修改kid值为各种Payload路径遍历Payload、SQL注入Payload、URL等并观察服务器响应的时间差异、错误信息变化或者直接尝试触发敏感行为。5. 漏洞防御策略与安全开发实践经历了前面的攻击演练我们现在站在防御者的角度来看看如何构建一个坚固的JWT体系。安全是一个过程而不是一个特性它需要贯穿于设计、开发、部署的整个生命周期。5.1 安全的JWT验证逻辑实现这是最根本的一环。无论使用哪种语言和库都必须遵循以下铁律永远验证签名这是JWT安全的底线。任何未经签名验证的Token都必须立即拒绝。显式指定允许的算法在验证时绝对不要使用通配符如[“*“]或过于宽松的算法列表。必须根据你的签发逻辑明确指定一个或几个预期的算法。最佳实践如果你只用HS256签发那么验证时只允许HS256algorithms[“HS256“]。如果你使用RS256验证时只允许RS256并且提供正确的公钥而不是一个通用的密钥字符串。彻底禁用none算法确保你的JWT库配置或代码逻辑中明确排除none算法。大多数现代库默认已禁用但检查一遍总没错。严格校验声明Claims签名有效不代表Token就可以用了。必须校验关键声明exp过期时间确保Token没有过期。nbfNot Before确保Token已经生效。iss签发者确保Token来自可信的颁发者。aud受众确保Token是发给本服务的。自定义声明如role、user_id等必须在验证签名后在业务逻辑层进行二次校验确保用户有权执行当前操作。不要盲目信任Payload里的任何数据。安全的密钥管理对称密钥HS256等必须使用足够长建议32字节以上的密码学安全随机字符串。通过安全的密钥管理服务KMS或环境变量来存储严禁硬编码在代码或客户端。非对称密钥对RS256等私钥必须被极其严格地保护最好存放在硬件安全模块HSM中。公钥可以提供给验证方。定期轮换密钥并维护一个有效的kid列表。5.2 针对算法混淆攻击的专项加固算法混淆攻击之所以能成功核心原因是对称密钥和非对称公钥在代码中被以相同的方式一个字符串参数传递给了验证函数。加固方法如下密钥类型分离在代码层面将对称密钥和非对称公钥用不同的变量、不同的配置项来存储。验证时根据业务场景选择正确的密钥和算法。使用库的高级API许多JWT库提供了更安全的API。例如在Python的pyjwt中可以使用jwt.algorithms.get_default_algorithms()并明确注册所需的算法和对应的密钥/密钥加载器避免混淆。依赖库版本更新及时更新JWT库。社区在发现这类混淆漏洞后通常会在新版本中加强验证逻辑。例如一些库现在会检查如果使用非对称算法验证提供的密钥材料必须“看起来像”一个非对称密钥如PEM格式的头部尾部标识否则会直接报错。5.3 生产环境部署与监控建议设置合理的Token有效期使用较短的过期时间exp比如15分钟到1小时。结合刷新令牌Refresh Token机制来获取新的访问令牌减少Token泄露后的风险窗口。实现令牌吊销机制虽然JWT本身是无状态的但在某些敏感操作如用户登出、密码修改后你仍然需要有能力立即使特定Token失效。这可以通过维护一个短小的“黑名单”如基于Redis的Token标识缓存来实现虽然会引入一点状态但对安全性是必要的补充。安全的传输与存储传输始终使用HTTPS。不要将Token放在URL中可能被日志记录应放在HTTP Authorization头或Post Body中。客户端存储在Web前端使用HttpOnly、Secure、SameSite属性的Cookie来存储可以有效防御XSS攻击窃取Token。如果使用localStorage或sessionStorage必须确保你的网站完全没有XSS漏洞。输入验证与过滤对JWT头部中的所有参数如kid、jku、x5u进行严格的验证和过滤。特别是kid如果使用必须将其值限定在白名单内防止路径遍历、SQL注入和SSRF攻击。全面的日志记录与监控记录所有JWT验证失败的事件无效签名、过期、算法不匹配等并设置告警。异常的验证失败模式如大量algnone的请求可能是攻击探测的信号。监控Token的使用频率和来源IP发现异常行为。6. 从漏洞利用到安全思维的转变通过这一系列从搭建环境、利用漏洞到部署防御的完整流程我希望你收获的不仅仅是几种针对JWT的攻击手法。更重要的是建立起一种“攻击者视角”的安全思维模式。在编写每一行身份验证代码时都能下意识地问自己几个问题如果用户提交了一个修改过的Token会怎样如果密钥泄露了会怎样这个依赖Token的权限检查是否在所有分支逻辑上都万无一失安全漏洞往往诞生在那些“这应该没问题吧”的假设时刻。JWT的设计是优雅的但它的安全性完全依赖于开发者的正确实现和配置。没有“开箱即用”的安全。无论是使用JWT、OAuth还是其他任何认证方案深入理解其协议规范、潜在威胁和最佳实践是每一位负责任的开发者必须完成的功课。在实际工作中我建议将JWT的安全检查纳入代码审查清单和自动化安全测试SAST的规则中。同时定期使用像jwt-tool这样的专业工具对你自己的API服务进行授权测试主动发现潜在问题。最后保持对安全社区的关注及时了解新披露的与JWT相关的漏洞例如一些Web服务器或代理的JWT解析模块也曾曝出过漏洞并更新你的依赖库和配置。这条路没有终点但每一步扎实的学习和实战都会让你和你所构建的系统变得更加可靠。