JWT认证安全分析
前言
0x01 JWT相关
1. JWT定义
JWT 是 JSON web token ,为了在网络应用环境中传递声明而执行的一种基于json的开放标准。非常适用于分布式的单点登录场景。主要是用来校验身份提供者和服务提供者之间传递的用户身份信息。
2. JWT结构
jwt由三个部分组成:header.payload.signature,各部分用“.”相连构成一个完整的Token,形如xxxxx.yyyyy.zzzzz
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中的"+"和"/"分别改成了"-"和"_"。
3. JWT完整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
4. JWT令牌解码
由于Header和Payload部分是使用可逆base64方法编码的,因此任何能够看到令牌的人都可以读取数据。
要读取内容,只需要将每个部分传递给base64解码函数。当然也可以使用一些在线解密网站
5.加密算法
JWT中最常用的两种算法为HMAC和RSA。
HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)的缩写,它是一种对称加密算法,使用相同的密钥对传输信息进行加解密。
RSA则是一种非对称加密算法,使用私钥加密明文,公钥解密密文。
0x02 JWT安全问题分析
1.加密算法
1.1 空加密算法
原理
JWT支持使用空加密算法,可以在header中指定alg为None。这样的话,只要把signature设置为空(即不添加signature字段),提交到服务器,任何token都可以通过服务器的验证。
实例
通过普通用户登录,点击投票按钮,显示不是管理员用户:
然后对cookie中的token进行解码查看:
然后将加密算法改为none,将admin值改为true重新加密。这里采用的加密只是base64url加密,可以用脚本生成:
import base64
def b64urlencode(data):
return base64.b64encode(data).replace(b'+', b'-').replace(b'/', b'_').replace(b'=', b'')
print(b64urlencode(b'{"alg":"none"}') + b'.' + b64urlencode(b'{"iat":1640403035,"admin":"true","user":"Jerry"}') + b'.')
生成:
eyJhbGciOiJub25lIn0.eyJpYXQiOjE2NDA0MDMwMzUsImFkbWluIjoidHJ1ZSIsInVzZXIiOiJKZXJyeSJ9.
替换之后,作为管理员可以投票
1.2 修改加密算法
原理
在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算法对接收到的签名进行验证。
实例
访问:
https://demo.sjoerdlangkemper.nl/jwtdemo/rs256.php
得到jwttoken信息,然后解密查看:
加密算法为rs256。访问http://demo.sjoerdlangkemper.nl/jwtdemo/public.pem
得到公钥:
然后通过py重新加密生成
import jwt
public = open('public.pem', 'r').read()
print (public)
print (jwt.encode({"data":"test"}, key=public, algorithm='HS256'))
在python安装jwt之后运行encode可能会遇到:
解决方式为找到python中jwt包中的algorithms.py文件,然后注释下面几行
最后利用生成的token可以伪造信息
2. KID参数
2.1 任意文件读取
原理
KID参数用于读取密钥文件,但系统并不会知道用户想要读取的到底是不是密钥文件,所以,如果在没有对参数进行过滤的前提下,攻击者是可以读取到系统的任意文件的。
利用
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/etc/passwd"
}
通过在kid参数中添加相应绝对或者相对路径进行读取敏感文件信息。
2.2 sql注入
原理
kid也可以从数据库中提取数据,这时候就有可能造成SQL注入攻击,通过构造SQL语句来获取数据或者是绕过signature的验证
实例
Webgoat的最后一关,当前是jerry的用户,需要删除tom的用户
这道题是利用sql注入解题,其中kid参数的webgoat_key就是jwt加密的secret,我们的目的就是利用kid的注入,让secret可控,进而可以生成合法token
首先kid参数可以变成:a' union select 'VG9t' from information_schema.tables; --
这样sql在回显的时候就是显示联合查询中的内容,然后代码中会进行一次base64解码,所以最后通过这个kid查出来的secret就是可控的,再利用这个密钥重新生成twt即可:
2.3 命令注入
原理
对kid参数过滤不严也可能会出现命令注入问题,但是利用条件比较苛刻。如果服务器后端使用的是Ruby,在读取密钥文件时使用了open函数,通过构造参数就可能造成命令注入。
利用
"/path/to/key_file|whoami"
3. JKU/X5U参数
原理
JKU的全称是"JSON Web Key Set URL",用于指定一组用于验证令牌的密钥的URL。类似于kid,JKU也可以由用户指定输入数据,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定web应用使用该组密钥来验证token。
利用
{
"typ": "JWT",
"jku": "http://xxx.com:8000/jwks.json",
"alg": "HS256"
}
4. 密钥可爆破
原理
使用弱密钥进行加密,可利用工具进行爆破。
JWT 的密钥爆破需要在一定的前提下进行:
a) 知悉JWT使用的加密算法
b) 一段有效的、已签名的token
c) 签名用的密钥不复杂(弱密钥)
实例
拿到一个token
这道题token已过期,修改时间戳后,还需要改变用户名,再用secret重新生成才可以,所以先利用工具爆破密钥。
根据代码,是在数组中随机选择一个作为密钥:
把5个secret作为字典跑一下:
跑出key为:washington
然后修改字段后重新生成token:
参考
https://www.cnblogs.com/Secde0/p/13968608.html