Dans les scénarios d’infection de masse, notre équipe de recherche sur les logiciels malveillants recherche souvent des vecteurs d’attaque pour trouver des modèles et d’autres similitudes entre les sites Web compromis. L’identification de ces modèles nous permet de fournir des solutions meilleures et plus rapides à nos clients, minimisant ainsi les effets d’attaques massives.
Récemment, lors d’une enquête de routine, nous avons trouvé un certain nombre de failles de sécurité dans 123Formulaire de contact pour WordPress Version du plugin WordPress <= 1.5.6. Ces vulnérabilités critiques permettent aux attaquants de publier arbitrairement et de livrer des fichiers malveillants sur le site Web sans aucune authentification.
Avec plus de 3000 installations, le plugin 123contactform-for-WordPress a été développé pour aider les propriétaires de sites Web à ajouter des formulaires Web, des sondages, des quiz ou des sondages à partir d’un compte 123FormBuilder à leur site Web ou blog WordPress.
Contournement de la validation via la vérification du plugin
Commençons par analyser le processus de révision des plugins. Dans le script 123contactform-for-wordpress.php, le plugin enregistre le cfp-connect Action pour charger le cfp_connect () Fonction via l’API AJAX:
109 add_action( 'wp_ajax_cfp-connect', 'cfp_connect' ); 110 add_action( 'wp_ajax_nopriv_cfp-connect', 'cfp_connect' ); 111 function cfp_connect() { 112 $cfp_pub_key = $_POST["pk"]; 113 $message = $_POST["message"]; 114 $signature = base64_decode(str_replace(" ", "+", $_POST["signature"])); 115 if(!isset($cfp_pub_key) || $cfp_pub_key=="") { echo cfp_message("Key is not sent",0);exit(); } // Key is not sent 116 $verify = openssl_verify($message, $signature, base64_decode($cfp_pub_key), OPENSSL_ALGO_SHA1); 117 if ($verify == 1) { 118 if(!get_option("123cf_post_public_key")) { 119 add_option("123cf_post_public_key",$cfp_pub_key); 120 } else { 121 update_option("123cf_post_public_key",$cfp_pub_key); 122 } 123 echo cfp_message("WordPress connected",1);exit(); 124 } elseif ($verify == 0) { 125 echo cfp_message("Signature not verified",0);exit(); 126 } else { 127 echo cfp_message("error: " . openssl_error_string(), 0); 128 exit(); 129 } 130 exit(); 131 }
Cette fonction effectue une vérification de signature avec le openssl_verify ()puis vérifie les résultats: 1 si la signature est correcte ou 0 si elle est incorrecte.
Si la vérification réussit, le script vérifie si le Nom de l’option 123cf_post_public_key existe dans la base de données. À partir de là, cela ajoute la valeur de $ cfp_pub_key Variable dans le Valeur d’option Champ ou met à jour sa valeur lorsque le Nom de l’option n’existe pas.
Au début, il n’y a rien de mal avec cette procédure, mais puisque tous les champs sont du openssl_verify () sont reçus sur $ _POST Les attaquants peuvent simplement produire ces valeurs ($ message, $ signature, $ cf_pub_key) contourner les mécanismes de validation et injecter leurs propres Clé publique dans la base de données.
Toute création de poste
Quelques lignes plus loin, une autre action cfp-new-post est enregistré pour charger le cfp_new_post () Fonction via l’API AJAX:
167 add_action( 'wp_ajax_cfp-new-post', 'cfp_new_post' ); 168 add_action( 'wp_ajax_nopriv_cfp-new-post', 'cfp_new_post' ); 169 function cfp_new_post() { 170 if(!cfp_authenticate()) { echo cfp_message("There was an error while trying to authenticate with wordpress",0); exit(); } ...
Avant de décrire le problème, il y a un appel intéressant à la fonction cfp_authenticate () à la ligne 170, qui tente à nouveau de valider la signature via openssl_verify ().
Toutes les variables sont reçues via des requêtes $ _POST, nous avons donc toujours le contrôle sur le flux d’exécution et le résultat de la variable $ verify.
287 function cfp_authenticate() { 288 if(!get_option( "123cf_post_public_key")) { return false; } 289 $cfp_pub_key = get_option( "123cf_post_public_key"); 290 $message = $_POST["message"]; 291 $signature = base64_decode(str_replace(" ", "+", $_POST["signature"])); 292 $verify = openssl_verify($message, $signature, base64_decode($cfp_pub_key), OPENSSL_ALGO_SHA1); 293 return $verify; 294 }
Continuez vers le cfp_new_post () Fonction, le code attribue tous les champs liés aux entrées de publication pour WordPress à différentes variables dans le $ new_post Array, y compris post_author, post_title, et Publier un contenu, Parmi d’autres.
Ces variables sont ensuite insérées dans la zone de publication à l’aide de la fonction WordPress wp_insert_post ().
171 $post_title = strip_tags(rawurldecode($_POST["post_title"])); 172 $post_title = preg_replace("/ /",' ',$post_title); 173 $post_title = stripslashes($post_title); 174 $post_content = rawurldecode($_POST["post_content"]); 175 $post_content = stripslashes($post_content); 176 $post_status = $_POST["post_status"]; 177 $post_category = urldecode($_POST["post_category"]); 178 $post_author = $_POST["post_author"]; 179 $post_format = $_POST["post_format"]; 180 $comments = $_POST["comment_status"]; 181 $comments == "1" ? $comment_status = "open" : $comment_status = "closed"; 182 $post_excerpt = rawurldecode($_POST["post_excerpt"]); 183 $post_excerpt = preg_replace("/ /",' ',$post_excerpt); 184 $post_excerpt = stripslashes($post_excerpt); 185 $post_tags = explode(",",rawurldecode($_POST["post_tags"])); 186 $post_image = str_replace(" ", "+",$_POST["post_image"]); 187 $post_image_name = $_POST["post_image_name"]; … 205 $new_post = array( 206 'post_author' => $post_author, 207 'post_title' => $post_title, 208 'post_content' => $post_content, 209 'post_status' => $post_status, 210 'comment_status' => $comment_status, 211 'post_excerpt' => $post_excerpt, 212 'post_category' => $cat_id_arr 213 ); 214 $post_id = wp_insert_post( $new_post ); 215 if($post_id) { 216 foreach($custom_fields_values as $meta_key=>$meta_value) { 217 add_post_meta($post_id,str_replace("|***|"," ",$meta_key), $meta_value); 218 } 219 set_post_format($post_id, $post_format); 220 wp_set_post_tags($post_id, $post_tags); 221 if(isset($post_image)) { 222 cfp_upload_image($post_id,$post_image,$post_image_name); 223 } 224 echo cfp_message("New post created",1); exit(); 225 } 226 echo cfp_message("There was an error while trying to create new post",0); exit(); 227 }
Tout téléchargement de fichier
Quand vous avez regardé la fonctionnalité cfp_upload_image () à la ligne 222 et j’y ai réfléchi « Il peut y avoir quelque chose ici » Vous avez raison!
Cette fonction est responsable du téléchargement des « images » sur le serveur. En essayant de définir le nom de fichier ($ Nom de fichier) à image.png (Ligne 137) Les attaquants peuvent insérer n’importe quelle valeur dans la variable $ nom_image_post (Ligne 187) et nommez le fichier comme vous le souhaitez. Vous pouvez également utiliser les variables pour modifier le contenu inséré dans le fichier $ post_image (Ligne 186)
Pour réduire le risque de troncature du nom de fichier, les développeurs ont ajouté un moyen intéressant d’enregistrer le fichier sur le serveur en utilisant md5 () pour calculer le hachage du $ Nom de fichier et Micro heure (), qui renvoie l’horodatage Unix actuel en microsecondes.
132 function cfp_upload_image($post_id,$post_image, $post_image_name = null) { 133 $upload_dir=wp_upload_dir(); 134 $upload_path=str_replace( '/', DIRECTORY_SEPARATOR, $upload_dir['path'] ) . DIRECTORY_SEPARATOR; 135 $decoded_img=base64_decode($post_image); 136 if(!$post_image_name) { $filename='image.png'; } else { $filename=$post_image_name; }; 137 $hashed_filename=md5( $filename . microtime() ) . '_' . $filename; 138 $image_upload=file_put_contents( $upload_path . $hashed_filename, $decoded_img ); ... 145 $file = array(); 146 $file['error'] = ''; 147 $file['tmp_name'] = $upload_path . $hashed_filename; 148 $file['name'] = $hashed_filename; 149 $file['type'] = 'image/jpg'; 150 $file['size'] = filesize( $upload_path . $hashed_filename ); 151 $file_return = wp_handle_sideload( $file, array( 'test_form' => false ) ); 152 $file_url = $file_return["file"]; 153 $filetype = wp_check_filetype( basename( $file_url ), null ); 154 $attachment = array( 155 'guid' => $upload_dir['url'] . '/' . basename( $file_url ), 156 'post_mime_type' => $filetype['type'], 157 'post_title' => preg_replace( '/.[^.]+$/', '', basename( $file_url ) ), 158 'post_content' => '', 159 'post_status' => 'inherit' 160 ); 161 $attach_id = wp_insert_attachment( $attachment, $file_url, $post_id ); 162 require_once( ABSPATH . 'wp-admin/includes/image.php' ); 163 $attach_data = wp_generate_attachment_metadata( $attach_id, $file_url ); 164 wp_update_attachment_metadata( $attach_id, $attach_data ); 165 update_post_meta( $post_id, '_thumbnail_id', $attach_id ); 166 }
Le nom du fichier est très difficile à deviner à cause de cette implémentation. Cependant, si l’option de liste de répertoires n’est pas définie correctement sur le serveur Web, les attaquants peuvent facilement rechercher le répertoire de téléchargement pour obtenir le nom du fichier et exécuter son contenu.
Bonus Sidetrack (deviner le nom de fichier de la porte arrière)
Lorsque les attaquants sont déterminés ce qu’ils sont normalement, ils peuvent faire une supposition sauvage et faire plusieurs millions de requêtes pour obtenir le résultat souhaité. Pour comprendre comment cela fonctionne, il faut plonger un peu plus dans le monde Micro heure () Une fonction.
Tel que défini dans Saisie manuelle PHP, Par défaut, Micro heure () renvoie une chaîne de caractères sous la forme « ms sec« , Où seconde est le nombre de secondes depuis l’époque Unix (0:00:00 janvier 1.1970 GMT) et Mme mesure les microsecondes écoulées en secondes – également exprimées en secondes.
En pratique, c’est le résultat de la fonction effectuée:
$ php -r "echo microtime();" 0.01101800 1588386363
Où « 0,01101800 » représente les microsecondes et la deuxième pièce « 1588386363 » l’ère Unix:
$ date --date @1588386363 Fri May 1 23:26:03 -03 2020
Parce que les attaquants peuvent contrôler le $ filename et le récupérer Micro heure () A partir de la requête au moment de l’injection, seules les microsecondes peuvent être devinées. Lors de nos tests, nous avons estimé une moyenne de 2 à 4 millions de requêtes pour que toutes les variables soient correctes.
Conclusion
Cette analyse montre clairement comment les attaquants peuvent utiliser des vulnérabilités logicielles dans 123contactform-for-wordpress pour créer des publications aléatoires et transférer des fichiers malveillants sur le site Web sans authentification.
Malheureusement, toutes ces vulnérabilités peuvent être corrigées et sont le résultat direct d’un manque de vérification des compétences, d’une vérification incorrecte des utilisateurs et de problèmes d’autorisation dans l’écosystème WordPress. Il existe de nombreuses méthodes et mécanismes disponibles pour empêcher ces injections arbitraires, et les développeurs de plugins doivent les utiliser pour s’assurer que les utilisateurs sont protégés contre les vulnérabilités logicielles connues.
Dans ce cas particulier, les propriétaires de plugins n’ont pas publié de correctif pour corriger ces vulnérabilités. Au lieu de cela, ils ont supprimé le plugin du référentiel de plugins WordPress. Pour minimiser les risques et protéger votre environnement, nous recommandons vivement aux propriétaires de sites Web de désinstaller le plugin et de trouver une solution alternative à partir d’une source fiable.