环境搭建
下载镜像
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的时候要注意。