Jumpserver随机数种子泄露导致账户劫持漏洞(CVE-2023-42820)

环境搭建

下载镜像

wget https://github.com/vulhub/vulhub/tree/master/jumpserver/CVE-2023-42820

版本:v3.6.4

修改config.env文件

启动环境前,修改config.env中DOMAINS的值为你的IP和端口,我这里修改为192.168.85.129:8080

启动环境

docker compose up -d

复现过程

使用了网上的EXPGitHub - tarimoe/blackjump: JumpServer 堡垒机未授权综合漏洞利用, CVE-2023-42442 / CVE-2023-42820 Exploit

成功!

原理分析

random模块的伪随机问题

首先需要知道的相关语言的random模块的随机数都不是真正的随机数,而是通过算法来进行的伪随机,一般都是通过一个初始化的随机种子来进行生成随机数,而如果使用的初始化种子的值不变的话,那么后续生成的随机数的值和顺序也不变。

这里用python演示,可以看到只要初始化了seed的值,那么后续生成的随机数都是固定的,意味着如果我们能控制每次生成随机数的种子的话,那么就能预测到生成的值。

random模块的伪随机问题在django-simple-captcha的体现

django-simple-captcha/view.py

def captcha_image(request, key, scale=1):
    if scale == 2 and not settings.CAPTCHA_2X_IMAGE:
        raise Http404
    try:
        store = CaptchaStore.objects.get(hashkey=key)
    except CaptchaStore.DoesNotExist:
        # HTTP 410 Gone status so that crawlers don't index these expired urls.
        return HttpResponse(status=410)

    random.seed(key)  # Do not generate different images for the same key

这里可以看到captcha_image会接受一个key参数并且把这个key作为random的初始种子

store = CaptchaStore.objects.get(hashkey=key)
//这是一个Django函数,用于从数据库中检索CAPTCHA图像,它使用CapchaStore模型来存储CA[TCHA图像的哈希密钥、挑战和响应

django-simple-captcha/urls.py

urlpatterns = [
    re_path(
        r"image/(?P<key>\w+)/$",
        views.captcha_image,
        name="captcha-image",
        kwargs={"scale": 1},
    )

每当用户访问匹配这个模式的 URL 时,将调用 captcha_image 视图函数,并传入从 URL 中获取的 key 和值为 1 的 scale,\w+是一个正则表达式,匹配一个或多个字母、数字或下划线。当URL为image/123/时,123就会被捕获,并在视图函数中以参数key的形式传入

而这个库把key直接返回给用户作为验证码的索引了,也就是说访问一次验证码图片后,当前进程的全局seed就会被设置为key,用户也可以拿到这个key,就可以利用这个key预测之后生成的随机数。

jumpserver重置验证码时会向邮箱发送包含随机字符串验证码的一封邮件 (因为代码上的问题 没有正确配置邮件服务器时也能正常生成验证码)

在这里我们可以通过拿到的key预测这个验证码

jumpserver/apps/authentication/api/password.py

class UserResetPasswordSendCodeApi(CreateAPIView):
    permission_classes = (AllowAny,)
    serializer_class = ResetPasswordCodeSerializer

    @staticmethod
    def is_valid_user(**kwargs):
        user = get_object_or_none(User, **kwargs)
        if not user:
            err_msg = _('User does not exist: {}').format(_("No user matched"))
            return None, err_msg
        if not user.is_local:
            err_msg = _(
                'The user is from {}, please go to the corresponding system to change the password'
            ).format(user.get_source_display())
            return None, err_msg
        return user, None

    def create(self, request, *args, **kwargs):
        token = request.GET.get('token')
        userinfo = cache.get(token)
        if not userinfo:
            return HttpResponseRedirect(reverse('authentication:forgot-previewing'))

        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        username = userinfo.get('username')
        form_type = serializer.validated_data['form_type']
        code = random_string(6, lower=False, upper=False)
        other_args = {}

        target = serializer.validated_data[form_type]
        query_key = 'phone' if form_type == 'sms' else form_type
        user, err = self.is_valid_user(username=username, **{query_key: target})
        if not user:
            return Response({'error': err}, status=400)

        subject = '%s: %s' % (get_login_title(), _('Forgot password'))
        context = {
            'user': user, 'title': subject, 'code': code,
        }
        message = render_to_string('authentication/_msg_reset_password_code.html', context)
        other_args['subject'], other_args['message'] = subject, message
        SendAndVerifyCodeUtil(target, code, backend=form_type, **other_args).gen_and_send_async()
        return Response({'data': 'ok'}, status=200)
code = random_string(6, lower=False, upper=False)

这里可以看到生成6个字符作为验证码

通过上面可知,如果random模块的随机数种子可知,也就是说验证码可以通过random_string方法推算出来,但是实际上在生成了6位数的验证码之前captcha模块还调用多次random模块

第1处调用:django-simple-captcha/captcha/view.py

    for char in charlist:
        fgimage = Image.new("RGB", size, settings.CAPTCHA_FOREGROUND_COLOR)
        charimage = Image.new("L", getsize(font, " %s " % char), "#000000")
        chardraw = ImageDraw.Draw(charimage)
        chardraw.text((0, 0), " %s " % char, font=font, fill="#ffffff")
        if settings.CAPTCHA_LETTER_ROTATION:
            charimage = charimage.rotate(
                random.randrange(*settings.CAPTCHA_LETTER_ROTATION),
                expand=0,
                resample=Image.BICUBIC,
            )

在生成图片中的旋转时调用了一次

第2处调用调用:django-simple-captcha/captcha/helpers.py

def noise_dots(draw, image):
    size = image.size
    for p in range(int(size[0] * size[1] * 0.1)):
        draw.point(
            (random.randint(0, size[0]), random.randint(0, size[1])),
            fill=settings.CAPTCHA_FOREGROUND_COLOR,
        )
    return draw

在 noise_dots() 函数中,random.randint() 函数被调用了 int(size[0] * size[1] * 0.1) 次。 最短的情况:1-1= namely ['1-', '1', '=']最长的情况:10*10= namely ['1', '0', '*', '1', '0', '='],所以是3-6次

第3处调用:jumpserver/apps/authentication/api/password.py

code = random_string(6, lower=False, upper=False)

所以自己构造EXP的时候要注意。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值