WordPress Elementor plugin fixed Safe Mode privilege escalation vulnerability.

Elementor, a popular WordPress plugin installed on 4+ million websites, fixed a high severity vulnerability affecting version 2.9.5 and below. Any authenticated user could enable its Safe Mode feature allowing everyone, logged-in or not, to interact with it and to disable security plugins installed on the website such as firewall, antispam, two-factor authentication or captcha plugins for instance.

Elementor has a Safe Mode feature that is described as “a safe environment that isolates Elementor and WordPress from the themes and plugins that might be causing the error”. It can be enabled by the administrator from the plugin “Tools” page:

The fact that the editor can be loaded without loading any other plugin and the theme is certainly interesting from a developer’s point of view but, because Elementor is accessible to low-privileged users too, it sounds even more interesting from a hacker’s point of view. Let’s see how it works.

When enabling it, it calls the enable_safe_mode function from the “elementor/modules/safe-mode/module.php” script:

public function enable_safe_mode() {
   WP_Filesystem();

   $this->update_allowed_plugins();

   if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
      wp_mkdir_p( WPMU_PLUGIN_DIR );
      add_option( 'elementor_safe_mode_created_mu_dir', true );
   }

   if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
      wp_die( __( 'Cannot enable Safe Mode', 'elementor' ) );
   }

   $results = copy_dir( __DIR__ . '/mu-plugin/', WPMU_PLUGIN_DIR );

   if ( is_wp_error( $results ) ) {
      return false;
   }
}

It drops the “elementor/modules/safe-mode/mu-plugin/elementor-safe-mode.php” script into the WordPress “/mu-plugins/” folder:

class Safe_Mode {

   const OPTION_ENABLED = 'elementor_safe_mode';

   public function is_enabled() {
      return get_option( self::OPTION_ENABLED );
   }

   public function is_requested() {
      return ! empty( $_REQUEST['elementor-mode'] ) && 'safe' === $_REQUEST['elementor-mode'];
   }

   public function is_editor() {
      return is_admin() && isset( $_GET['action'] ) && 'elementor' === $_GET['action'];
   }

   public function is_editor_preview() {
      return isset( $_GET['elementor-preview'] );
   }

   public function is_editor_ajax() {
      return is_admin() && isset( $_POST['action'] ) && 'elementor_ajax' === $_POST['action'];
   }

   public function add_hooks() {
      add_filter( 'pre_option_active_plugins', function () {
         return get_option( 'elementor_safe_mode_allowed_plugins' );
      } );

      add_filter( 'pre_option_stylesheet', function () {
         return 'elementor-safe';
      } );

      add_filter( 'pre_option_template', function () {
         return 'elementor-safe';
      } );

      add_action( 'elementor/init', function () {
         do_action( 'elementor/safe_mode/init' );
      } );
   }
...
...
   public function __construct() {
      add_filter( 'plugin_row_meta', [ $this, 'plugin_row_meta' ], 10, 4 );

      $enabled_type = $this->is_enabled();

      if ( ! $enabled_type ) {
         return;
      }

      if ( ! $this->is_requested() && 'global' !== $enabled_type ) {
         return;
      }

      if ( ! $this->is_editor() && ! $this->is_editor_preview() && ! $this->is_editor_ajax() ) {
         return;
      }

      $this->add_hooks();
   }
}

new Safe_Mode();

It uses the pre_option_ hook so that when WordPress queries the database to check which plugins, theme and style sheet must be loaded (active_plugins, template and stylesheet respectively), it returns that only Elementor should be activated. But this code is accessible to all users, logged-in or not, and can be triggered just by adding a specific query string to an HTTP request that will disable all other plugins while the request is being processed by WordPress.

In Elementor’s documentation, we can find some additional info about the Safe Mode activation:

In fact, there isn’t one but two ways to enable the Safe Mode: either from the settings page by an admin as described above or from the editor. The latter will only occur if there is a problem while it loads.
With a contributor account, and after messing a bit with some of the HTML code, it wasn’t too difficult to force it to appear inside the editor screen:

When clicking the “Enable Safe Mode” button, it will send an AJAX request along with a security nonce to the handle_ajax_request function, which will forward it to ajax_enable_safe_mode and then will reach our enable_safe_mode function that will drop the MU plugin. None of these functions check user capabilities and they only rely on a nonce that is populated in the “elementor/core/common/app.php” script using the admin_enqueue_scripts hook.

add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ] );

Because the purpose of this hook is to enqueue scripts for all admin pages, the nonce is accessible to all authenticated users:

Therefore, any logged-in user, including a subscriber, can activate the Safe Mode and it can be triggered by anyone, authenticated or not, in the backend or frontend of WordPress. Attackers could use it for several purposes, for instance:

  • To send spam: they could disable a captcha and an antispam such as Akismet used to protect a form.
  • To bypass a firewall: if there were a vunerability in Elementor or WordPress, attackers could disable the firewall plugin and exploit it.
  • To attack the login page: they could disable any plugin used to protect it (two-factor authentication, brute-force protection, activity log, login notification etc) or to hide it.

Note that Elementor’s Role Manager won’t prevent the exploitation of this vulnerability.
Additionally, if the Safe Mode can be enabled by any user in the backend, it can also be disabled because the disable_safe_mode action relies on the same AJAX function and security nonce.

Demonstration

The following video has been edited to obscure some parts of the payload used to perform the attack.

This video demonstrates the exploitation of the Elementor Safe Mode privilege escalation vulnerability.
1. Attackers will use a subscriber account to enable the Safe Mode.
2. They will disable the login page captcha so that they can run a brute-force attack until they find the admin password.
3. They will disable the two-factor authentication security plugin in order to gain access to the administrator account.

Recommendation

Update ASAP if you have version 2.9.5 or below installed.
If you are using our web application firewall for WordPress, NinjaFirewall WP Edition (free) and NinjaFirewall WP+ Edition (premium), you are protected against this vulnerability since March 10, 2020.

Timeline

The vulnerability was reported on March 10, 2020 and a new version 2.9.6 was released on March 12, 2020.

Stay informed about the latest vulnerabilities