Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

JWT (JSON Web Token) 攻击

| , 4 minutes reading.

1. 定义

JWT (JSON Web Token) 是一种紧凑的、URL 安全的令牌格式,用于认证和信息交换。JWT 攻击利用应用程序在创建、验证或处理这些令牌时的弱点。

JWT 由三部分组成:header.payload.signature

常见的 JWT 漏洞允许攻击者:

  • 在不知道密钥的情况下伪造有效令牌
  • 通过修改令牌声明来提升权限
  • 完全绕过认证
  • 冒充其他用户

2. 技术原理

JWT 结构:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.    <- 头部 (Base64)
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.  <- 载荷 (Base64)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c     <- 签名

头部: 指定算法(如 HS256、RS256、none) 载荷: 包含声明(用户 ID、角色、过期时间) 签名: 验证令牌未被篡改

常见的 JWT 攻击类型:

  1. 算法混淆 (alg=none): 某些库接受 "alg": "none",允许未签名的令牌。

  2. 算法切换 (RS256 到 HS256): 欺骗服务器使用公钥作为 HMAC 密钥。

  3. 弱密钥暴力破解: 离线破解弱 HMAC 密钥。

  4. JWT 头部注入 (jku/x5u): 指向攻击者控制的密钥服务器。

  5. 声明篡改: 在没有正确签名验证的情况下修改载荷。

3. 攻击流程(算法混淆)

sequenceDiagram
    participant Attacker as 攻击者
    participant Server as Web 服务器
    participant Library as JWT 库

    Note over Server: 服务器使用 RS256<br/>公钥/私钥对

    Attacker->>Attacker: 获取服务器公钥

    Attacker->>Attacker: 创建伪造的 JWT<br/>头部: alg=HS256<br/>载荷: admin=true

    Attacker->>Attacker: 用公钥签名<br/>将其视为 HMAC 密钥

    Attacker->>Server: 使用伪造的 JWT 请求

    Server->>Library: 验证 JWT

    Library->>Library: 从头部读取 alg=HS256<br/>使用公钥作为 HMAC 密钥<br/>签名匹配!

    Library-->>Server: 令牌有效

    Server-->>Attacker: 以管理员身份授予访问权限

4. 真实案例:Auth0 算法混淆 (2015)

目标: Auth0 JWT 库用户。 漏洞类别: 算法混淆 (CVE-2015-9235)。

漏洞背景: Auth0 的 Node.js jsonwebtoken 库存在严重缺陷。在验证使用 RSA (RS256) 签名的令牌时,如果攻击者将头部中的算法更改为 HS256,库会使用 RSA 公钥作为 HMAC 密钥。

为什么会这样:

  1. RS256 使用非对称加密:私钥签名,公钥验证。
  2. HS256 使用对称加密:签名和验证使用相同的密钥。
  3. 公钥通常是…公开的(在 JWKS 端点、证书等中)。
  4. 库信任来自令牌本身的 alg 头部。

攻击过程:

// 原始令牌 (RS256)
// 头部: {"alg": "RS256", "typ": "JWT"}
// 使用服务器私钥签名

// 攻击者伪造的令牌 (HS256)
// 头部: {"alg": "HS256", "typ": "JWT"}
// 载荷: {"user": "admin", "role": "superuser"}
// 使用服务器公钥作为 HMAC 密钥签名

影响: 任何使用该易受攻击库的应用程序都可能完全绕过其认证。这影响了数千个使用 Auth0 库的应用程序。

5. 深度防御策略

A. 显式指定算法

永远不要信任令牌头部中指定的算法。

// 坏:来自令牌头部的算法
jwt.verify(token, secret);

// 好:显式算法白名单
jwt.verify(token, secret, { algorithms: ['HS256'] });

// 好:对于 RSA
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

B. 强密钥

对于基于 HMAC 的算法,使用加密强度的密钥。

  • 最小长度: HS256 需要 256 位(32 字节)。
  • 随机生成: 使用加密随机生成器。
  • 避免字典词: 避免可猜测的密码。
# 生成强密钥
openssl rand -base64 32

弱密钥可以被离线破解:

# 攻击者可以暴力破解弱密钥
hashcat -m 16500 jwt.txt wordlist.txt

C. 适当的密钥管理

  • 分离密钥: 不同环境使用不同密钥。
  • 密钥轮换: 定期轮换并保留重叠期。
  • 安全存储: 使用 HSM 或密钥管理服务。

D. 验证所有声明

不仅要验证签名——还要验证载荷。

jwt.verify(token, secret, {
  algorithms: ['HS256'],
  issuer: 'https://myapp.com',      // 验证发行者
  audience: 'https://api.myapp.com', // 验证受众
  clockTolerance: 30,                // 允许 30 秒时钟偏差
  maxAge: '1h'                       // 拒绝超过 1 小时的令牌
});

E. 短过期时间

最小化被盗令牌的机会窗口。

  • 访问令牌: 15 分钟或更短。
  • 刷新令牌: 数小时到数天,需安全存储。
  • 实现令牌撤销: 为受损令牌维护黑名单。

F. 避免在载荷中放置敏感数据

JWT 是签名的,不是加密的(除非使用 JWE)。

// 坏:JWT 中的敏感数据
{
  "user_id": 123,
  "credit_card": "4111-1111-1111-1111",  // 永远不要这样做!
  "ssn": "123-45-6789"                   // 永远不要这样做!
}

// 好:仅非敏感标识符
{
  "user_id": 123,
  "role": "user",
  "exp": 1609459200
}

G. 分布式系统使用非对称算法

当多个服务需要验证令牌时:

  • RS256/RS384/RS512: RSA 签名
  • ES256/ES384/ES512: ECDSA 签名

只有认证服务器拥有私钥;所有服务都可以使用公钥验证。

H. 实现 JTI (JWT ID) 防止重放

{
  "jti": "unique-token-id-abc123",  // 唯一标识符
  "user_id": 123,
  "exp": 1609459200
}

跟踪已使用的 jti 值以防止令牌重放。

6. 参考资料