JWT 密钥混淆攻击深度解析:从 RS256 到 HS256 的 3 种实战场景与防御
JSON Web Token(JWT)作为现代身份验证的核心组件,其安全性直接关系到系统整体的防护能力。本文将深入探讨一种常被忽视的高危攻击手法——密钥混淆攻击(Algorithm Confusion),通过真实案例拆解攻击原理,并提供可落地的防御方案。
1. 密钥混淆攻击的核心原理与算法差异
密钥混淆攻击的本质在于利用服务器对JWT算法验证的逻辑缺陷,诱使系统使用非预期的密钥类型进行签名验证。要理解这种攻击,首先需要明确两种主流签名算法的关键区别:
RS256(非对称加密)工作流程:
- 私钥用于签名,公钥用于验证
- 典型应用场景:多服务端共享验证能力
- 数学基础:RSA算法,基于大整数分解难题
HS256(对称加密)工作流程:
- 同一密钥用于签名和验证
- 典型应用场景:单一服务端环境
- 数学基础:HMAC算法,基于哈希消息认证码
当开发者在代码中错误配置验证逻辑时,就会出现致命漏洞。例如以下危险代码模式:
# 危险示例:未强制指定算法类型 def verify_token(token): public_key = load_public_key() # 加载RSA公钥 return jwt.decode(token, public_key) # 依赖token头部的alg参数攻击者可以通过以下步骤实施攻击:
- 获取或推导服务器公钥
- 修改JWT头部
alg为HS256 - 使用公钥作为HMAC密钥重新签名
- 服务器误用公钥进行HS256验证
2. 三种典型攻击场景实战分析
2.1 公钥直接泄露场景
攻击条件:
- 公钥通过接口或文件意外暴露
- 服务器未校验算法白名单
操作步骤:
- 从
/static/public.pem获取公钥 - 使用以下命令生成恶意令牌:
python3 jwt_tool.py <original_token> -X k -pk public.pem防御代码示例(Node.js):
const verifyOptions = { algorithms: ['RS256'], // 强制算法类型 ignoreExpiration: false }; jwt.verify(token, publicKey, verifyOptions);2.2 通过JWKS端点间接获取公钥
攻击条件:
- 存在
/.well-known/jwks.json端点 - 服务器支持动态密钥加载
关键攻击步骤:
- 请求JWKS端点获取公钥参数:
{ "keys": [{ "kty": "RSA", "e": "AQAB", "n": "mVx2PicKCcRTrUZ..." }] }- 使用以下Python脚本转换格式:
from jwt.algorithms import RSAAlgorithm key = RSAAlgorithm.from_jwk(jwk_data) # 转换为PEM格式- 制作恶意JWT时注意保留原始
kid值
防御建议:
- 禁用动态JWKS加载
- 实现密钥指纹校验机制
2.3 从已有令牌暴力推导公钥
攻击条件:
- 能获取两个使用相同密钥签名的有效令牌
- 服务器使用弱密钥对
使用sig2n工具推导:
docker run --rm -it portswigger/sig2n <token1> <token2>输出结果示例:
Possible n values found: 3 [1] X.509 PEM: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzXo7Wt... [2] PKCS1 PEM: -----BEGIN RSA PUBLIC KEY----- MIIBCgKCAQEAzXo7Wt...防御对策:
- 定期轮换加密密钥
- 使用强密钥(RSA-2048以上)
3. 多维度防御体系构建
3.1 代码层防护
Spring Security配置示例:
@Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withPublicKey(publicKey) .signatureAlgorithm(SignatureAlgorithm.RS256) // 强制算法 .jwtProcessorCustomizer(processor -> { processor.setJWTClaimsSetVerifier((claims, context) -> { if (!"RS256".equals(claims.getHeader().getAlgorithm())) { throw new JwtException("Invalid algorithm"); } }); }) .build(); }3.2 架构设计原则
密钥管理矩阵:
| 方案类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| HS256+密钥轮换 | 实现简单 | 密钥分发复杂 | 单服务架构 |
| RS256+集中验证 | 验证端无密钥 | 性能开销大 | 微服务架构 |
| ES256+PQC | 抗量子计算 | 兼容性差 | 金融级系统 |
3.3 运维监控策略
异常算法告警规则:
- 同一用户HS256/RS256交替出现
- alg字段频繁变更
密钥生命周期管理:
graph LR A[密钥生成] --> B[安全存储] B --> C[定期轮换] C --> D[历史密钥归档] D --> E[安全销毁]
4. 主流框架加固指南
4.1 Express-jwt 安全配置
const { expressjwt } = require('express-jwt'); app.use(expressjwt({ secret: publicKey, algorithms: ['RS256'], getToken: (req) => { if (!req.headers.authorization?.match(/^Bearer /)) { throw new Error('Invalid authorization header'); } return req.headers.authorization.split(' ')[1]; } }).unless({ path: ['/public'] }));4.2 Django REST框架实践
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ] } SIMPLE_JWT = { 'ALGORITHM': 'RS256', 'VERIFYING_KEY': open('public.pem').read(), 'AUTH_HEADER_TYPES': ('Bearer',), 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',) }5. 深度防御进阶技巧
密钥派生方案:
def derive_key(master_key, key_id): # 使用HKDF派生业务专用密钥 return HKDF( algorithm=hashes.SHA256(), length=32, salt=None, info=key_id.encode(), ).derive(master_key)JWT验证流程图:
开始 → 解析Header → 检查alg是否在白名单? → 否 → 拒绝 ↓是 验证签名有效性 → 失败 → 拒绝 ↓成功 检查payload时效 → 过期 → 拒绝 ↓有效 权限上下文构建 → 完成验证在实际项目经验中,最容易被忽视的是开发环境与生产环境的一致性检查。曾遇到测试环境使用HS256而生产环境使用RS256,但验证逻辑未同步更新的案例,这为攻击者创造了完美的混淆条件。建议在CI/CD流程中加入算法验证步骤,确保全环境策略统一。