JWT 的全称是 Json Web Token。它遵循 JSON 格式,将用户信息加密到 token 里,服务器不保存任何用户信息,只保存密钥信息,通过使用特定加密算法验证 token,通过 token 验证用户身份。基于 token 的身份验证可以替代传统的 cookie+session 身份验证方法。

jwt 由三个部分组成:header.payload.signature

header 部分

header 部分最常用的两个字段是algtypalg指定了 token 加密使用的算法(最常用的为HMACRSA算法),typ`声明类型为 JWT

header 通常会长这个样子:

  1. {
  2. "alg" : "HS256",
  3. "typ" : "jwt"
  4. }

payload 部分

payload 则为用户数据以及一些元数据有关的声明,用以声明权限,举个例子,一次登录的过程可能会传递以下数据

  1. {
  2. "user_role" : "finn", //当前登录用户
  3. "iss": "admin", //该JWT的签发者
  4. "iat": 1573440582, //签发时间
  5. "exp": 1573940267, //过期时间
  6. "nbf": 1573440582, //该时间之前不接收处理该Token
  7. "domain": "example.com", //面向的用户
  8. "jti": "dff4214121e83057655e10bd9751d657" //Token唯一标识
  9. }

signature 部分

signature 的功能是保护 token 完整性。

生成方法为将 header 和 payload 两个部分联结起来,然后通过 header 部分指定的算法,计算出签名。

抽象成公式就是

signature = HMAC-SHA256(base64urlEncode(header) + '.' + base64urlEncode(payload), secret_key)

值得注意的是,编码 header 和 payload 时使用的编码方式为base64urlencodebase64url编码是base64的修改版,为了方便在网络中传输使用了不同的编码表,它不会在末尾填充 “=” 号,并将标准 Base64 中的 “+” 和 “/“ 分别改成了 “-“ 和 “-“。

完整 token 生成

一个完整的 jwt 格式为 (header.payload.signature),其中 header、payload 使用 base64url 编码,signature 通过指定算法生成。

python 的Pyjwt使用示例如下

import jwt

encoded_jwt \= jwt.encode({‘user_name’:’admin’},’key’, algorithm\=’HS256’)
print(encoded_jwt)
print(jwt.decode(encoded_jwt, ‘key’, algorithms\=[‘HS256’]))

生成的 token 为

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiJ9.oL5szC7mFoJ_7FI9UVMcKfmisqr6Qlo1dusps5wOUlo

加密算法

空加密算法

JWT 支持使用空加密算法,可以在 header 中指定 alg 为None

这样的话,只要把 signature 设置为空(即不添加 signature 字段),提交到服务器,任何 token 都可以通过服务器的验证。举个例子,使用以下的字段

  1. {
  2. "alg" : "None",
  3. "typ" : "jwt"
  4. }
  5. {
  6. "user" : "Admin"
  7. }

生成的完整 token 为ew0KCSJhbGciIDogIk5vbmUiLA0KCSJ0eXAiIDogImp3dCINCn0.ew0KCSJ1c2VyIiA6ICJBZG1pbiINCn0

(header+’.’+payload,去掉了’.’+signature 字段)

空加密算法的设计初衷是用于调试的,但是如果某天开发人员脑阔瓦特了,在生产环境中开启了空加密算法,缺少签名算法,jwt 保证信息不被篡改的功能就失效了。攻击者只需要把 alg 字段设置为 None,就可以在 payload 中构造身份信息,伪造用户身份。

修改 RSA 加密算法为 HMAC

JWT 中最常用的两种算法为HMACRSA

HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)的缩写,它是一种对称加密算法,使用相同的密钥对传输信息进行加解密。

RSA则是一种非对称加密算法,使用私钥加密明文,公钥解密密文。

在 HMAC 和 RSA 算法中,都是使用私钥对signature字段进行签名,只有拿到了加密时使用的私钥,才有可能伪造 token。

现在我们假设有这样一种情况,一个 Web 应用,在 JWT 传输过程中使用 RSA 算法,密钥pem对 JWT token 进行签名,公钥pub对签名进行验证。

  1. {
  2. "alg" : "RS256",
  3. "typ" : "jwt"
  4. }

通常情况下密钥pem是无法获取到的,但是公钥pub却可以很容易通过某些途径读取到,这时,将 JWT 的加密算法修改为 HMAC,即

  1. {
  2. "alg" : "HS256",
  3. "typ" : "jwt"
  4. }

同时使用获取到的公钥pub作为算法的密钥,对 token 进行签名,发送到服务器端。

服务器端会将 RSA 的公钥(pub)视为当前算法(HMAC)的密钥,使用 HS256 算法对接收到的签名进行验证。

REF: https://skysec.top/2018/05/19/2018CUMTCTF-Final-Web/#Pastebin/

爆破密钥

俗话说,有密码验证的地方,就有会爆破。

不过对 JWT 的密钥爆破需要在一定的前提下进行:

  • 知悉 JWT 使用的加密算法
  • 一段有效的、已签名的 token
  • 签名用的密钥不复杂(弱密钥)

所以其实 JWT 密钥爆破的局限性很大。

相关工具:c-jwt-cracker

以下是几个使用示例

攻击JWT的一些方法 - 先知社区 - 图1

可以看到简单的字母数字组合都是可以爆破的,但是密钥位数稍微长一点或者更复杂一点的话,爆破时间就会需要很久。

修改 KID 参数

kid是 jwt header 中的一个可选参数,全称是key ID,它用于指定加密算法的密钥

  1. {
  2. "alg" : "HS256",
  3. "typ" : "jwt",
  4. "kid" : "/home/jwt/.ssh/pem"
  5. }

因为该参数可以由用户输入,所以也可能造成一些安全问题。

任意文件读取

kid参数用于读取密钥文件,但系统并不会知道用户想要读取的到底是不是密钥文件,所以,如果在没有对参数进行过滤的前提下,攻击者是可以读取到系统的任意文件的。

  1. {
  2. "alg" : "HS256",
  3. "typ" : "jwt",
  4. "kid" : "/etc/passwd"
  5. }

SQL 注入

kid也可以从数据库中提取数据,这时候就有可能造成 SQL 注入攻击,通过构造 SQL 语句来获取数据或者是绕过 signature 的验证

  1. {
  2. "alg" : "HS256",
  3. "typ" : "jwt",
  4. "kid" : "key11111111' || union select 'secretkey' -- "
  5. }

命令注入

kid参数过滤不严也可能会出现命令注入问题,但是利用条件比较苛刻。如果服务器后端使用的是 Ruby,在读取密钥文件时使用了open函数,通过构造参数就可能造成命令注入。

“/path/to/key_file|whoami”

对于其他的语言,例如 php,如果代码中使用的是exec或者是system来读取密钥文件,那么同样也可以造成命令注入,当然这个可能性就比较小了。

修改 JKU/X5U 参数

JKU的全称是 “JSON Web Key Set URL”,用于指定一组用于验证令牌的密钥的 URL。类似于kidJKU也可以由用户指定输入数据,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定 web 应用使用该组密钥来验证 token。

X5U则以 URI 的形式数允许攻击者指定用于验证令牌的公钥证书或证书链,与JKU的攻击利用方式类似。

其他方式

信息泄露

JWT 保证的是数据传输过程中的完整性而不是机密性。

由于 payload 是使用base64url编码的,所以相当于明文传输,如果在 payload 中携带了敏感信息(如存放密钥对的文件路径),单独对 payload 部分进行base64url解码,就可以读取到 payload 中携带的信息。
https://xz.aliyun.com/t/6776#toc-5