12 septembre 2023 CVE Auteur : Florent

Démystifier un RCE dont le CVSSv3 est 10.0 CVE-2020-35489

WordPress est partout autour de nous

Je pense qu'en lisant cet article, vous êtes probablement conscient que Wordpress et ses plugins occupent une place très importante dans le paysage de la surface d'attaque externe. Lors de la surveillance de notre client, nous avons plus de 100 Wordpress, associés à plus de 1,5k différents plugins constamment supprimés et renouvelés, ce qui soulève à nouveau un nouveau défi dans notre automatisation.

La détection de tous les plugins avec leur version associée a été la première étape de base que nous avons rapidement automatisée pour notre client. Nous voulons être sûrs que notre solution sera capable de détecter tous les plugins installés, toutes les versions, et bien sûr toutes les vulnérabilités déjà connues et publiées sur les plugins.

Mais regardons la réalité des chiffres en face. Avec plus de 6 600 plugins vulnérables et plus de 11k CVE associées, vous pouvez rapidement faire le calcul : avertir constamment notre client de tous les CVE liés à tous ses plugins deviendra rapidement un gâchis, et, comme toujours, impossible à patcher, surtout lorsque vous devez gérer 7k autres actifs exposés sur internet.

Le choix, le problème c'est le choix

Comme toujours, l'établissement de priorités est la clé. Nos algorithmes sélectionnent soigneusement les vulnérabilités qui ont de fortes chances d'être rapidement exploitées par les attaquants.

Votre première pensée sera "Ok, facile, ils ne trient probablement que les vulnérabilités CVSS les mieux classées, quel est le problème ?". Bien sûr, les vecteurs CVSS sont le premier indice et l'un des premiers éléments pris en compte pour notre classification, mais encore une fois, au regard du nombre de CVE exposés et de leur exploitation potentielle, ce serait une perte de temps que de ne pas utiliser une technique spécifique.

Alors, quelle est la magie ? 😜

Suivre le lapin blanc

Prenons un exemple simple. Je pense que si vous utilisez le plus simple des scanners web, vous avez probablement déjà rencontré la fameuse vulnérabilité "WordPress Plugin Contact Form Arbitrary File Upload on version < 5.3.2", Si ce n'est pas le cas, c'est un parfait exemple de la façon dont vous pouvez perdre votre temps sur des questions inutiles.

D'après notre expérience, le plugin est largement utilisé et la plupart des scanners web incluent la vérification de cette CVE en raison de son classement : elle a été classée comme "critique" presque partout. Vous pouvez vérifier cette vulnérabilité en la recherchant sur Google : https://www.google.com/search?q=WordPress+Plugin+Contact+Form+Arbitrary+File+Upload

En consultant les premiers sites web et scanners, qu'ils soient open source ou commerciaux, vous constaterez que, selon le scanner, vous aurez :

  • une gravité différente, de "élevée" à "critique
  • un score CVSS différent
  • différentes conditions préalables à l'exploitation
  • Différentes méthodes de détection
  • ...

Ok, maintenant je vois que vous êtes confus. Et n'oubliez pas qu'il s'agit d'une seule vulnérabilité sélectionnée parmi les 11 000 CVE référencées sur les plugins Worpdress. Et vous n'avez encore rien vu.

L'ignorance est une bénédiction

Maintenant que la CVE est presque partout, et qu'il a été relevé, je pense, dans la plupart des équipes de sécurité utilisant des scans de sécurité de base, examinons de plus près la vulnérabilité et l'exploitation possible. Un téléchargement de fichier non authentifié et sans restriction sur un formulaire Wordpress est une bénédiction pour nous et pour l'attaquant, nous devons donc creuser un peu plus.

L'article provient d'ici : https://www.jinsonvarghese.com/unrestricted-file-upload-in-contact-form-7/ ou vous pouvez également le trouver ici : https://blog.wpsec.com/contact-form-7-vulnerability/. Il existe également un autre code d'exploitation qui est légèrement différent : https://packetstormsecurity.com/files/160630/WordPress-Contact-Form-7-5.3.1-Shell-Upload.html

Le schéma d'exploitation semble assez simple : si vous avez un formulaire de contact avec une fonctionnalité de téléchargement de fichier configurée, vous pouvez simplement utiliser une double extension séparée par un caractère spécial (ou un caractère Unicode) pour contourner le contrôle de sécurité et télécharger un fichier PHP comme en 1999. Essayons-le.

