JumpServer CVE-2023-42820 伪随机密码重置漏洞分析及自动化利用实现
前言:JumpServer伪随机密码重置漏洞分析笔记以及漏洞自动化利用的实现
参考文章:https://paper.seebug.org/3043/
参考文章:https://github.com/mbi/django-simple-captcha/blob/master/CHANGES
近期jumpserver官方公布了多个CVE漏洞,分别有如下,这边主要分析下CVE-2023-42820伪随机导致的密码重置漏洞
- JumpServer 重置密码验证码可被计算推演的漏洞,CVE编号为CVE-2023-42820
- JumpServer 重置密码验证码可被暴力破解的漏洞,CVE编号为CVE-2023-43650
- JumpServer 认证用户跨目录任意文件读取漏洞,CVE编号为CVE-2023-42819
- JumpServer 全局开启公钥认证后,用户可以使用公钥创建访问Token的漏洞,CVE编号为CVE-2023-43652
- JumpServer 认证用户开启MFA后,可以使用SSH公钥认证的逻辑缺陷漏洞,CVE编号为CVE-2023-42818
- JumpServer 认证用户连接MongoDB数据库,可执行任意系统命令的远程执行漏洞,CVE编号为CVE-2023-43651
- Jumpserver Session录像任意下载漏洞,CVE编号为CVE-2023-42442
random模块的伪随机问题
首先需要知道的相关语言的random模块的随机数都不是真正的随机数,而是通过算法来进行的伪随机,一般都是通过一个初始化的随机种子来进行生成随机数,而如果使用的初始化种子的值不变的话,那么后续生成的随机数的值和顺序也不变。
这里通过python代码来进行演示,如下图所示,可以看到如果通过相关的初始化随机种子进行生成随机数,后续生成的随机数的值都是相同
这里的话如果我们能控制每次生成随机数的时候的随机种子的话,那么就能预测到随机数生成的值,如下图所示
random模块的伪随机问题在django-simple-captcha的体现
这边如果要看django-simple-captcha的源码的话,记得先通过pip3来进行安装
pip3 install django-simple-captcha==0.5.17 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip3 list
,模块安装如下所示
这边跟到django-simple-captcha源码中的/jumpserver-3.5.3/lib/python3.10/site-packages/captcha/urls.py
,可以看到生成验证码的路由,如下图所示
这边继续跟到django-simple-captcha的captcha_image函数中,可以看到该方法会接受一个key参数,然后进行random.seed(key)
,如下图所示
而这边的key参数实际上访问用户是可以进行控制的,如下图所示
在captcha_image方法中的store = CaptchaStore.objects.get(hashkey=key)
,每次都会在CaptchaStore中取出对应的key参数的store对象,而这个store实际上就是验证码对象。
JumpServer 伪随机密码重置漏洞分析
这边主要先看找回密码的流程是如何的,这边在登陆界面点击忘记密码,可以看到首先加载的接口是有captcha/image
http://139.196.127.88/core/auth/captcha/image/ef4cdd6fa1e1a2f03f4fce009e0d1185329fcfb2/
上面图中的ef4cdd6fa1e1a2f03f4fce009e0d1185329fcfb2
就是上面captcha_image方法中传入的key参数
这边接着继续看apps.users.views.profile.reset.UserForgotPasswordPreviewingView对象,UserForgotPasswordPreviewingView类就是当点击忘记密码的时候触发执行的对象
这里还有个知识点就是关于form_valid方法,form_valid()的作用是将表单验证成功后的数据传送给视图函数中的其他函数进行处理,比如通过form_valid()函数就可以将表单数据存储到数据库中,或者根据表单数据生成一些响应。
这边继续看apps.users.views.profile.reset.UserForgotPasswordPreviewingView的form_valid方法,实际上就是提交表单对username用户名是否存在进行验证和token = random_string(36)
生成一个token,然后带着这个token参数跳转到forgot-password视图,这个视图的处理是apps.users.views.profile.reset.UserForgotPasswordView进行处理的
在UserForgotPasswordView处理之前,需要检验验证码,而这边的验证码是通过apps.authentication.api.password.UserResetPasswordSendCodeApi.create来进行发送的
UserResetPasswordSendCodeApi可以通过发包来进行寻找,我这边测试发送验证码的接口是password/reset-code
,通过搜索可以找到对应的视图处理
对应的处理视图就是UserResetPasswordSendCodeApi类,如下图所示
可以看到生成6个字符作为验证码放入到celery中进行发送,如下图所示
具体是如何缓存这个发送的验证码的可以来到apps.common.utils.verify_code.SendAndVerifyCodeUtil.__send方法中
接着继续回到UserForgotPasswordView,最终处理验证码的正确性是通过UserForgotPasswordView的form_valid方法来进行处理,如下图所示
最终验证过程取出缓存中存储的验证码和输入的验证码进行对比判断是否成功
把apps.authentication.api.password.UserResetPasswordSendCodeApi.create中的的random_string函数实现复制出来进行模拟生成,如下所示
通过上面可知,如果random模块的随机数种子可控,也就是验证码的key可控的话,那么random_string生成的验证码即可控,模拟测试如下所示
这边测试随机数种子为ef4cdd6fa1e1a2f03f4fce009e0d1185329fcfb2
利用的过程中需要注意的地方
以下是利用poc的几个注意的地方
JumpServer多进程负载均衡的情况
这种情况的话就通过单次发包污染key是不太行的,如下图所示,存在多个gunicorn web服务,此时每个gunicorn web服务中的随机数种子都是不同的
上述情况的话就可以通过大量发包来控制jumpserver所有gunicorn web服务的key
random生成的次数
发送随机数种子生成验证码图片之前,captcha模块中的random调用过了几次
一开始是random_string调用了一次来生成验证码图片中的6位数的验证码,但是实际上在生成了6位数的验证码之前captcha模块还调用了多次的random模块
lib/python3.10/site-packages/captcha/views.py的captcha_image方法中继续看,可以看到在生成图片中的旋转的时候也用到了random模块,总共是循环4次
CAPTCHA_LETTER_ROTATION默认是(-35, 35)
生成图片中的噪点noise_functions中也存在random调用
跟过去可以看到("captcha.helpers.noise_arcs", "captcha.helpers.noise_dots")
captcha.helpers.noise_dots中循环次数为for p in range(int(size[0] * size[1] * 0.1)):
size变量在jumpserver配置jumpserver-3.5.3/apps/jumpserver/settings/libs.py
文件中已经进行配置,如下图所示
总结下就是生成验证码之前还有两处会进行调用,分别是旋转图片和图片噪点
验证码的生成
在忘记密码输入的时候,第一次生成的验证码保留(不要作为验证码进行输入),保留该验证码的key即可,因为如果第一次生成的验证码作为输入的话,那么在后续污染key的时候(将第一次生成的验证码的key)是会失败的,原因的第一次生成的验证码作为了输入跳转到了发送邮件的页面,第一个验证码就会被销毁。
所以正确的操作是第一次验证码保留,然后重新刷新一个验证码作为输入即可。
或者遵守这句话即可,如下图所示
自动化实现
post ⻚⾯会有⼀个隐藏值csrfmiddlewaretoken,带着cookie先get访问⼀次, 正则匹配csrf token 然后post提交即可
django-simple-captcha 漏洞修复
参考文章:https://github.com/mbi/django-simple-captcha/blob/master/CHANGES