28 février 2024 CVE Nicolas D.

D'un warning à une CSRF impactant plus de 3 millions de sites

WordPress est un outil fascinant. Sorti en 2003, il y a plus de 20 ans, il a su devenir rapidement le CMS (Content Management System) le plus utilisé, et de loin. Pas moins de 43% des sites sur Internet l'utilisent.

Chez Patrowl, nous réalisons de tests d'intrusion automatisés à grande échelle avec près de 18.000 assets testés en continue et plus de 200.000 assets en reconnaissance passive et ce que nous observons confirme cette statistique : WordPress est utilisé partout ! Que ce soit pour des sites institutionnels, des blogs ou des plateformes de e-commerce, WordPress est aujourd'hui connu pour sa grande polyvalence. Cela n'est possible que grace à son fameux système de plugins.

WordPress est conçu de telle sorte que des plugins peuvent interagir avec tout son fonctionnement, notamment en utilisant la notion d'actions, de filtres ou de blocs. En utilisant ces éléments, un plugin peut complètement changer l'affichage ou le comportement de WordPress. Au passage, il peut également très significativement réduire son niveau de sécurité.

À la recherche d'une cible

En tant que pentester, nous aimons tout particulièrement nous intéresser aux sites WordPress et vérifier la sécurité des plugins installés. Obtenir la liste des plugins d'un site est simple grâce à notre solution unique de scans qui, au delà de nous retourner des vulnérabilités, nous remonte également des informations précieuses sur les technologies utilisées.

Il y a peu, nous avons reçu une alerte liée à un warning PHP sur un site WordPress :

Ce type de comportement est intriguant et potentiellement indicateur d'un défaut important. Nous avons donc une cible à étudier : UpdraftPlus avec plus de 3 millions d'installations actives.

CVE-2023-5982 - UpdraftPlus <= 1.23.10 - Cross-Site Request Forgery to Google Drive Storage Update

Description

Le plugin UpdraftPlus permet aux utilisateurs de gérer simplement les backups de sites WordPress. C'est un plugin largement utilisé avec plus de 3 millions d'installations. Afin de gérer ces backups, il est possible de lier le plugin à un compte Google pour automatiquement envoyer les backups sur Google Drive.

En analysant la sécurité de ce plugin, Patrowl a identifié qu'il est vulnérable à une attaque CSRF (Cross Site Request Forgery) qui pourrait permettre à un attaquant de configurer son propre compte Google pour le stockage distant des backups. Ainsi, tous les futurs backups du site victime seraient envoyés sur un Google Drive contrôlé par l'attaquant, conduisant à la compromission de données sensibles et à la potentielle compromission totale du site WordPress. En effet, selon la configuration du plugin, les backups peuvent contenir tous les fichiers de l'installation, y compris ses paramètres et le contenu de la base de donnée.

Analyse du code source

Durant la phase d'initialisation du plugin, la fonction handle_url_actions est associée à l'action init:

// class-updraftplus.php L.126
add_action('init', array($this, 'handle_url_actions'));

Cette fonction récupère la valeur du paramètre action dans l'URL et vérifie si elle est au format suivant :

updraftmethod-[method_name]-[call_method]

Elle s'assure ensuite que la méthode [method_name] est définie et que l'utilisateur est autorisé à gérer le plugin UpdraftPlus. Cette dernière vérification protège efficacement contre un attaquant anonyme tentant de déclencher l'execution du reste de la fonction.

Toutefois, aucune protection contre les attaques CSRF (Cross-Site Request Forgery) n'est implémentée. L'exploitation de ce type de vulnérabilité nécessite qu'un attaque parvienne à faire en sorte qu'un utilisateur authentifié avec des privilèges suffisant visite un site sous son contrôle. Si c'est le cas, le site malveillant enverra alors une requête au site victime en usurpant la session du l'utilisateur.

Savoir qu'il est possible d'exploiter une CSRF sur ce point d'entrée est certes intéressant, voyons maintenant ce qu'il est possible d'en faire !

Dans la suite du code, après avoir fait quelques vérifications, le plugin appel la méthode action_[call_method] de la classe présente dans le fichier /methods/[method_name].php. Il y a un certain nombre de classes présentes dans ce dossier, mais l'une d'entre elle est particulièrement intéressante : /methods/googledrive.php. En effet, elle contient la méthode action_auth suivante :

// /methods/googledrive.php L.31
public function action_auth() {
    if (isset($_GET['state'])) {

        $parts = explode(':', $_GET['state']);
        $state = $parts[0];

        if ('success' == $state) {

            if (isset($_GET['user_id']) && isset($_GET['access_token'])) {
                $code = array(
                    'user_id' => $_GET['user_id'],
                    'access_token' => $_GET['access_token']
                );
            } else {
                $code = array();
            }

            $this->do_complete_authentication($state, $code);

        } elseif ('token' == $state) {
            $this->gdrive_auth_token();
        } elseif ('revoke' == $state) {
            $this->gdrive_auth_revoke();
        }
    } elseif (isset($_GET['updraftplus_googledriveauth'])) {
        // [...]
    }
}

Si les paramètres GET state, access_token et user_id sont définis et au bon format, la méthode do_complete_authentication est appelée avec les arguments suivants :

  • state = "success:[instance_id]"
  • code = array('user_id': [user_id], 'access_token': [access_token])

