Critical Vulnerabilities Patched in Jupiter X Core Plugin

Published 24 August 2023
Rafie Muhammad
Security Researcher at Patchstack
Table of Contents

This blog post is about the Jupiter X Core plugin vulnerability. If you're a Jupiter X user, please update the plugin to at least version 3.4.3.

Patchstack Developer and Business users are protected from the vulnerability. You can also sign up for the Patchstack Community plan to be notified about vulnerabilities as soon as they become disclosed.

For plugin developers, we have security audit services and Threat Intelligence Feed API for hosting companies.

About the Jupiter X Core Plugin

The plugin Jupiter X Core (versions 3.3.8 and below, premium version) is a required plugin for the Jupiter X theme. The theme itself has more than 170,000 sales at ThemeForest. The Jupiter X theme is known as the more popular premium theme for WordPress. This plugin is developed by ArtBees.

This theme claims to be a fast, light, and powerful WordPress premium theme for building all kinds of websites. Jupiter X allows you to customize every inch of the website using a powerful visual editor. With the help of WordPress Customizer technology and Elementor page builder, it offers a seamless and limitless control, of both global and in-page elements.

The security vulnerabilities

The Jupiter X Core plugin suffers from multiple vulnerabilities. The first vulnerability is an unauthenticated arbitrary file upload which allow any unauthenticated users to upload any files to the server including PHP files to achieve remote code execution. The described vulnerability was fixed in version 3.3.8 and assigned CVE-2023-38388.

The second vulnerability is an unauthenticated account takeover which allow any unauthenticated user to login and takeover any WordPress user's account by only knowing their email address used for the account. The described vulnerabilities were fixed in version 3.4.3 and assigned CVE-2023-38389.

Unauthenticated Arbitrary File Upload

The underlying vulnerable code exists in the upload_files function:

