Remote Code Execution (RCE) Vulnerability in Elementor

The authors of the Elementor Website Builder plugin for WordPress have just released version 3.6.3 to address a critical remote code execution vulnerability that may impact as many as 500,000 websites.

Although exploiting the flaw requires authentication, its critical severity is given by the fact that anyone logged into the vulnerable website can exploit it, including regular subscribers.

A threat actor creating a normal user account on an affected website could change the affected site’s name and theme, making it look entirely different.

Security researchers believe that a non-logged-in user could also exploit the recently fixed flaw in Elementor plugin but they have not confirmed this scenario.

Vulnerability details
In a report released this week by researchers at the WordPress security service Plugin Vulnerabilities, who found the vulnerability, describe the technical details behind the issue in Elementor.

The problem lies in the absence of a crucial access check on one of the plugin’s files, “module.php”, which is loaded on every request during the admin_init action, even for users that are not logged in, the researchers explain.

“The RCE vulnerability we found involves the function upload_and_install_pro() accessible through the previous function. That function will install a WordPress plugin sent with the request” – Plugin Vulnerabilities

One of the functions triggered by the admin_init action allows file upload in the form of a WordPress plugin. A threat actor could place a malicious file there to achieve remote code execution.

In the plugin’s file /core/app/modules/onboarding/module.php, the following code is set to run during admin_init, which means it runs even for those not logged in to WordPress:

add_action( 'admin_init', function() {
if ( wp_doing_ajax() &&
isset( $_POST['action'] ) &&
isset( $_POST['_nonce'] ) &&
wp_verify_nonce( $_POST['_nonce'], Ajax::NONCE_KEY )
) {
$this->maybe_handle_ajax();

That code will run another function in the file, maybe_handle_ajax(), if an AJAX request is being made and a valid nonce is provided. That function, in turn, will run other functions depending on the value of the POST input “action”:

private function maybe_handle_ajax() {
$result = [];
// phpcs:ignore WordPress.Security.NonceVerification.Missing
switch ( $_POST['action'] ) {
case 'elementor_update_site_name':
// If no value is passed for any reason, no need ot update the site name.
$result = $this->maybe_update_site_name();
break;
case 'elementor_update_site_logo':
$result = $this->maybe_update_site_logo();
break;
case 'elementor_upload_site_logo':
$result = $this->maybe_upload_logo_image();
break;
case 'elementor_update_data_sharing':
$result = $this->set_usage_data_opt_in();
break;
case 'elementor_activate_hello_theme':
$result = $this->activate_hello_theme();
break;
case 'elementor_upload_and_install_pro':
$result = $this->upload_and_install_pro();
break;
case 'elementor_update_onboarding_option':
$result = $this->maybe_update_onboarding_db_option();
}

Neither of those do capabilities check to limit who can access them.

The RCE vulnerability we found involves the function upload_and_install_pro() accessible through the previous function. That function will install a WordPress plugin sent with the request:

private function upload_and_install_pro() {
$result = [];
$error_message = __( 'There was a problem uploading your file', 'elementor' );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( empty( $_FILES['fileToUpload'] ) ) {
$result = [
'status' => 'error',
'payload' => [
'error_message' => $error_message,
],
];

return $result;
}

if ( ! class_exists( 'Automatic_Upgrader_Skin' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}

$skin = new Automatic_Upgrader_Skin();
$upgrader = new Plugin_Upgrader( $skin );
$upload_result = $upgrader->install( $_FILES['fileToUpload']['tmp_name'], [ 'overwrite_package' => false ] );

That means that arbitrary files can be uploaded to the website. The RCE occurs as the code then tries to activate a plugin that would be located in the location of Elementor Pro:

$activated = activate_plugin( WP_PLUGIN_DIR . '/elementor-pro/elementor-pro.php', false, false, true );

So if plugin being uploaded matches that, it will get activated and the code in the relevant file will then run.

Based on all that, the only restriction in place is access to a valid nonce. What we found is that the relevant nonce is included in the line of the source code of admin pages of WordPress that starts “elementorCommonConfig”, which is included when logged in as a user with the Subscriber role.

This has been fixed in Elementor 3.6.4,

Photo by Luca Bravo on Unsplash