JWT Header JWK 注入提权漏洞

记录 JWT Header 中嵌入 JWK 导致签名信任源错误的原理、Burp JWT Editor 操作过程、接口识别与防御思路。

  • #JWT
  • #JWK
  • #Header 注入
  • #身份认证
  • #Burp Suite

本文记录授权靶场中的学习过程,仅用于理解 JWT 验签中的信任源问题。

一、漏洞原理

1. 核心成因:信任源错误

JWK(JSON Web Key)是一种使用 JSON 表示密码学密钥的格式。JWT Header 可以包含 jwk 字段,用于携带验证签名所需的公钥。

支持 jwk 字段本身不一定存在漏洞。真正的问题是:后端验签时直接信任客户端传入的公钥,却没有验证这个密钥是否来自受信任来源。这样一来,服务器虽然执行了签名验证,验证的却是攻击者自己提供的密钥。

2. 技术细节

以 RS256 为例,私钥用于签名,公钥用于验证。存在漏洞的后端没有强制使用服务器保存的可信公钥,而是读取了 JWT Header 中客户端可控的 jwk:

{
  "kid": "attacker-key-id",
  "alg": "RS256",
  "jwk": {
    "kty": "RSA",
    "kid": "attacker-key-id",
    "use": "sig",
    "alg": "RS256",
    "n": "攻击者公钥的模数",
    "e": "AQAB"
  }
}

jwk 中只嵌入公开部分。对应私钥仍保留在本地,用来给修改后的 Header 和 Payload 重新签名。

3. 攻击闭环

  1. 在本地生成一对自己的 RSA 密钥,包括公钥和私钥。
  2. 修改 Payload,将身份提权为 "role":"admin"。
  3. 将自己的公钥以 jwk 字段嵌入 JWT Header。
  4. 使用对应私钥对修改后的 Header 和 Payload 重新签名。
  5. 服务器收到 Token 后,错误地使用 Header 中的 JWK 公钥验签。因为签名正是由配套私钥生成的,所以验证通过并造成越权。

这个过程不是破解服务器私钥,也不是跳过签名验证,而是诱导服务器使用攻击者指定的公钥进行验证。

二、解题步骤拆解

1. 抓包分析

登录系统,在 Burp 中捕获身份验证请求,并发送到 Repeater。请求头中包含 JWT:

Authorization: Bearer <Token>

先使用原始 Token 请求目标数据接口,确认当前普通用户的权限和响应,作为后续对照。

2. 生成 RSA 密钥对

进入 Burp 顶部的 JWT Editor Keys 标签页:

  1. 点击 New RSA Key。
  2. 点击 Generate,自动生成新的 RSA 密钥对。
  3. 保存密钥。

生成的密钥只用于授权靶场测试,不应混入真实业务密钥库或提交到博客仓库。

3. 修改 Payload

回到 Repeater,切换至 JSON Web Token 标签页,根据靶场实际使用的身份字段修改 Payload:

{
  "username": "admin",
  "role": "admin"
}

字段名没有统一规定。必须以原始 Token 和后端实际授权逻辑为准,不是所有系统都通过 username 或 role 判断权限。

4. 执行 Embedded JWK

点击:

Attack → Embedded JWK

选择刚才生成的 RSA 密钥。插件会完成两个关键动作:

  • 把该密钥对的公钥部分写入 Header 的 jwk 字段;
  • 使用对应私钥重新签名修改后的 Token。

执行后应检查 Header 中是否已经出现 jwk,并确认顶层 kid 与嵌入密钥的 kid 保持一致。手工构造时,这个细节尤其容易漏掉。

5. 请求数据接口

将目标路径修改为个人信息 API,并携带构造后的 Token:

GET /api/userinfo HTTP/1.1
Authorization: Bearer <构造后的 Token>

发送请求后,根据 HTTP 状态码、响应正文和实际权限综合判断。在该靶场中,成功后可以从 JSON 响应的 flag 字段获取结果。

三、踩坑与考点总结

1. 接口识别

  • POST /api/login 用于获取初始 Token。
  • POST /api/logout 仅用于注销会话。
  • 只有携带 Token 请求数据接口,例如 /api/userinfo,才能触发后端的管理员鉴权逻辑。

2. 前后端分离逻辑

访问页面路由,例如 /user,返回的是前端渲染视图(HTML);访问数据接口,例如 /api/userinfo,返回的是纯 JSON 业务数据。该靶场的 Flag 绑定在 API 的数据响应中。

3. 不要只看状态码

200 可能只是普通用户接口正常返回,302 也可能是应用本身的跳转流程。应对比修改前后的响应内容、身份字段以及是否真正获得了目标权限。

四、防御修复建议

  • 建立可信密钥边界: 服务端应使用本地配置、密钥管理服务或经过严格认证和白名单限制的密钥源验签,不能直接信任 Token 自带的公钥。
  • 限制 Header 参数: 如果业务不需要嵌入 JWK,应拒绝或忽略客户端传入的 jwk、jku 等密钥选择参数;确实需要时,必须验证来源和允许范围。
  • 固定允许算法: 服务端根据自身配置确定允许的算法,不能让客户端 Header 单独决定验签策略。
  • 校验密钥标识: kid 只能在服务端维护的可信密钥集合中选择,不能据此加载任意密钥或文件。
  • 完成声明校验: 签名通过后仍要检查 exp、nbf、iss、aud 等声明,并在业务层重新执行权限判断。

五、复盘 Tips

  1. JWK 是 JSON 格式的密钥表示,JWT 是承载声明的 Token,两者不是同一种东西。
  2. RS256 中私钥负责签名、公钥负责验签。 Embedded JWK 注入成功的关键,是服务器错误地相信了客户端提供的公钥。
  3. 签名验证成功不等于 Token 值得信任。 验签所使用的密钥来源也必须可信。
  4. 手工嵌入 JWK 时注意 kid。 顶层 Header 的 kid 通常需要与嵌入 JWK 的 kid 一致;JWT Editor 的自动攻击功能会帮助处理这一步。
  5. 修改身份字段应遵循最小变更原则。 一次只改真正参与授权的字段,便于判断是哪一步产生了效果。

参考:PortSwigger Web Security Academy:JWK Header 注入靶场