Donc, comme décrit précisément dans l'article, nous intégrons la version vulnérable du plugin (5.3.1) dans nos laboratoires, créons un formulaire d'upload valide (cela nécessite d'avoir le mail configuré correctement sur WordPress), et créons un article en utilisant le formulaire de contact que nous venons de créer, aucune autre configuration n'a été mise en place :

Comme vous pouvez le voir, tout d'abord, Pentester n'est clairement pas un designer.

Maintenant, essayons d'exploiter la vulnérabilité et d'avoir notre RCE tant désiré. Le premier test avec un fichier .php a directement levé une erreur avec un type incorrect, bonne nouvelle.

Maintenant, vérifions avec les caractères spéciaux non imprimables (tabulation, octet nul, espaces non imprimables, caractères Unicode etc etc), la réponse :

Très bien ! Cela semble fonctionner parfaitement. Maintenant vérifions notre dossier d'upload par défaut concernant l'article, par défaut c'est : WPCF7_UPLOADS_TMP_DIR qui est par défaut dans wp-content/uploads/wpcf7_uploads :

Rien ici. Hmm. C'est décevant.

Il n'y a pas de cuillère

Regardons de plus près le code source. Le composant dit "vulnérable" se trouve dans le fichier de formatage, dans le nom du fichier wpcf7_antiscript_file_name :

function wpcf7_antiscript_file_name( $filename ) {

    $filename = wp_basename( $filename );
    $parts = explode( '.', $filename );

    if ( count( $parts ) < 2 ) {
        return $filename;
    }

    $script_pattern = '/^(php|phtml|pl|py|rb|cgi|asp|aspx)\d?$/i';

    $filename = array_shift( $parts );
    $extension = array_pop( $parts );

    foreach ( (array) $parts as $part ) {
        if ( preg_match( $script_pattern, $part ) ) {
            $filename .= '.' . $part . '_';
        } else {
            $filename .= '.' . $part;
        }
    }

En lisant l'article, il semble évident que vous pouvez contourner la vérification de l'extension :

1.  Si un utilisateur malveillant télécharge un fichier dont le nom contient des extensions doubles, séparées par un caractère non imprimable ou spécial, par exemple un fichier appelé test.php .webp (le caractère \t est le séparateur).
2.  Contact Form 7 ne supprime pas les caractères spéciaux du nom de fichier téléchargé et analyse le nom de fichier jusqu'à la première extension, mais rejette la seconde à cause du séparateur. Ainsi, le nom de fichier final deviendra test.php (voir l'image ci-dessous).

Ok, la deuxième phrase est intéressante, "mais écarte la deuxième à cause du séparateur".

La première vérification consiste à analyser la taille de $parts qui est un éclaté du nom de fichier $filename en utilisant les séparateurs ".". Vous pouvez ajouter autant de caractères spéciaux que vous voulez, l'explosion verra toujours votre deuxième "." et la vérification ne pourra pas être contournée.

J'ai volontairement pris une très vieille version de PHP au cas où nous manquerions une vulnérabilité massive dans les trois dernières décennies.

Donc, la première vérification n'a pas pu être contournée avec un caractère Unicode. Peut-être parlent-ils de l'autre boucle qui ajoutera un '_' comme protection sur les extensions dangereuses ? Essayons :

D'accord, l'insertion d'un caractère non imprimable à la fin de l'extension nous permet de contourner l'ajout des caractères de protection "_". Cependant, le caractère non imprimable est toujours considéré comme faisant partie de l'extension finale. Pour que cela fonctionne, il faut que la fonction permettant de déplacer le fichier malveillant dans le répertoire WPCF7_UPLOADS_TMP_DIR soit vulnérable en sautant les caractères non imprimables.

D'accord. Maintenant, nous avons potentiellement notre chemin d'exploitation. C'est passionnant !

Mais attendez, il semble que l'article ait légèrement oublié une partie de la fonction, faisons défiler l'article un peu plus bas.

if ( preg_match( $script_pattern, $extension ) ) {
        $filename .= '.' . $extension . '_.txt';
    } else {
        $filename .= '.' . $extension;
    }

C'est intéressant. Maintenant dans un mot imaginaire où vous avez réussi à passer le premier contrôle, vous aurez systématiquement la dernière extension ajoutée à la fin de votre fichier. Vous pouvez ajouter une double extension et .webp si vous voulez, mais à la fin de la fonction vous aurez toujours la dernière (bonne extension) ajoutée et stockée dans le système de fichiers de la victime :

There is no way you can bypass the final extension here.

Il n'y a aucun moyen de contourner l'extension finale ici.

La seule possibilité pour que cette fonction renvoie un chemin vulnérable "potentiel" est d'insérer un nom de fichier se terminant par ".php "+caractères non imprimables et d'espérer que notre fonction de stockage ignore les caractères non imprimables (comme le nullbyte dans ... 2005 : https://bugs.php.net/bug.php?id=39863)){:target="_blank"} )

Il semble que l'exploit de PacketStorm fonctionne de la même manière : pas de double extension, utilisation d'un simple type d'extension .php+u00xx.

Mais pourquoi diable mentionnent-ils une double extension ? Vous allez être surpris.

How deep the rabbit hole goes

Lors de nos tests, nous n'avons jamais réussi à atteindre la fonction vulnérable wpcf7_antiscript_file_name en utilisant une extension simple ou double se terminant par " .php ". Toujours la même erreur, différente de l'erreur de double extension avec php en premier lieu : validation_failed.

Cette erreur semble indiquer qu'il y a, quelque part, un autre contrôle sur le type. Un rapide coup d'œil sur le code nous permet de voir qu'il n'y a pas un mais plusieurs contrôles effectués avant même d'atteindre la fonction "vulnérable". Premièrement :

/* File type validation */

    $file_type_pattern = wpcf7_acceptable_filetypes(
        $tag->get_option( 'filetypes' ), 'regex'
    );

    $file_type_pattern = '/\.(' . $file_type_pattern . ')$/i';

    if ( empty( $file['name'] )
    or ! preg_match( $file_type_pattern, $file['name'] ) ) {
        $result->invalidate( $tag,
            wpcf7_get_message( 'upload_file_type_invalid' )
        );

Par défaut, le $file_type_pattern est égal à : webp|webp|webp|gif|pdf|doc|docx|ppt|pptx|odt|avi|ogg|m4a|mov|mp3|mp4|mpg|wav|wmv.

Ainsi, toute autre extension sera automatiquement refusée. C'est probablement pour cela que l'on parle de la fameuse double extension pour contourner cette vérification. Mais comme indiqué ci-dessous, la dernière extension sera toujours ajoutée à la fin du fichier.

Vous en voulez plus ? Si par miracle, vous avez réussi à contourner toutes les fonctions de sécurité mises en place, le fichier n'est pas stocké directement dans le répertoire WPCF7_UPLOADS_TMP_DIR, mais dans un répertoire aléatoire, généré avec un nom aléatoire de 10 chiffres que l'attaquant devra alors deviner :

wpcf7_init_uploads(); // Confirm upload dir
$uploads_dir = wpcf7_upload_tmp_dir();
$uploads_dir = wpcf7_maybe_add_random_dir( $uploads_dir );

//the function
function wpcf7_maybe_add_random_dir( $dir ) {
    do {
        $rand_max = mt_getrandmax();
        $rand = zeroise( mt_rand( 0, $rand_max ), strlen( $rand_max ) );
        $dir_new = path_join( $dir, $rand );
    } while ( file_exists( $dir_new ) );

    if ( wp_mkdir_p( $dir_new ) ) {
        return $dir_new;
    }

    return $dir;
}

Vous en voulez plus ? Après la vérification précédente, la fonction wp_unique_fileneame() est appelée sur le nom de fichier.

Et que fait cette fonction ? Pas de chance, elle remplace les caractères non imprimables par des caractères "-" depuis, je crois, le début de Wordpress. Exemple de fichier téléchargé avec des caractères non imprimables ou d'autres caractères spéciaux :

Encore une fois ? Un .htaccess est systématiquement créé pour chaque upload, et refuse tout accès aux fichiers dans le répertoire d'upload.

function wpcf7_init_uploads() {
    $dir = wpcf7_upload_tmp_dir();
    wp_mkdir_p( $dir );

    $htaccess_file = path_join( $dir, '.htaccess' );

    if ( file_exists( $htaccess_file ) ) {
        return;
    }

    if ( $handle = fopen( $htaccess_file, 'w' ) ) {
        fwrite( $handle, "Deny from all\n" );
        fclose( $handle );
    }
}

Un autre dernier mot ? Disons la glace sur le gâteau.

Une fois envoyé, le fichier et les dossiers associés sont ... immédiatement supprimés.

$this->remove_uploaded_files();

Ah.

Bienvenue dans le désert du réel

Maintenant que vous avez compris, voici comment la vulnérabilité n'a pas pu être exploitée :

  • Un administrateur a installé Contact-Form dans la version 5.3.1
  • Il a créé un formulaire Contact-Form incluant une fonctionnalité de téléchargement de fichier (File Upload)
  • Il a conservé la valeur par défaut de WPCF7_UPLOADS_TMP_DIR afin que l'attaquant puisse la deviner.

L'attaquant a ensuite réussi à

  • Remplacer le fichier .htaccess créé par défaut sur WPCF7_UPLOADS_TMP_DIR par "Allow from all" (Autoriser de tous)
  • Modifier ou contourner la liste de la fonction wpcf7_acceptable_filetypes() afin de pouvoir inclure une extension .phpXX
  • Supprimer ou contourner l'appel à la fonction wp_unique_filename() pour qu'il puisse inclure des caractères non imprimables.
  • Le nom de fichier est ensuite transmis à la fonction "vulnérable" wpcf7_antiscript_file_name et la vérification par regex de la sécurité de php est contournée.
  • Le fichier move_uploaded_file est appelé avec une extension .php+u0009 par exemple.
  • Le fichier est stocké dans le système de fichiers dans un répertoire aléatoire pendant une milliseconde, et le +u0009 est ignoré par la fonction move_uploaded_file de sorte que nous avons notre fichier .php malveillant stocké.
  • Trouver un moyen, en une milliseconde, de trouver le dossier temporaire aléatoire à 10 chiffres et d'atteindre le fichier php.
  • Ou réussir à éviter l'appel à la fonction remove_uploaded_files() et forcer brutalement le dossier...

Quelle vérité ?

Cette vulnérabilité apparaît sur quelques sources d'informations qui pourraient probablement permettre à des équipes de sécurité perdues de comprendre qu'elle n'est pas exploitable, Wordfense indique que "Notre équipe n'a pas été en mesure de reproduire ce problème, ce qui nous amène à penser qu'il s'agit d'une attaque très complexe ou d'une configuration spéciale requise.", et un pentester demande quelques précisions sur un commentaire mais sans aucune réponse de l'auteur : https://blog.wpsec.com/contact-form-7-vulnerability/

Cela révèle l'un des nombreux problèmes majeurs que posent les vulnérabilités non qualifiées :

  • Premièrement, la CVE n'aurait jamais dû être attribué, ou avec le score CVSS le plus bas, car il n'est pas exploitable dans 100 % des cas.
  • L'exploit n'a jamais été contesté ou testé correctement avant d'être mis en œuvre dans des scanners web ou globaux, puis signalé aux équipes de sécurité.
  • La CVSS n'a jamais été remis en question, modifié, rejeté ou même supprimé au cours des trois dernières années.

Imaginez maintenant le temps et l'argent perdus par les équipes de sécurité, demandant aux agences web ou aux fournisseurs de patcher, suite à la correction de la vulnérabilité dite "critique" avec le SLA associé et le stress que cela implique...

Bien sûr, les clients de Patrowl n'ont pas ce genre de problème. La vulnérabilité n'est pas levée, le seul cas qui a été automatisé est une notification de durcissement si un formulaire Contact-Form est détecté sur une version de plugin vulnérable, non pas parce qu'il pourrait être exploitable, juste parce que les plugins doivent être mis à jour, mais il n'y a absolument aucune urgence et mieux à faire avant cela.

Note : même le développeur du plugin a parfois perdu en correction. Il a ajouté une regex inutile à la fonction qui supprime les caractères non imprimables : $filename = preg_replace( '/[\pC\pZ]+/iu', '', $filename ). Ce qui n'apporte aucune amélioration du niveau de sécurité des plugins.

Ref:
https://blog.wpsec.com/contact-form-7-vulnerability/
https://blog.wpsec.com/contact-form-7-vulnerability/
https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/contact-form-7/contact-form-7-531-arbitrary-file-upload-via-bypass
https://www.jinsonvarghese.com/unrestricted-file-upload-in-contact-form-7/
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-35489
https://wpscan.com/vulnerability/10508