JWT 的全称是 Json Web Token。它遵循 JSON 格式,将用户信息加密到 token 里,服务器不保存任何用户信息,只保存密钥信息,通过使用特定加密算法验证 token,通过 token 验证用户身份。基于 token 的身份验证可以替代传统的 cookie+session 身份验证方法。
jwt 由三个部分组成:header
.payload
.signature
header 部分
header 部分最常用的两个字段是alg
和typ
,alg
指定了 token 加密使用的算法(最常用的为HMAC和RSA算法),typ`声明类型为 JWT
header 通常会长这个样子:
{
"alg" : "HS256",
"typ" : "jwt"
}
payload 部分
payload 则为用户数据以及一些元数据有关的声明,用以声明权限,举个例子,一次登录的过程可能会传递以下数据
{
"user_role" : "finn", //当前登录用户
"iss": "admin", //该JWT的签发者
"iat": 1573440582, //签发时间
"exp": 1573940267, //过期时间
"nbf": 1573440582, //该时间之前不接收处理该Token
"domain": "example.com", //面向的用户
"jti": "dff4214121e83057655e10bd9751d657" //Token唯一标识
}
signature 部分
signature 的功能是保护 token 完整性。
生成方法为将 header 和 payload 两个部分联结起来,然后通过 header 部分指定的算法,计算出签名。
抽象成公式就是
signature = HMAC-SHA256(base64urlEncode(header) + '.' + base64urlEncode(payload), secret_key)
值得注意的是,编码 header 和 payload 时使用的编码方式为base64urlencode
,base64url
编码是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 都可以通过服务器的验证。举个例子,使用以下的字段
{
"alg" : "None",
"typ" : "jwt"
}
{
"user" : "Admin"
}
生成的完整 token 为ew0KCSJhbGciIDogIk5vbmUiLA0KCSJ0eXAiIDogImp3dCINCn0.ew0KCSJ1c2VyIiA6ICJBZG1pbiINCn0
(header+’.’+payload,去掉了’.’+signature 字段)
空加密算法的设计初衷是用于调试的,但是如果某天开发人员脑阔瓦特了,在生产环境中开启了空加密算法,缺少签名算法,jwt 保证信息不被篡改的功能就失效了。攻击者只需要把 alg 字段设置为 None,就可以在 payload 中构造身份信息,伪造用户身份。
修改 RSA 加密算法为 HMAC
JWT 中最常用的两种算法为HMAC
和RSA
。
HMAC
是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)的缩写,它是一种对称加密算法,使用相同的密钥对传输信息进行加解密。
RSA
则是一种非对称加密算法,使用私钥加密明文,公钥解密密文。
在 HMAC 和 RSA 算法中,都是使用私钥对signature
字段进行签名,只有拿到了加密时使用的私钥,才有可能伪造 token。
现在我们假设有这样一种情况,一个 Web 应用,在 JWT 传输过程中使用 RSA 算法,密钥pem
对 JWT token 进行签名,公钥pub
对签名进行验证。
{
"alg" : "RS256",
"typ" : "jwt"
}
通常情况下密钥pem
是无法获取到的,但是公钥pub
却可以很容易通过某些途径读取到,这时,将 JWT 的加密算法修改为 HMAC,即
{
"alg" : "HS256",
"typ" : "jwt"
}
同时使用获取到的公钥pub
作为算法的密钥,对 token 进行签名,发送到服务器端。
服务器端会将 RSA 的公钥(pub
)视为当前算法(HMAC)的密钥,使用 HS256 算法对接收到的签名进行验证。
REF: https://skysec.top/2018/05/19/2018CUMTCTF-Final-Web/#Pastebin/
爆破密钥
俗话说,有密码验证的地方,就有会爆破。
不过对 JWT 的密钥爆破需要在一定的前提下进行:
- 知悉 JWT 使用的加密算法
- 一段有效的、已签名的 token
- 签名用的密钥不复杂(弱密钥)
所以其实 JWT 密钥爆破的局限性很大。
相关工具:c-jwt-cracker
以下是几个使用示例
可以看到简单的字母数字组合都是可以爆破的,但是密钥位数稍微长一点或者更复杂一点的话,爆破时间就会需要很久。
修改 KID 参数
kid
是 jwt header 中的一个可选参数,全称是key ID
,它用于指定加密算法的密钥
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/home/jwt/.ssh/pem"
}
因为该参数可以由用户输入,所以也可能造成一些安全问题。
任意文件读取
kid
参数用于读取密钥文件,但系统并不会知道用户想要读取的到底是不是密钥文件,所以,如果在没有对参数进行过滤的前提下,攻击者是可以读取到系统的任意文件的。
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/etc/passwd"
}
SQL 注入
kid
也可以从数据库中提取数据,这时候就有可能造成 SQL 注入攻击,通过构造 SQL 语句来获取数据或者是绕过 signature 的验证
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "key11111111' || union select 'secretkey' -- "
}
命令注入
对kid
参数过滤不严也可能会出现命令注入问题,但是利用条件比较苛刻。如果服务器后端使用的是 Ruby,在读取密钥文件时使用了open
函数,通过构造参数就可能造成命令注入。
“/path/to/key_file|whoami”
对于其他的语言,例如 php,如果代码中使用的是exec
或者是system
来读取密钥文件,那么同样也可以造成命令注入,当然这个可能性就比较小了。
修改 JKU/X5U 参数
JKU
的全称是 “JSON Web Key Set URL”,用于指定一组用于验证令牌的密钥的 URL。类似于kid
,JKU
也可以由用户指定输入数据,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定 web 应用使用该组密钥来验证 token。
X5U
则以 URI 的形式数允许攻击者指定用于验证令牌的公钥证书或证书链,与JKU
的攻击利用方式类似。
其他方式
信息泄露
JWT 保证的是数据传输过程中的完整性而不是机密性。
由于 payload 是使用base64url
编码的,所以相当于明文传输,如果在 payload 中携带了敏感信息(如存放密钥对的文件路径),单独对 payload 部分进行base64url
解码,就可以读取到 payload 中携带的信息。
https://xz.aliyun.com/t/6776#toc-5