QNAP QSA-21-25 : Helpdesk - A simple user without privileges can gain administrative access on the NAS

CVE CVE-2021-28814 QNAP QSA-21-25

QTS vQTS 4.4.3.1444 HelpDesk 3.0.3

Timeline (DD/MM/YYYY)

Summary

A simple user can access dangerous endpoints on HelpDesk allowing to create a _qnap_support account, get his password and activate his account on the system. The created account is in the administrators group. SSH access with administrators privileges can be enabled with this account.

Preparation

System and package upgrade

First, we need to ensure that the system and HelpDesk package are up to date.

image-20201204124613872

image-20201204132639249

User Account Creation

To exploit the vulnerability, we need user credentials. We can create a user “exploit” with password “MA7adCdS”. This user doesn’t require any privileges.

image-20201204124936037

Exploitation

To exploit the vulnerability, we need a few steps:

image-20201204131325229

Explanation

The first problem is that dangerous entry points are not restricted to the administrator. Indeed, the user doesn’t need access to :

Then, the “enable” endpoint, need two POST parameters :

The ticketId is verified by making a request on https://help.qnap.com/apps/qdesk_service/api/v1/qpkg/remote/{ticketId}

// www/App/Models/RemoteAccessModel.php
public function getRemoteSessionStatus()
    {
        $curlUrl = 'https://' . config('helpdesk_server') . "/apps/qdesk_service/api/v1/qpkg/remote/{$this->ticketId}";
        $srvOutput = curlWrapper('GET', $curlUrl);
        if (is_array($srvOutput)) {
            $srvOutput = json_encode($srvOutput);
        }
        $srvOutput = json_decode($srvOutput);

        if (isset($srvOutput->status) && $srvOutput->status === 'FALSE') {
            if (isset($srvOutput->result->errCode) && $srvOutput->result->errCode == 28) {
                return array('status' => '408', 'msg' => $srvOutput->result->errMsg);
            } else {
                return $srvOutput->status;
            }
        }

        $ticketStatus = $srvOutput->status;
        if ($ticketStatus == 0) {
            $ticketStatus = $srvOutput->data->status;
        }

        return $ticketStatus;
    }

If the “ticketId” variable contains a new line character, “$srvOutput” will be NULL. All checks in the enable function will fail to catch the error.

// www/App/Controllers/Api/RemoteAccess.php
public function enable()
{       
        [...]
        if (is_array($remoteStatus)) {
            $objRStatus = json_encode($remoteStatus);
            $objRStatus = json_decode($objRStatus);

            if (isset($objRStatus->status)) {
                Response::error($objRStatus->status, apiFormater($objRStatus->msg, 2014));
            }
        }

        if ($remoteStatus == 1001 || $remoteStatus == 1002) {
            $this->fileLogModel->logDebug('Remote access case is already enabled.', __FILE__, __LINE__);
            Response::error(403, apiFormater(null, 2007));
        }

        if ($this->remoteAccessModel->isExist($ticketId, $email) && $remoteStatus == 1004) {
            $this->fileLogModel->logDebug('Remote access case is already expired.', __FILE__, __LINE__);
            Response::error(403, apiFormater(null, 2018));
        }

        if ($this->remoteAccessModel->isExist($ticketId, $email) && $remoteStatus == 1005) {
            $this->fileLogModel->logDebug('Remote access case is already closed.', __FILE__, __LINE__);
            Response::error(403, apiFormater(null, 2009));
        }

        if ($remoteStatus == 1) {
            $this->fileLogModel->logError('Remote access case is not activate.', __FILE__, __LINE__);
            Response::error(404, apiFormater(null, 2010));
        }
    
        // Fix SSHD config permission
        $this->qtsModel->fixSSHDConfigPerm();

        // Generate SSH Keypair
        $privateKey = file_get_contents($this->tmpKeyPath);
        $publicKey = shell_exec("/usr/bin/ssh-keygen -y -f $this->tmpKeyPath");

        $pattern = '/[\\r\\n]/';
        $pkWithTicketID = preg_replace($pattern, '', $publicKey) . ' ' . $ticketId . "\r\n";

        $this->remoteAccessModel->resetAuthedKey();

        file_put_contents($this->authedKeyPath, $pkWithTicketID, FILE_APPEND);
        $base64SK = base64_encode($privateKey);

        // Check and create QTS account
        $tempAccount = $this->remoteAccessModel->getTempAccount();
        $supportID = $tempAccount['tempId'];
        $supportPW = $tempAccount['tempPw'];

        $this->remoteAccessModel->removeQTSUserByAPI();
        if ($this->remoteAccessModel->checkQTSUser()) {
            $this->remoteAccessModel->removeQTSUser();
            if ($this->remoteAccessModel->checkQTSUser()) {
                $this->fileLogModel->logError('Can not remove QTS support account.', __FILE__, __LINE__);
                Response::error(500, apiFormater(null, 2010));
            }
        }

        $this->remoteAccessModel->qtsUserPW = $supportPW;
        $po = $this->remoteAccessModel->createQTSUserByAPI();
        if (!$this->remoteAccessModel->checkQTSUser()) {
            $this->remoteAccessModel->createQTSUser();
            if (!$this->remoteAccessModel->checkQTSUser()) {
                $this->fileLogModel->logError('Can not create QTS support account.', __FILE__, __LINE__);
                Response::error(500, apiFormater(null, 2011));
            }
        }

        //set config to close popup
        $this->remoteAccessModel->setQTSUserConfig();

        // Get NAS http, https and ssh port
        if (empty($httpPort) && empty($sshPort)) {
            $httpPort = shell_exec("/sbin/getcfg 'System' 'Web Access Port' -d 8080");
            $httpsPort = shell_exec("/sbin/getcfg 'Stunnel' 'Port' -d 443");
            $sshPort = shell_exec("/sbin/getcfg 'LOGIN' 'SSH Port' -d 22");
            $httpPort = str_replace("\n", '', $httpPort);
            $httpsPort = str_replace("\n", '', $httpsPort);
            $sshPort = str_replace("\n", '', $sshPort);
        }

The function will crash after this code but the support account is enabled now.

Remediation

To fix these vulnerabilities, a few steps are required: