این ماه که میخواستم نسخه وردپرس آسیب پذیر رو برای جولای آماده میکردم، متوجه شدم که برای جولای بیش از 1200 پلاگین باید جمع آوری و نصب کنم. خب عدد بزرگی بود. برام جالب بود که چرا با این تعداد پلاگین آسیب پذیر روبرو هستم. با بررسی که کردم متوجه شدم، علت اون کشف یه آسیب پذیری در کتابخونه Freemius WordPress SDK هستش.
این کتابخونه به دلیل اینکه در هزاران پلاگین و تم مورد استفاده قرار گرفته ، در نتیجه آسیب پذیر بودنش، کل اونارو تحت تاثیر قرار میده. لیستی از این پلاگینها رو میتونید اینجا مشاهده کنید.
کتابخونه ی Freemius WordPress SDK ، نسخه های 2.5.9 و قبلتر تر ، در بیش از هزاران افزونه و قالب استفاده شده و تخمین میزنن این پلاگین ها و تم ها ، در بیش از 7 میلیون سایت نصب شده . این کتابخونه بعنوان یه کتابخونه شناخته شده برای ادغام سرویس های Freemius با افزونه ها و قالب ها بکار میره.
خود Freemius هم محبوب ترین پلتفرم تجارت الکترونیک برای فروش افزونه ها و تم های وردپرس هستش که به توسعه دهندگان این امکان رو میده تا به سرعت شروع به فروش و صدور لایسنس برای محصولات خودشون بکنن.
آسیب پذیری CVE-2023-33999 ، از نوع reflected XSS هستش و مهاجم احرازهویت نشده امکان سرقت اطلاعات حساس رو داره . مهاجم میتونه با فریب دادن ادمین یا کاربر با امتیاز بالا برای مشاهده یه URL مخرب، امتیازش رو افزایش بده. این آسیب پذیری به دلیل عدم پاکسازی (sanitization and escaping) هستش.
آسیب پذیری نسخه های 2.5.9 و پایین تر رو تحت تاثیر قرار میده و در نسخه 2.5.10 و بالاتر ، اصلاح شده. بنابراین اگه توسعه دهنده هستید، حتما نسخه بروز رو اعمال کنید.
بررسی علت آسیب پذیری :
آسیب پذیری در تابع fs_request_get
هستش :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
includes/fs-core-functions.php, function fs_request_get() function fs_request_get( $key, $def = false, $type = false ) { if ( is_string( $type ) ) { $type = strtolower( $type ); } /** * Note to WordPress.org Reviewers: * This is a helper method to fetch GET/POST user input with an optional default value when the input is not set. The actual sanitization is done in the scope of the function's usage. */ switch ( $type ) { case 'post': $value = isset( $_POST[ $key ] ) ? $_POST[ $key ] : $def; break; case 'get': $value = isset( $_GET[ $key ] ) ? $_GET[ $key ] : $def; break; default: $value = isset( $_REQUEST[ $key ] ) ? $_REQUEST[ $key ] : $def; break; } return $value; } |
اگه به کد بالا دقت کنید، این تابع صرفا یه تابع برای گرفتن پارامترهای درخواست هستش. اما نکته ای که هست این تابع در کل کتابخونه بارها مورد استفاده قرار گرفته. یکی از جاهایی که از اون استفاده شده ، تابع dynamic_init
هستش :
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 |
includes/class-freemius.php, function dynamic_init() -------------------------- CUT HERE -------------------------- if ( $this->is_user_in_admin() ) { if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) { $this->_admin_notices->add_sticky( sprintf( /* translators: %s: License type (e.g. you have a professional license) */ $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ), fs_request_get( 'purchased_plan' ) ) . sprintf( $this->get_text_inline(" The %s's %sdownload link%s, license key, and installation instructions have been sent to %s. If you can't find the email after 5 min, please check your spam box.", 'post-purchase-email-sent-message' ), $this->get_module_label( true ), ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ), ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ), sprintf( '<strong>%s</strong>', fs_request_get( 'purchase_email' ) ) ), 'plan_purchased', $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!' ); } -------------------------- CUT HERE -------------------------- |
در این تابع، fs_request_get( 'purchased_plan' )
و fs_request_get( 'purchase_email' )
، مستقیما بعنوان ورودی تابع this->_admin_notices->add_sticky
ارائه میشن. خود تابع add_sticky
هم برای نمایش پیامهای ورودی به عنوان admin notices ، استفاده میشه. اما این کار رو بدون پاکسازی (escaping or sanitization) انجام میده. این اخطارها در پنل ادمین نمایش داده میشه تا زمانیکه کاربر اونو ببنده.
اکثر افزونه ها و قالب هایی که از Freemius استفاده میکنن، بعد از فعالسازی، یه پیامی رو بصورت modal دریافت میکنن برای اینکه میخوان بروزرسانی های امنیتی یا ویژگی های جدید رو دریافت کنن یا نه :
اگه کاربر این ویژگی رو تایید کنه، یه ایمیل براش ارسال میشه تا فرایند تایید کنه. در این ایمیل یه لینکی هستش که میشه از طریق تابع dynamic_init
حمله XSS انجام داد.
اگه کاربر این ویژگی رو نخواد، باز میشه حمله XSS رو از طریق تابع _install_with_new_user
انجام داد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
includes/class-freemius.php, function _install_with_new_user() -------------------------- CUTTED HERE -------------------------- } else if ( $has_pending_activation_confirmation_param ) { $this->set_pending_confirmation( fs_request_get( 'user_email' ), true, false, false, fs_request_get_bool( 'is_suspicious_email' ), fs_request_get_bool( 'has_upgrade_context' ), fs_request_get( 'support_email_address' ) ); } -------------------------- CUTTED HERE -------------------------- |
در این مورد هم ، fs_request_get( 'user_email' )
و fs_request_get( 'support_email_address' )
بدون پاکسازی (escaped or sanitized) مستقیما به تابع $this->set_pending_confirmation
ارسال میشن :
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 |
includes/class-freemius.php, function set_pending_confirmation() -------------------------- CUTTED HERE -------------------------- private function set_pending_confirmation( $email = false, $redirect = true, $license_key = false, $is_pending_trial = false, $is_suspicious_email = false, $has_upgrade_context = false, $support_email_address = false ) { $is_network_admin = fs_is_network_admin(); if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) { /** * If explicitly asked to ignore pending mode, set to anonymous mode * if require confirmation before finalizing the opt-in except after completing a purchase (otherwise, in this case, they wouldn't see any notice telling them that they should receive their license key via email). * * @author Vova Feldman * @since 1.2.1.6 */ $this->skip_connection( $is_network_admin ); } else { // Install must be activated via email since // user with the same email already exist. $this->_storage->is_pending_activation = true; $this->_add_pending_activation_notice( $email, $is_pending_trial, $is_suspicious_email, $has_upgrade_context, $support_email_address ); } -------------------------- CUTTED HERE -------------------------- |
تابع بالا، تابع _add_pending_activation_notice
رو با پارامترهای $email
و $support_email_address
فراخوانی میکنه. در ادامه به نحوه پردازش این دو پارامتر نگاهی میندازیم :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
includes/class-freemius.php, function _add_pending_activation_notice(), line 6978-6981 & line 7024-7030 -------------------------- CUTTED HERE -------------------------- $formatted_message_args = array( "<b>{$this->get_plugin_name()}</b>", "<b>{$email_address}</b>", ); -------------------------- CUTTED HERE -------------------------- $formatted_message_args[] = ( ! empty( $support_email_address ) ) ? ( "<b>{$support_email_address}</b>" ) : $this->get_text_x_inline( "the product's support email address", 'Part of the message that tells the user to check their spam folder for a specific email.', 'product-support-email-address-phrase' ); -------------------------- CUTTED HERE -------------------------- |
این دو پارامتر بدون پاکسازی ، در متغیر $formatted_message_args
قرار میگیرن. formatted_message_args
هم همانند مورد اول، برای نمایش admin notices استفاده میشه:
1 2 3 4 5 |
$this->_admin_notices->add_sticky( vsprintf( $formatted_message, $formatted_message_args ), 'activation_pending', $notice_title ); |
محققا اعلام کردن که موارد دیگه ای از تابع fs_request_get رو در بخشهای دیگه ای از کد کتابخونه کشف کردن که بطور بالقوه میتونه XSS رو اجرا کنه. یه نکته دیگه اینکه، توابع dynamic_init و _install_with_new_user میتونن از هر یک از نقاط پایانی WP Admin فعال بشن، بنابراین این یه reflected XSS هستش.
اصلاحیه:
همونطور که بالا گفته شد، این آسیب پذیری در نسخه ی 2.5.10 اصلاح شده. نحوه اصلاح هم اینجوری بوده که از sanitize_text_field برای پاکسازی ورودی استفاده کردن.