یک آسیب پذیری افزایش امتیاز در پلاگین وردپرسی LiteSpeed Cache گزارش شده که به مهاجم بدون احرازهویت شده امکان دسترسی با سطح ادمین رو میده.
این آسیب پذیری توسط محقق امنیتی John Blackbourn در برنامه ی باگ بانتی Patchstack Zero Day گزارش شده و بالاترین میزان بانتی در تاریخ شکار باگ های وردپرسی رو به خودش اختصاص داده. بانتی این گزارش، 14,400 دلار بوده.
برای شرکت در برنامه های باگ بانتی وردپرسی میتونید از Patchstack و Wordfence استفاده کنید. همچنین یک نسخه از پلاگین های آسیب پذیر که امتیازشون بالای 7 هستن رو بصورت ماهیانه جمع آوری و در قالب وردپرس آسیب پذیر منتشر میکنیم که میتونید از اینجا بهش دسترسی داشته باشید.
این آسیبپذیری با شناسه CVE-2024-28000 مشخص و در نسخه 6.4 اصلاح شده.
بصورت کلی برای اینکه سایتهای وردپرسی سریعتر بالا بیان، از یسری راهکارها استفاده میکنن که یک نمونه اش کش کردن هستش. برای کش کردن هم معمولا از پلاگین ها استفاده میکنن، چون کار باهاشون آسون هستش و امکانات متنوعی رو یکجا، ارائه میدن. یکی از این پلاگین ها، LiteSpeed Cache هستش.
نسخه ی رایگان LiteSpeed Cache، با 5 میلیون نصب فعال، بعنوان محبوب ترین پلاگین کش در وردرپس شناخته میشه.
در زمان نگارش این پست، وضعیت این پلاگین به این صورت هستش:
بررسی آسیب پذیری CVE-2024-28000 :
آسیب پذیری به هر بازدید کننده بدون احراز هویت، امکان دسترسی به سطح ادمین میده. بعد از دسترسی مهاجم میتونه افزونه های مخرب رو آپلود و نصب کنه.
این آسیبپذیری از یک ویژگی شبیه سازی کاربر در افزونه سوء استفاده میکنه که توسط یک هش ضعیف که از مقادیر شناخته شده استفاده میکنه، محافظت میشه.
ویژگی شبیه سازی کاربر (User Simulation) به قابلیتی در نرمافزارها، به ویژه افزونهها، اشاره داره که به یک برنامه یا اسکریپت اجازه میده تا خودش رو به جای یک کاربر واقعی جا بزنه و اقداماتی رو که یک کاربر معمولی انجام میده رو، تقلید کنه. این ویژگی معمولاً برای انجام تستها، خودکارسازی وظایف و یا ایجاد تجربه کاربری شخصیسازی شده استفاده میشه.
LiteSpeed Cache شامل یک ویژگی خزنده (Crawler) هستش که به صورت برنامهریزی شده در سایت شما میخزه، تا کشها رو برای صفحات سایت شما پر کنه.
خزنده میتونه یک کاربر لاگین شده با یک ID مشخص رو شبیهسازی کنه، بنابراین افزونه شامل یک ویژگی شبیهسازی کاربر هستش که مقادیر کوکیهای litespeed_role
و litespeed_hash
رو برای یک ID کاربر و هش امنیتی بررسی میکنه و اگه از یک هش امنیتی معتبر استفاده شده باشه، ID کاربر فعلی رو با استفاده از تابع wp_set_current_user
تنظیم میکنه.
هش امنیتی از ویژگی شبیهسازی کاربر توسط هر چیزی غیر از یک درخواست خزنده معتبر محافظت میکنه. قطعه کد زیر مثالی از نحوه تولید هش امنیتی در متدStr::rrand()
قبل از استفاده توسط عملکرد خزنده رو نشون میده:
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 37 38 39 40 41 42 43 44 45 |
src/str.cls.php, function rrand() public static function rrand($len, $type = 7) { mt_srand((int) ((float) microtime() * 1000000)); switch ($type) { case 0: $charlist = '012'; break; case 1: $charlist = '0123456789'; break; case 2: $charlist = 'abcdefghijklmnopqrstuvwxyz'; break; case 3: $charlist = '0123456789abcdefghijklmnopqrstuvwxyz'; break; case 4: $charlist = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; case 5: $charlist = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; case 6: $charlist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; case 7: $charlist = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; } $str = ''; $max = strlen($charlist) - 1; for ($i = 0; $i < $len; $i++) { $str .= $charlist[mt_rand(0, $max)]; } return $str; } |
متاسفانه این هش امنیتی چندین مشکل امنیتی داره که میشه مقادیر احتمالی اون رو مشخص کرد:
- تولیدکننده عدد تصادفی که هش امنیتی رو تولید میکنه، با قسمت میکروثانیه زمان فعلی عمل Seed رو انجام میده. یعنی تنها مقادیر ممکن برای seed اعداد صحیح ۰ تا ۹۹۹۹۹۹ هستن.
- تولیدکننده عدد تصادفی از نظر رمزنگاری ایمن نیست، به این معنی که اگه Seed شناخته بشه، مقادیر “تصادفی” که تولید میکنه رو میشه بصورت دقیق مشخص کرد.
- هش امنیتی یک بار تولید میشه و در
litespeed.router.hash
در دیتابیس ذخیره میشه. هش با مقداری salt نمیشه و به یک درخواست یا یک کاربر خاص مرتبط نیست. پس از تولید، مقدارش هرگز تغییر نمیکنه. - با توجه به موارد بالا، فقط ۱ میلیون مقدار ممکن برای هش امنیتی وجود داره. این مقادیر شناخته شده و در تمام محیط ها و وبسایتها یکسان هستن. یعنی میشه همه ی این یک میلیون هش رو تولید کرد و روی همه ی سایتها چک کرد تا هش مربوطه مورد استفاده توسط اون سایت رو بدست آورد.
قطعه کد زیر نشون دهنده اینه که چگونه مقدار کوکی litespeed_hash
به عنوان یک شرط محافظ برای فراخوانی تابع wp_set_current_user
که از مقدار کوکی litespeed_role
استفاده میکنه، اعتبارسنجی میشه. این کد در init hook وردپرس فراخوانی میشه و برای همه درخواستها به جز درخواستهای بخش مدیریت فعال میشه.
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 |
src/router.cls.php, function is_role_simulation() public function is_role_simulation() { if (is_admin()) { return; } if (empty($_COOKIE['litespeed_role']) || empty($_COOKIE['litespeed_hash'])) { return; } Debug2::debug('[Router] starting role validation'); // Check if is from crawler // if ( empty( $_SERVER[ 'HTTP_USER_AGENT' ] ) || strpos( $_SERVER[ 'HTTP_USER_AGENT' ], Crawler::FAST_USER_AGENT ) !== 0 ) { // Debug2::debug( '[Router] user agent not match' ); // return; // } // Hash validation $hash = self::get_option(self::ITEM_HASH); if (!$hash || $_COOKIE['litespeed_hash'] != $hash) { Debug2::debug('[Router] hash not match ' . $_COOKIE['litespeed_hash'] . ' != ' . $hash); return; } $role_uid = $_COOKIE['litespeed_role']; Debug2::debug('[Router] role simulate litespeed_role uid ' . $role_uid); wp_set_current_user($role_uid); } |
مقدار هش امنیتی معتبر از litespeed.router.hash
بازیابی و با مقدار موجود در کوکی litespeed_hash
مقایسه میشه. بدلیل مقایسه غیر دقیق (Non-strict) و غیر ثابت زمانی (Non-constant-time) ، این امکان رو میده که هش امنیتی در برابر حملات اجبار (Coercion) یا حمله زمانی (Timing attack) آسیبپذیر باشه، اما در عمل، استفاده از این حملات نسبت به تولید مقادیر شناخته شده هش، نیازمند تلاش بیشتری هستش. (بصرفه نیست)
با توجه به اینکه در کل یک میلیون هش داریم، میتونیم همه ی اونا رو تولید کنیم و به litespeed_hash
ارسال کنیم، تا هش معتبر رو بدست بیاریم.
منظور از مقایسه دقیق و غیر دقیق:
- مقایسه دقیق: در یک مقایسه دقیق، دو رشته کاراکتر به طور کامل و بیت به بیت با هم مقایسه میشن. اگه حتی یک بیت از دو رشته با هم متفاوت باشه، مقایسه نادرست اعلام میشه.
- مقایسه غیر دقیق: در این نوع مقایسه، ممکنه برخی از تفاوتهای جزئی بین دو رشته نادیده گرفته بشه. این نوع مقایسه میتونه به دلایل مختلفی مانند نادیده گرفتن حروف بزرگ و کوچک، فضاهای اضافی یا تفاوت در فرمت بندی انجام بشه.
مقایسه ثابت زمانی و غیر ثابت زمانی:
- مقایسه ثابت زمانی: در این نوع مقایسه، زمان لازم برای مقایسه دو رشته، صرف نظر از اینکه چقدر شبیه به هم هستن، ثابت هستش. این ویژگی برای جلوگیری از حملات زمانی بسیار مهم هستش.
- مقایسه غیر ثابت زمانی: در این نوع مقایسه، زمان لازم برای مقایسه دو رشته بسته به میزان شباهت اونا متفاوت هستش. مثلاً اگه دو رشته از ابتدا تا جایی که یک کاراکتر متفاوت پیدا بشه، کاملاً یکسان باشن، مقایسه سریعتر انجام میشه.
مهاجم میتونه با اندازهگیری زمان لازم برای انجام عملیاتهای مختلف، اطلاعاتی در مورد ساختار دادهها یا الگوریتمهای استفاده شده بدست بیاره. مثلاً در مورد هشها، با اندازهگیری زمان مقایسه یک هش ورودی با هشهای مختلف، میتونه احتمالاً بخشی از هش رو حدس بزنه. به این نوع حملات، حملات زمانی یا Timing Attack میگن.
تا این جای کار مشخص شد که این هش امنیتی ضعیف هستش و بنابراین یک آسیبپذیری رو فراهم میکنه که منجر به فراخوانی تابع wp_set_current_user
میشه. اما نکته ای که وجود داره اینه که برای اینکه این آسیب پذیری فعال بشه، باید ویژگی خزنده در پلاگین LiteSpeed Cache فعال شده باشه. این ویژگی بصورت پیش فرض فعال نیست.
یعنی برای اینکه بتونیم این آسیب پذیری رو اکسپلویت کنیم، باید ویژگی خزنده در پلاگین فعال و یک بار عمل خزیدن انجام شده باشه تا این هش و … تولید شده باشن. بنابراین این میتونه تعداد سایت های آسیب پذیر رو کاهش بده.
اما نکته ای که وجود داره اینه که، این پلاگین باز یک مشکل امنیتی داره که میشه باهاش، بدون فعال بودن ویژگی خزنده، باز این هش امنیتی رو تولید و ذخیره کرد. بنابراین باز همه ی سایتهایی که از این پلاگین استفاده میکنن، آسیب پذیر هستن.
هش در متد Router::get_hash
تولید و ذخیره میشه. این متد توسط برخی متدهای مرتبط با پیکربندی که در طول خزیدن فراخوانی میشن، فراخوانی میشه و جالب هستش که این متدها میتونن در نهایت توسط یک Ajax handler محافظت نشده فعال بشن. متد Task::async_litespeed_handler
به اکشن wp_ajax_nopriv_async_litespeed
هوک شده.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
src/router.cls.php, function get_hash() public static function get_hash() { // Reuse previous hash if existed $hash = self::get_option(self::ITEM_HASH); if ($hash) { return $hash; } $hash = Str::rrand(6); self::update_option(self::ITEM_HASH, $hash); return $hash; } |
یک فراخوانی به /wp-admin/admin-ajax.php?action=async_litespeed&litespeed_type=crawler
توسط هر کاربر بدون احراز هویت، منجر به فراخوانی Router::get_hash و تولید هش امنیتی مورد نیاز میشه. در زیر فراخوانی های زنجیره ای از درخواست اکشن AJAX رو مشاهده میکنید:
- Task::async_litespeed_handler()
- Crawler::async_handler()
- Crawler::start()
- Crawler::_crawl_data()
- Crawler::_engine_start()
- Crawler::_do_running()
- Crawler::_get_curl_options()
- Router::get_hash()
- Str::rrand()
بنابراین این اکشن AJAX رو، اولین قدم در حمله، برای تضمین وجود هش امنیتی، میشه در نظر گرفت.
نکته ای که وجود داره اینه که، این آسیب پذیری، در نسخه های ویندوزی اکسپلویت نمیشه. دلیلش اینه که در قسمتی از تولید هش مسیری مانند این رو داریم:
1 |
_engine_start() -> _adjust_current_threads() -> get_server_load() -> sys_getloadavg() |
این مسیر در نسخه های ویندوزی وجود نداره. یعنی هش در نسخه های ویندوزی ایجاد نمیشه. بنابراین وردپرس هایی که روی ویندوز نصب شدن، تحت تاثیر این آسیب پذیری نیستن.
اکسپلویت آسیب پذیری:
محققا اعلام کردن که اگه یک میلیون هش رو داشته باشیم، میتونیم با حملات بروت فورس و ارسال این هش ها به کوکی litespeed_hash
، با فرض اینکه هر ثانیه 3 درخواست رو ارسال کنیم، میتونیم در عرض چند ساعت یا هفته، به سایت با شناسه کاربری داده شده (User ID)، دسترسی داشته باشیم.
تنها نکته ای که وجود داره اینه که باید شناسه یک کاربر سطح ادمین رو بدونیم و اونو به کوکی litespeed_role
ارسال کنیم. مشخص کردن شناسه این کاربر، بستگی به سایت داره اما در بسیاری از موارد ID=1 جواب میده.
انجام این حمله در قسمت فرانت سایت امکانپذیر هستش، اما لازمه که پاسخ HTML رو بررسی کنیم تا مشخص بشه که آیا هش معتبر هستش و کاربر لاگین کرده یا نه. البته، میتونیم آسیب پذیری رو از طریق REST API تشدید کنیم که در صورت معتبر بودن هش، با یک کد وضعیت HTTP بهتر پاسخ میده.
با ارسال درخواستهای POST به نقطه پایانی /wp/v2/users
از REST API همراه با مقادیر هش در کوکی litespeed_hash
و شناسه کاربر هدف در کوکی litespeed_role
، در صورتیکه هش معتبر باشه، یک حساب کاربری کاملاً جدید سطح مدیر ایجاد میشه.
این حمله بدون نیاز به یک nonce REST API به دلیل فراخوانی تابع wp_set_current_user
که کاربر فعلی رو برای ما در زمینه درخواست REST API تنظیم میکنه موفق خواهد بود.
اگه حمله موفقیت آمیز باشه یک کد وضعیت HTTP 201 دریافت میکنیم، در غیر این صورت یک 401 می گیریم.
اگه ویژگی Debug Log افزونه Litespeed Cache روی ON تنظیم شده باشه، هش امنیتی در فایل debug.log در فولدر wp-content در هر درخواست با یک هش نادرست، نشت داده میشه.
اصلاح آسیب پذیری:
برای اصلاح آسیب پذیری، تیم توسعه دهنده موارد زیر رو اضافه کرده:
- افزودن اعتبارسنجی هش از مقدار
async_call-hash
در تابعRouter::async_litespeed_handler()
- افزودن مقدار
litespeed_flash_hash
یک بار مصرف: یک بررسی هش اضافی که بلافاصله پس از اعتبارسنجی پاک میشه و TTL رو به ۱۲۰ ثانیه تنظیم میکنه. - استفاده از ۳۲ کاراکتر تصادفی برای مقادیر
async_call-hash
،litespeed_flash_hash
وlitespeed_hash
. - برای شبیه سازی نقش خزنده، کد هر بار که خزنده دوباره اجرا میشه، یک هش تولید میکنه و پس از اعتبارسنجی هش، IP درخواست فعلی رو برای اعتبارسنجی بعدی ذخیره میکنه.
برای مشاهده ی همه تغییرات، میتونید اینجا رو ببینید.
علاوه بر موارد بالا، محققای Patchstack هم یسری توصیه در زمینه ی اصلاح آسیب پذیری داشتن:
- ما ابتدا استفاده از تابع
hash_equals
رو برای فرآیند مقایسه مقدار هش توصیه میکنیم تا از حملات زمانی احتمالی جلوگیری کنه. - همچنین توصیه میکنیم از یک تولیدکننده مقدار تصادفی ایمنتر مانند تابع
random_bytes
استفاده کنن. این به دلیل نیاز به پشتیبانی از PHP قدیمی پیادهسازی نشده. بنابراین، امیدواریم که تیم Litespeed همچنان استفاده از یک کتابخانه polyfill رو در نظر بگیره.
در نهایت اگه به کشف آسیب پذیری در وردپرس دارید، میتونید از آکادمی Patchstack استفاده کنید و به رشد اون هم کمک کنید.
جدول زمانی:
- یک آگوست، گزارش John Blackbourn دریافت شده.
- پنج آگوست این آسیب پذیری رو به تیم LiteSpeed ارسال کردن.
- سیزده آگوست، نسخه ی اصلاحیه (6.4) منتشر شده.
- نوزده آگوست، آسیب پذیری به دیتابیس آسیب پذیری های Patchstack اضافه شده.
- بیست و یک آگوست، آسیب پذیری در قالب مقاله منتشر شده.