Voici le code de cette fonction :

// methods/googledrive.php L.448
public function do_complete_authentication($state, $code, $return_instead_of_echo = false) {
    
    // If these are set then this is a request from our master app and the auth server has returned these to be saved.
    if (isset($code['user_id']) && isset($code['access_token'])) {
        $opts = $this->get_options();
        $opts['user_id'] = base64_decode($code['user_id']);
        $opts['tmp_access_token'] = base64_decode($code['access_token']);
        // Unset this value if it is set as this is a fresh auth we will set this value in the next step
        if (isset($opts['expires_in'])) unset($opts['expires_in']);
        // remove our flag so we know this authentication is complete
        if (isset($opts['auth_in_progress'])) unset($opts['auth_in_progress']);
        $this->set_options($opts, true);
    }

    if ($return_instead_of_echo) {
        return $this->show_authed_admin_success($return_instead_of_echo);
    } else {
        add_action('all_admin_notices', array($this, 'show_authed_admin_success'));
    }
}

La fonction se contente d'enregistrer les valeurs de user_id et access_token fournies par l'utilisateur dans les options du plugin. Ces valeurs seront ensuite utilisées chaque fois qu'un backup est effectué pour sauvegarder les données sur le Drive associé aux identifiants.

Le défaut important de cette fonction est que son premier argument, la variable state, n'est jamais utilisée et aucune vérification n'est effectuée pour vérifier sa valeur. En effet, cette variable devrait être au format suivant : success:[instance_id]instance_id est une valeur aléatoire unique pour chaque installation du plugin. Cette valeur étant inconnue d'un attaquant, si elle était vérifiée, cela rendrait impossible l'exploitation d'une attaque CSRF.

Exploitation

Pouvoir manipuler des options du plugin au moyen d'une attaque CSRF est une chose, mais concrètement, comment exploiter efficacement cette attaque ?

Après que les options contenant les identifiants Google sont enregistrées par la fonction do_complete_authentication, le plugin utilise ces informations pour se connecter au compte Google et, si le plugin est configuré pour envoyer les backups sur Google Drive, les prochains backups seront envoyés sur le Google Drive associé.

De ce fait, afin d'exploiter cette vulnérabilité, un attaquant doit dans un premier temps générer un jeton d'accès valide donnant accès à son compte Google Drive. La manière la plus simple de faire cela est d'installer le plugin UpdraftPlus sur un instance locale de WordPress, d'aller dans les paramètres du plugin, d'activer le stockage sur Google Drive et d'effectuer l'authentification sur un compte Google valide. Suite à cela, le plugin redirige automatiquement vers une page hébergée sur auth.updraftplus.com contenant un lien vers l'instance locale de WordPress au format suivant : https://[domain]/wp-admin/options-general.php?action=updraftmethod-googledrive-auth&state=success%3A[instance_id]&access_token=[access_token]&user_id=[user_id]

Afin d'exploiter la CSRF, il suffit de changer le nom de domaine du lien pour le faire diriger vers un site vulnérable et de faire en sorte qu'un administrateur authentifié sur ce site y accède.

Si l'attaque est un succès, l'attaquant recevra les prochains backups sur son compte Google Drive. Il aura potentiellement accès à la base de donnée et aux paramètres de l'installation.

Correction

Voici le correctif implémenté par l'éditeur dans la fonction handle_url_actions:

// If we don't have an instance_id but the state is set then we are coming back to finish the auth and should extract the instance_id from the state
if ('' == $instance_id && isset($state) && false !== strpos($state, ':')) {
    $parts = explode(':', $state);
    $instance_id = $parts[1];
}

if (!preg_match('/^[-A-Z0-9]+$/i', $instance_id)) die('Invalid input.');
if (empty($storage_objects_and_ids[$method]['instance_settings'][$instance_id])) {
    error_log("UpdraftPlus::handle_url_actions(): no such instance ID found in settings.");
    return;
}

Si la variable state est fournie par l'utilisateur, la valeur de instance_id contenu est comparée à l'identifiant d'instance de l'installation. Si ces valeurs ont différentes, l'action est bloquée. instance_id étant une valeur non connue par un attaquant anonyme, il s'agit d'une protection efficace contre la vulnérabilité.

Conclusion

Est-ce que cette vulnérabilité est simple à exploiter ? Clairement pas, comme toute attaque CSRF, quand bien même cela reste envisageable sur un plugin avec plus de 3 millions d'installations actives. Mais cette découverte montre bien que le développement d'un plugin WordPress sécurisé est une tâche difficile. Même si les permissions sont correctement vérifiées, les développeurs doivent s'assurer qu'aucune attaque CSRF n'est possible et, si une protection est en place, s'assurer qu'il n'est pas possible de faire fuiter le jeton de protection !

Chez Patrowl, on aime les tests d'intrusion automatisés et continus. Notre solution unique de scan et de tests automatisé nous guide dans la conduite de tests manuels afin de toujours remonter des vulnérabilités pertinentes et utiles à nos clients. Nous allons continuer de chercher de nouvelles vulnérabilités, toujours guidés par les résultats que nous obtenons sur les assets de nos clients. Alors reste attentif à de futurs articles !