QNAP QSA-21-25 : Helpdesk - Un simple utilisateur sans privilèges peut devenir administrateur du NAS

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

QTS vQTS 4.4.3.1444 HelpDesk 3.0.3

Historique (DD/MM/YYYY)

Résumé

Un simple utilisateur peut accéder à des fonctionnalités dangereuses sur HelpDesk permettant de créer un compte _qnap_support, obtenir son mot de passe et activer le compte sur le NAS. Le compte créé est dans le groupe administrateurs. L’accès SSH avec les privilèges administrateurs peut être activé avec ce compte.

Préparation

Mise à jour du système et de l’application

Tout d’abord, nous devons nous assurer que le système et le paquet HelpDesk sont à jour.

image-20201204124613872

image-20201204132639249

Création d’un compte utilisateur

Pour exploiter la vulnérabilité, nous avons besoin d’un utilisateur. Nous pouvons créer un utilisateur “exploit” avec le mot de passe “MA7adCdS”. Cet utilisateur n’a pas besoin de privilèges.

image-20201204124936037

Exploitation

Pour exploiter cette vulnérabilité, nous devons suivre quelques étapes :

image-20201204131325229

Détails

Le premier problème est que les fonctionnalités dangereuses ne sont pas réservées à l’administrateur. En effet, l’utilisateur n’a pas besoin d’accéder à :

Ensuite, l’URL “enable” a besoin de deux paramètres POST :

Le “ticketId” est vérifié en faisant une requête sur 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;
    }

Si la variable “ticketId” contient un caractère de nouvelle ligne, “$srvOutput” sera NULL. Toutes les vérifications de la fonction enable ne permettront pas de détecter l’erreur.

// 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);
        }

La fonction se bloque après ce code, mais le compte de support est maintenant activé.

Remédiation

Pour corriger ces vulnérabilités, quelques étapes sont nécessaires :