JWT 攻击专题大全——从算法混淆到密钥泄露全覆盖
前言
JWT(JSON Web Token)是现代 Web 应用中最广泛使用的无状态认证方案。它由三部分组成——Header、Payload、Signature,通过 Base64URL 编码后用 . 连接。服务器验证签名即可确认 Token 未被篡改。
JWT 的安全问题集中在三个环节:
- 算法层面——
alg:none跳过验证、RS256→HS256 密钥混淆 - 密钥层面——弱密钥被爆破、私钥/公钥泄露
- Header 注入——
jku/jwk/kid参数被利用
本文从 JWT 结构讲起,按照攻击类型系统整理了 7 大类攻击手法,覆盖从经典的 alg:none 到高级的 kid SQL 注入,并附完整的工具链和防御方案。
一、JWT 基础
1.1 结构
1 | eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c |
解码后:
Header:
1 | { |
Payload:
1 | { |
Signature:
1 | HMACSHA256( |
1.2 常见算法
| 算法 | 类型 | 密钥 | 签名过程 |
|---|---|---|---|
| HS256 | HMAC + SHA256 | 对称密钥(同一个 secret) | 双方用相同 secret 签名和验证 |
| HS384 | HMAC + SHA384 | 对称密钥 | 同上 |
| HS512 | HMAC + SHA512 | 对称密钥 | 同上 |
| RS256 | RSA PKCS#1 + SHA256 | 非对称(私钥签名,公钥验证) | 服务器持有私钥签名 |
| RS384 | RSA PKCS#1 + SHA384 | 非对称 | 同上 |
| RS512 | RSA PKCS#1 + SHA512 | 非对称 | 同上 |
| ES256 | ECDSA + P-256 + SHA256 | 非对称 | 椭圆曲线签名 |
| none | 无签名 | 无 | 不签名,直接信任! |
核心区别: HS256 用同一个 secret 签名→验证。RS256 用私钥签名、公钥验证。这个区别是密钥混淆攻击的基础。
1.3 在线/离线调试
1 | # 解码 JWT(不验证签名) |
二、攻击面总览
1 | JWT 攻击面: |
三、算法层面攻击
3.1 alg:none —— 签名完全绕过
原理: 某些 JWT 库在处理 "alg": "none" 时,会直接跳过签名验证,认为 Token 合法。
攻击方式: 修改 Header 中的 alg 为 none,Payload 改为任意内容,Signature 部分留空。
手动构造:
1 | # Header: {"alg":"none","typ":"JWT"} |
用 Python 生成:
1 | import base64 |
注意:
alg:none在主流 JWT 库(如PyJWT、jsonwebtoken)的较新版本中已被修复——必须显式允许none算法。但如果目标用的是旧版或配置不当,仍然有效。
3.2 RS256 → HS256 密钥混淆(公钥泄露场景)
这是 CTF 中最常见的 JWT 考点。
前提:
- 服务器使用 RS256(非对称),公钥可获取(如
/public.key、/.well-known/jwks.json) - JWT 库没有严格限制算法类型——把 Header 中的
alg从RS256改为HS256后,服务器会用对称密钥方式验证签名 - 由于 HS256 的”secret”就是签名和验证的同一把钥匙,如果我们把 RS256 的公钥作为 HS256 的 secret 来签名,服务器验证时会用同样的公钥验证 → 验证通过!
攻击流程:
1 | 服务端原本:header.alg=RS256 → 用公钥验证签名 |
Python 实现:
1 | import jwt |
Node.js 实现:
1 | const jwt = require('jsonwebtoken'); |
为什么有效?
- RS256:签名用私钥 → 验证用公钥
- HS256:签名和验证用同一个 secret
- 攻击者拿到公钥 → 把
alg改成HS256→ 用公钥作为 secret 签名 → 服务器验证 HS256 时也用公钥 → 签名一致 → 通过
3.3 私钥泄露 —— 直接伪造任意 Token
如果私钥(private.key)泄露,攻击者可以用它正常签名 RS256 Token:
1 | const jwt = require('jsonwebtoken'); |
1 | import jwt |
私钥常见泄露路径:
- Git 仓库中的
.git/泄露 → 找到private.pem - 源码备份文件(
.bak、.swp、.old) - 错误配置的静态文件路径(
/static/private.key) - 目录遍历漏洞
四、密钥爆破
4.1 弱密钥 —— 暴力破解 HS256
当目标使用 HS256 且 secret 是弱密码时,可以离线爆破。
c-jwt-cracker(C 语言,速度快):
1 | # 安装 |
hashcat(GPU 加速):
1 | # 将 JWT 转为 hashcat 格式 |
jwt_tool 字典爆破:
1 | python3 jwt_tool.py TOKEN -C -d /usr/share/wordlists/rockyou.txt |
4.2 常见弱密钥 Top 10
1 | secret |
五、Header 注入攻击
5.1 jku 注入 —— 指定远程 JWK Set
jku(JWK Set URL)是 JWT Header 中的一个可选参数,告诉服务器从指定 URL 获取公钥。
1 | { |
攻击流程:
- 攻击者自生成 RSA 密钥对
- 将公钥以 JWK 格式托管在
http://attacker.com/jwks.json - 在 JWT Header 中注入
jku,指向攻击者的 JWKS - 用对应的私钥签名 Token
- 服务器从
jkuURL 取公钥 → 验证签名通过
JWKS 格式(jwks.json):
1 | { |
用 jwt_tool 生成攻击 Key:
1 | # 生成 RSA 密钥对 + JWKS |
防御 jku 注入: 服务端白名单允许的 jku URL,拒绝一切不在白名单中的地址。
5.2 jwk 注入 —— 直接嵌入公钥
jwk 参数允许在 JWT Header 中直接嵌入公钥(无需远程 URL),服务器使用该公钥验证签名。
1 | { |
攻击流程:
- 生成属于自己的 RSA 密钥对
- 将公钥嵌入 Header 的
jwk字段 - 用对应的私钥签名
- 服务器从 Header 中取公钥验证 → 通过
1 | # jwt_tool 一键攻击 |
CVE 案例: CVE-2018-0114(Cisco 的 JWT 库信任 Header 中的 jwk)
5.3 kid 注入 —— 路径遍历与 SQL 注入
kid(Key ID)是 Header 中用于标识密钥的字段。服务器根据 kid 从密钥库中选择对应的 key。
场景一:路径遍历
如果后端用 kid 的值作为文件路径读取密钥:
1 | { |
服务器读取 /etc/passwd 的内容作为 HS256 的 secret → 攻击者用同样的文件内容签名即可通过验证。
更暴力的方式——指向 /dev/null(空文件):
1 | { |
然后用空字符串作为 secret 签名。
场景二:SQL 注入
如果后端把 kid 拼接到 SQL 查询中:
1 | { |
服务器查询密钥时执行了注入后的 SQL,返回 attacker_secret,攻击者用 attacker_secret 做 HS256 secret 签名即可。
1 | # jwt_tool kid SQL 注入 |
5.4 kid + 对称密钥组合拳
更隐蔽的做法:将 kid 指向一个已知内容的文件(如 /proc/sys/kernel/randomize_va_space 内容为 2\n),然后用该内容作为 HS256 secret 签名。
1 | { |
已知该文件内容为 2\n,攻击者用 2\n 作为 secret → 服务器验证通过。
5.5 其他 Header 参数注入
| 参数 | 作用 | 攻击面 |
|---|---|---|
cty |
Content-Type | 可能影响后续处理逻辑 |
typ |
Token 类型 | JWT → JWE 混淆,或 JWT → JWS 绕过 |
x5c |
X.509 证书链 | 注入恶意证书 |
x5u |
X.509 URL | 指定远程证书,类似 jku |
crit |
Critical Headers | 声明必须被理解的字段,可能造成拒绝服务 |
六、Payload 注入利用
6.1 常见敏感 Claim
| Claim | 含义 | 攻击目标 |
|---|---|---|
sub |
Subject(用户标识) | 改成 admin / root |
iss |
Issuer | 绕过 Issuer 校验 |
aud |
Audience | 绕过 Audience 校验 |
exp |
Expiration | 改为未来时间(或直接删除) |
nbf |
Not Before | 改为过去时间(或直接删除) |
iat |
Issued At | 通常不影响鉴权 |
role |
自定义(角色) | 改为 admin / superuser |
username |
自定义(用户名) | 改为管理员用户名 |
6.2 批量 Payload 篡改
1 | import jwt |
七、攻击工具汇总
7.1 jwt_tool(瑞士军刀)
1 | # 安装 |
7.2 Burp Suite 插件
- JWT Editor (BApp Store) — 解码、编辑、签名 JWT
- JWT4B — 自动检测常见 JWT 漏洞
- JSON Web Tokens — 在 Repeater/Proxy 中自动识别和解码 JWT
7.3 其他工具
| 工具 | 用途 |
|---|---|
| c-jwt-cracker | C 语言暴力破解 HS256 secret |
| hashcat | GPU 加速爆破(mode 16500) |
| jwt.io | 在线 JWT 调试器 |
| jwks.io | JWK Set 生成器 |
| PyJWT | Python JWT 库(用于构造攻击脚本) |
| PortSwigger JWT Lab | 在线 JWT 攻击实验环境 |
八、实战攻击链
8.1 典型 CTF 攻击链
1 | ┌─────────────────────────────────────────────────────────────┐ |
8.2 更复杂的链:kid SQL 注入 → 控制密钥
1 | 1. 拿到 JWT,发现 Header 中有 kid → "some-key-id" |
九、防御措施
| 层面 | 措施 | 说明 |
|---|---|---|
| 算法白名单 | 服务端指定允许的算法列表 | 拒绝 none,只允许 RS256 或只允许 HS256 |
| 算法类型绑定 | 非对称密钥时禁止对称算法 | RS256 的公钥不能用于 HS256 验证 |
| 密钥安全 | 使用足够强的密钥(HS256 ≥ 256 bit) | 防止暴力破解 |
| 密钥管理 | 私钥不外泄、定期轮换 | 密钥文件不放在 Web 目录下 |
| Header 白名单 | 忽略或拒绝 Header 中的 jku/jwk/kid 等敏感参数 |
除非业务确实需要 |
| kid 处理 | kid 仅用作查找索引,不参与文件路径拼接 |
防止路径遍历和 SQL 注入 |
| JWKS 端点 | 不暴露私钥、限制请求频率 | /.well-known/jwks.json 仅返回公钥 |
| 库版本 | 使用最新版 JWT 库 | 旧版库对 alg:none 和 jwk 注入缺乏防护 |
| Token 有效期 | 设置较短的 exp(如 15 分钟) |
即使 Token 被伪造,窗口期也有限 |
服务端安全配置示例
Node.js(jsonwebtoken):
1 | const jwt = require('jsonwebtoken'); |
Python(PyJWT):
1 | import jwt |
安全密钥生成
1 | # HS256:生成 256-bit 强随机密钥 |
十、快速决策树
1 | 拿到 JWT Token: |