public function upload_files() {
    // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
    $fields = isset( $_FILES['fields'] ) ? $_FILES['fields'] : false;

    if ( ! $fields ) {
        return $this;
    }

    foreach ( $fields as $id => $field ) {
        if ( empty( $field ) ) {
            continue;
        }

        foreach ( $field as $index => $file ) {
            if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
                continue;
            }

            $uploads_dir    = $this->get_ensure_upload_dir();
            $file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
            $filename       = uniqid() . '.' . $file_extension;
            $filename       = wp_unique_filename( $uploads_dir, $filename );
            $new_file       = trailingslashit( $uploads_dir ) . $filename;

            if ( ! is_dir( $uploads_dir ) || ! is_writable( $uploads_dir ) ) {
                $this
                    ->add_response( 'errors', __( 'Upload directory is not writable or does not exist.', 'jupiterx-core' ), $field['_id'] )
                    ->set_success( false );
                return $this;
            }

            $move_new_file = @move_uploaded_file( $file['tmp_name'], $new_file );
------------------------------- CUTTED HERE -------------------------------

Above function could be called from handle_frontend function:

public function handle_frontend() {
    $post_id      = filter_input( INPUT_POST, 'post_id' );
    $form_id      = filter_input( INPUT_POST, 'form_id' );
    $this->record = $_POST; // @codingStandardsIgnoreLine

    // Convert array data to string. Used for checkbox.
    foreach ( $this->record['fields'] as $_id => $field ) {
        if ( is_array( $field ) ) {
            $this->record['fields'][ $_id ] = implode( ', ', $field );
        }
    }

    $form_meta              = Elementor::$instance->documents->get( $post_id )->get_elements_data();
    $this->form             = Module::find_element_recursive( $form_meta, $form_id );
    $this->form['settings'] = Elementor::$instance->elements_manager->create_element_instance( $this->form )->get_settings_for_display();

    $this
        ->clear_step_fields()
        ->set_custom_messages()
        ->validate_form()
        ->validate_fields()
        ->upload_files()
        ->run_actions()
        ->send_response();
}

The handle_frontend function itself will be set as a function handler to wp_ajax_nopriv_raven_form_frontend action. Since it's a nopriv action hook, any unauthenticated user is able to call the function.

Back to the upload_files function, notice that the code will iterate thru the $_FILES['fields'] and upload the files using move_uploaded_file function with the filename constructed from wp_unique_filename function. The wp_unique_filename itself only make sure that the uploaded filename is unique in the upload directory and doesn't make the filename unique by default.

Unauthenticated Account Takeover

The underlying vulnerable code exists in the ajax_handler function of the facebook login process (Below function code is initially discovered in version 3.3.0 of the plugin) :

public function ajax_handler( $ajax_handler ) {
    $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL );
    $name  = filter_input( INPUT_POST, 'name', FILTER_SANITIZE_STRING );
    $fbid  = filter_input( INPUT_POST, 'fbid', FILTER_SANITIZE_STRING );

    if ( empty( $fbid ) || empty( $name ) ) {
        wp_send_json_error( __( 'Wrong Details.', 'jupiterx-core' ) );
    }

    if ( ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
        wp_send_json_error( __( 'Not a Valid Email.', 'jupiterx-core' ) );
    }

    $user_id = email_exists( $email );

    // Email is not registered.
    if ( false === $user_id ) {
        $user_id = $this->create_user( $email );
    }

    $set_meta         = $this->set_user_facebook_id( $user_id, $fbid );
    $unique_login_url = $this->create_unique_link_to_login_facebook_user( $fbid );
    $login            = [
        'login_url' => $unique_login_url,
    ];

    if ( ! empty( $ajax_handler->form['settings']['redirect_url']['url'] ) ) {
        $login['redirect_url'] = $ajax_handler->form['settings']['redirect_url']['url'];
    }

    wp_send_json_success( $login );
}

This function can be called by any unauthenticated user since this function is used as a handler to the facebook login process. Notice that there is a call to the $this->set_user_facebook_id() function with $user_id and $fbid parameter fully controlled by user input from the HTTP POST parameter. Let's view the set_user_facebook_id function:

private function set_user_facebook_id( $user_id, $facebook_id ) {
    update_user_meta( $user_id, 'social-media-user-facebook-id', $facebook_id );
}

Turns out the set_user_facebook_id will set the social-media-user-facebook-id value of a user. So, in this case, any unauthenticated user is able to set any user's social-media-user-facebook-id meta with any value. This meta value is used to authenticate user to the WordPress site. Let's take a look at facebook_log_user_in function:

public function facebook_log_user_in() {
    if ( ! isset( $_GET['jupiterx-facebook-social-login'] ) ) { // phpcs:ignore
        return;
    }

    $value = filter_input( INPUT_GET, 'jupiterx-facebook-social-login', FILTER_SANITIZE_STRING );
    $user  = get_users(
        [
            'meta_key'    => 'social-media-user-facebook-id', // phpcs:ignore
            'meta_value'  => $value, // phpcs:ignore
            'number'      => 1,
            'count_total' => false,
        ]
    );
    $id    = $user[0]->ID;

    wp_clear_auth_cookie();
    wp_set_current_user( $id ); // Set the current user detail
    wp_set_auth_cookie( $id ); // Set auth details in cookie

    if ( isset( $_GET['redirect'] ) ) { // phpcs:ignore
        $redirect = filter_input( INPUT_GET, 'redirect' );
        wp_redirect( $redirect ); // phpcs:ignore
        exit();
    }

    wp_redirect( site_url() ); // phpcs:ignore
    exit();
}

The facebook_log_user_in simply acts as the handler for the final login process of Facebook by fetching the user's object using the given social-media-user-facebook-id value. The function then will authenticate the user with the wp_set_current_user and wp_set_auth_cookie function. Since any unauthenticated user is able to set any user's social-media-user-facebook-id meta value, account takeover is possible.

The patch

For the unauthenticated arbitrary file upload vulnerability, the vendor decided to apply a check to the file fields inside of the upload_files function:

The patch code will check if the uploaded file field is actually registered in the form settings field. The check for the file itself is already done from validate_fields which only allows specific file types to be uploaded.

For the unauthenticated account takeover, the vendor patched the issue by fetching the needed email address and the unique user id directly from the Facebook authentication endpoint:

Conclusion

For the custom form process, make sure to validate all of the registered form fields and ignore the non-registered fields input from the user. Always check everything along the file upload process and make sure to apply a whitelist check on the uploaded file type or extension.

For custom social login process with platform such as Facebook, Twitter, Google, make sure to implement the best practice of the social login process of each platform and only trust response input such as email or unique user id from the platform's valid authentication response and don't directly rely on the user input.

Timeline

13 July, 2023We found the vulnerability and reached out to the plugin vendor.
31 July, 2023Jupiter X Core version 3.3.8 released to patch the reported Unauthenticated Arbitrary File Upload issue.
09 August, 2023Jupiter X Core version 3.4.3 released to patch the reported Unauthenticated Account Takeover issue.
24 August, 2023Added the vulnerabilities to the Patchstack vulnerability database. Security advisory article publicly released.

Help us make the Internet a safer place

Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.

  • If you're a plugin developer, join our mVDP program that makes it easier to report, manage and address vulnerabilities in your software.
  • If you're a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.

The latest in Security Advisories

Looks like your browser is blocking our support chat widget. Turn off adblockers and reload the page.
crossmenu