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
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.