QNAP QSA-21-25 : Helpdesk - Un simple utilisateur sans privilèges peut devenir administrateur du NAS
Historique (DD/MM/YYYY)
- 04/12/2020 : Rapport envoyé à l’équipe sécurité de QNAP
- 07/12/2020 : QNAP confirme la réception du rapport
- 30/01/2021 : Mise à jour demandée sur l’avancée de la résolution
- 01/02/2021 : QNAP confirme la vulnérabilité
- 08/04/2021 : La correction est publiée (Helpdesk 3.0.4)
- 11/06/2021 : Le bulletin de sécurité est publié sur le site de QNAP
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.
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.
Exploitation
Pour exploiter cette vulnérabilité, nous devons suivre quelques étapes :
- Se connecter avec un utilisateur valide
- Accéder à l’URL /apps/qdesk/api/v1/remote_access/getkeys pour générer un nouveau compte de support.
- Accéder à /apps/qdesk/api/v1/remote_access/enable pour activer ce compte.
- A cette étape, le compte de support est utilisable dans l’interface web.
- Ajouter l’utilisateur _qnap_support au groupe ssh
- Se connecter à SSH avec cet utilisateur
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 à :
- /apps/qdesk/api/v1/remote_access/getkeys.
- /apps/qdesk/api/v1/access_remote/enable
Ensuite, l’URL “enable” a besoin de deux paramètres POST :
- ticketId
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 :
- Vérifier les permissions d’administration sur les routes requises
- Vérifier le format ticketId avec une regex