محققای Patchstack یه آسیب پذیری بحرانی به شناسه CVE-2023-32243 در پلاگین وردپرسی Essential Addons for Elementor کشف و گزارش کردن که بیش از یک میلیون نصب فعال داره.
این پلاگین یکی از محبوبترین پلاگین های جانبی Elementor هستش که بیش از 90 ویجیت در اختیار کاربران قرار میده تا فرایند طراحی ساده تر و زیباتری رو تجربه کنن.
آسیب پذیری CVE-2023-32243 در این پلاگین، امکان افزایش امتیاز ،به یه مهاجم بدون احرازهویت شده رو میده. مهاجم میتونه با ریست پسورد هر کاربری، وارد اکانت اون بشه. نکته ای که هست، مهاجم باید نام کاربری قربانی رو بدونه و نکته بعدی اینکه ، مهاجم میتونه اکانت ادمین سایت رو با این آسیب پذیری بزنه و کل سایت رو تصاحب کنه.
علت کلی آسیب پذیری اینه که ، تابع ریست پسورد، بدون بررسی و تایید کلید ریست پسورد، مستقیما پسورد عوض میکنه.
نسخه های تحت تاثیر :
نسخه های 5.4.0 تا 5.7.1
نسخه های اصلاح شده:
نسخه 5.7.2 به بالاتر
بررسی آسیب پذیری :
شروع کشف این آسیب پذیری ، با مشاهده init hook در تابع register_hooks بوده :
1 2 3 4 5 |
includes/Classes/Bootstrap.php // Login | Register add_action('init', [$this, 'login_or_register_user']); |
در کد بالا ، تابع login_or_register_user اجرا میشه، این تابع بصورت زیر هستش :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
includes/Traits/Login_Registration.php public function login_or_register_user() { do_action( 'eael/login-register/before-processing-login-register', $_POST ); // login or register form? if ( isset( $_POST['eael-login-submit'] ) ) { $this->log_user_in(); } else if ( isset( $_POST['eael-register-submit'] ) ) { $this->register_user(); } else if ( isset( $_POST['eael-lostpassword-submit'] ) ) { $this->send_password_reset(); } else if ( isset( $_POST['eael-resetpassword-submit'] ) ) { $this->reset_password(); } do_action( 'eael/login-register/after-processing-login-register', $_POST ); } |
در کد بالا با بررسی پارامتر $_POST ، تابع مربوطه اجرا میشه. آسیب پذیری در تابع reset_password هستش که از نسخه 5.4.0 اضافه شده.
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
includes/Traits/Login_Registration.php public function reset_password() { $ajax = wp_doing_ajax(); $page_id = 0; if ( ! empty( $_POST['page_id'] ) ) { $page_id = intval( $_POST['page_id'], 10 ); } else { $err_msg = esc_html__( 'Page ID is missing', 'essential-addons-for-elementor-lite' ); } $widget_id = 0; if ( ! empty( $_POST['widget_id'] ) ) { $widget_id = sanitize_text_field( $_POST['widget_id'] ); } else { $err_msg = esc_html__( 'Widget ID is missing', 'essential-addons-for-elementor-lite' ); } $rp_data = [ 'rp_key' => ! empty( $_POST['rp_key'] ) ? sanitize_text_field( $_POST['rp_key'] ) : '', 'rp_login' => ! empty( $_POST['rp_login'] ) ? sanitize_text_field( $_POST['rp_login'] ) : '', ]; update_option( 'eael_resetpassword_rp_data_' . esc_attr( $widget_id ), maybe_serialize( $rp_data ), false ); update_option( 'eael_show_reset_password_on_form_submit_' . $widget_id, true, false ); if (!empty( $err_msg )){ if ( $ajax ) { wp_send_json_error( $err_msg ); } update_option( 'eael_resetpassword_error_' . $widget_id, $err_msg, false ); if (isset($_SERVER['HTTP_REFERER'])) { wp_safe_redirect($_SERVER['HTTP_REFERER']); exit(); } } if ( empty( $_POST['eael-resetpassword-nonce'] ) ) { $err_msg = esc_html__( 'Insecure form submitted without security token', 'essential-addons-for-elementor-lite' ); if ( $ajax ) { wp_send_json_error( $err_msg ); } update_option( 'eael_resetpassword_error_' . $widget_id, $err_msg, false ); if (isset($_SERVER['HTTP_REFERER'])) { wp_safe_redirect($_SERVER['HTTP_REFERER']); exit(); } } if ( ! wp_verify_nonce( $_POST['eael-resetpassword-nonce'], 'essential-addons-elementor' ) ) { $err_msg = esc_html__( 'Security token did not match', 'essential-addons-for-elementor-lite' ); if ( $ajax ) { wp_send_json_error( $err_msg ); } update_option( 'eael_resetpassword_error_' . $widget_id, $err_msg, false ); if (isset($_SERVER['HTTP_REFERER'])) { wp_safe_redirect($_SERVER['HTTP_REFERER']); exit(); } } $settings = $this->lr_get_widget_settings( $page_id, $widget_id); if ( is_user_logged_in() ) { $err_msg = isset( $settings['err_loggedin'] ) ? __( Helper::eael_wp_kses( $settings['err_loggedin'] ), 'essential-addons-for-elementor-lite' ) : esc_html__( 'You are already logged in', 'essential-addons-for-elementor-lite' ); if ( $ajax ) { wp_send_json_error( $err_msg ); } update_option( 'eael_resetpassword_error_' . $widget_id, $err_msg, false ); if (isset($_SERVER['HTTP_REFERER'])) { wp_safe_redirect($_SERVER['HTTP_REFERER']); exit(); } } do_action( 'eael/login-register/before-resetpassword-email' ); $widget_id = ! empty( $_POST['widget_id'] ) ? sanitize_text_field( $_POST['widget_id'] ) : ''; // Check if password is one or all empty spaces. $errors = []; if ( ! empty( $_POST['eael-pass1'] ) ) { $post_eael_pass1 = trim( $_POST['eael-pass1'] ); if ( empty( $post_eael_pass1 ) ) { $errors['password_reset_empty_space'] = isset( $settings['err_pass'] ) ? __( Helper::eael_wp_kses( $settings['err_pass'] ), 'essential-addons-for-elementor-lite' ) : esc_html__( 'The password cannot be a space or all spaces.', 'essential-addons-for-elementor-lite' ); } } else { if ( empty( $_POST['eael-pass1'] ) ) { $errors['password_reset_empty_space'] = isset( $settings['err_pass'] ) ? __( Helper::eael_wp_kses( $settings['err_pass'] ), 'essential-addons-for-elementor-lite' ) : esc_html__( 'The password cannot be a space or all spaces.', 'essential-addons-for-elementor-lite' ); } } if( ! empty( $_POST['eael-pass1'] ) && strlen( trim( $_POST['eael-pass1'] ) ) == 0 ){ $errors['password_reset_empty'] = esc_html__( 'The password cannot be empty.', 'essential-addons-for-elementor-lite' ); } // Check if password fields do not match. if ( ! empty( $_POST['eael-pass1'] ) && $_POST['eael-pass2'] !== $_POST['eael-pass1'] ) { $errors['password_reset_mismatch'] = isset( $settings['err_conf_pass'] ) ? __( Helper::eael_wp_kses( $settings['err_conf_pass'] ), 'essential-addons-for-elementor-lite' ) : esc_html__( 'The passwords do not match.', 'essential-addons-for-elementor-lite' ); } if ( ( ! count( $errors ) ) && isset( $_POST['eael-pass1'] ) && ! empty( $_POST['eael-pass1'] ) ) { $rp_login = isset( $_POST['rp_login']) ? sanitize_text_field( $_POST['rp_login'] ) : ''; $user = get_user_by( 'login', $rp_login ); if( $user || ! is_wp_error( $user ) ){ reset_password( $user, sanitize_text_field( $_POST['eael-pass1'] ) ); |
در ابتدا $_POST[‘page_id’] و $_POST[‘widget_id’] بررسی میشن تا خالی نباشن، برای دور زدن این دو تا مورد یه مقدار تصادفی برای این دو پارامتر قرار میدیم. با این کار $err_msg ست نمیشه.
در ادامه باید مقداری هم برای $_POST[‘eael-resetpassword-nonce’] ست کنیم، چون مقدار nonce در کد بررسی میشه.
همچنین نیازه که یه رشته پسورد همانند برای $_POST[‘eael-pass1’] و $_POST[‘eael-pass2’] ست کنیم ، چون در ادامه بررسی میشن.
اگه همه مراحل بالا رو پشت سر بزارید، متغیر $rp_login از $_POST[‘rp_login’] مقدار دهی میشه.
در ادامه کد ، با استفاده از تابع get_user_by دنبال مقدار login (username) که با مقدار $rp_login مطابق داره، میگرده و در $user میریزه.
حالا اگه $user وجود داشته باشه و خطایی رخ نده، پسورد با استفاده از تابع reset_password ، مستقیما ریست میشه.
تقریبا همه مراحل بالا قابل دور زدن هستش به غیر از بدست آوردن مقدار essential-addons-elementor nonce . این مقدار در صفحه اصلی وردپرس هستش، چون متغیر $this->localize_objects توسط تابع load_commnon_asset ست میشه :
1 2 3 4 5 6 7 |
includes/Classes/Asset_Builder.php // localize object $this->localize_objects = apply_filters( 'eael/localize_objects', [ 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'essential-addons-elementor' ), |
متغیر $this->localize_objects که در کد بالا مقدار دهی شد، در تابع frontend_asset_load بعنوان شی در wp_localize_script فراخوانی میشه :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
includes/Classes/Asset_Builder.php public function frontend_asset_load() { $handle = 'eael'; $this->post_id = get_the_ID(); $this->elements_manager->get_element_list( $this->post_id ); $this->load_commnon_asset(); $this->register_script(); -------------------- CUTTED HERE ------------------------------------ wp_localize_script( $handle, 'localize', $this->localize_objects ); } |
خود تابع frontend_asset_load هم از طریق تابع init_hook فراخوانی میشه و همونطور که در کد بالا مشاهده میکنید به عنوان یه تابع کنترل کننده (function handler) ،هوک wp_enqueue_scripts مورد استفاده قرار میگیره. wp_enqueue_scripts برای نمایش همه اسکریپتها و استایلهای صف بندی شده مورد استفاده قرار میگیره. اصولا هر صفحه ای که لوود میشه، اگه تو قسمت هدرش تابع wp_head باشه ، باعث فراخوانی wp_enqueue_scripts میشه ، که فراخوانی اون هم منجر به اجرای تابع نا میشه.
اصلاح:
توسعه دهنده برای اصلاح این آسیب پذیری از مقدار eael_resetpassword_rp_data_ که توسط تابع eael_redirect_to_reset_password مقداردهی میشه، استفاده کرده (مشاهده اصلاحیه) :
نتیجه گیری:
به یاد داشته باشید که کاربران احرازهویت نشده، میتونن هوکهای init و admin_init رو روی وردپرس اجرا کنن، بنابراین اگه اکشن خاصی داریم که با دیتابیس در تعامل هست، نیاز داریم که یه کنترل کننده دسترسی مناسب و بررسی کننده nonce به function handler اضافه کنیم.
همچنین به هر موردی که مرتبط با ثبت نام، لاگین، بازیابی یا ریست پسورد هستش، توجه بیشتری کنیم و از تابع check_password_reset_key برای بررسی reset password key استفاده کنیم.