SQL Injection Vulnerabilities Found in ListingPro Theme and Plugin

Published 12 September 2024
Rafie Muhammad
Security Researcher at Patchstack
Table of Contents

This blog post is about ListingPro theme vulnerabilities. If you’re a ListingPro user, please update the theme and plugin to version 2.9.5 or higher.

All paid Patchstack users are protected from this vulnerability. Sign up for the free Community account first, to scan for vulnerabilities and apply protection for only $5 / site per month with Patchstack. For plugin developers, we have security audit services and Enterprise API for hosting companies.

About the ListingPro Theme and Plugin

The theme ListingPro (premium version), which has over 30,000 sales, is one of the more popular premium plugins specifically related to directory and listing features. This theme is also packed with a required plugin also named ListingPro. This plugin is developed by CridioStudio.

This theme is designed for directory and listing websites of any type. This directory WordPress theme is an all-in-one solution to running successful directory businesses as it includes all the necessary tools and plugins needed.

The security vulnerability

This theme and plugin suffers from multiple SQL Injection vulnerabilities. The theme itself is affected by Unauthenticated SQL Injection while the plugin is affected by Authenticated and Unauthenticated SQL Injection. The SQL Injection vulnerability itself allows any unauthenticated and also authenticated user to inject a malicious SQL query into a WordPress database query execution. The described vulnerabilities are patched in version 2.9.5 and assigned CVE-2024-39622, CVE-2024-39620, and CVE-2024-38795 respectively.

ListingPro Theme: Unauthenticated SQL Injection

The underlying vulnerable code exists in the generate_wire_invoice function:

function generate_wire_invoice($postid){

    global $listingpro_options, $wpdb;
    $output = null;
    $logo = '';
    $company = '';
    $address = '';
    $phone = '';
    $additional = '';
    $thanku_text = '';
    $user_name = '';
    $taxIsOn = $listingpro_options['lp_tax_swtich'];
    $tax = '';

    $logo = $listingpro_options['invoice_logo']['url'];
    $company = $listingpro_options['invoice_company_name'];
    $address = $listingpro_options['invoice_address'];
    $phone = $listingpro_options['invoice_phone'];
    $additional = $listingpro_options['invoice_additional_info'];
    $thanku_text = $listingpro_options['invoice_thankyou'];
    $userrow = '';
    $userID = '';
    $dbprefix = $wpdb->prefix;
    $counter = 1;
    $userID = '';
    $price = '';
    $invoiceno = '';
    $table = "listing_orders";
    $table =$dbprefix.$table;
    $results = array();
    if($wpdb->get_var("SHOW TABLES LIKE '$table'") == $table) {
        $query = "";
        $query = "SELECT * from $table WHERE post_id='$postid' ORDER BY main_id DESC";
        $results = $wpdb->get_results( $query);
        $results = array_reverse($results);
    }
---------------- CUT HERE ---------------- 

This function can be called from the listingpro_shortcode_checkout function listed both on include/plugins/listingpro-plugin/elementor-widgets/checkout.php and include/plugins/listingpro-plugin/shortcodes/checkout.php:

---------------- CUT HERE ---------------- 
        else if( isset($_GET['method']) && !empty($_GET['method']) && $_GET['method']=="wire" ){
            if (!isset($_SESSION)) { session_start(); }
            do_action('lp_pdf_enqueue_scripts');
            $postID = $_SESSION['post_id'];
            $discount = $_SESSION['discount'];
            if(!empty($postID)){
                $output ='<div class="page-container-four clearfix">';
                $output .='<div class="col-md-10 col-md-offset-1">';
                $output .= generate_wire_invoice( $postID);
                $output .='</div>';
                $output .='</div>';
                unset($_SESSION['post_id']);
            }
            else{
                $redirect = site_url();
                wp_redirect($redirect);
                exit();
            }
        }
---------------- CUT HERE ---------------- 
---------------- CUT HERE ---------------- 
        else if (isset($_GET['method']) && !empty($_GET['method']) && $_GET['method'] == "wire") {
            if (!isset($_SESSION)) {
                session_start();
            }
            do_action('lp_pdf_enqueue_scripts');
            $postID = $_SESSION['post_id'];
            $discount = $_SESSION['discount'];
            if (!empty($postID)) {
                $output = '<div class="page-container-four clearfix">';
                $output .= '<div class="col-md-10 col-md-offset-1">';
                $output .= generate_wire_invoice($postID);
                $output .= '</div>';
                $output .= '</div>';
                unset($_SESSION['post_id']);
            } else {
                $redirect = site_url();
                wp_redirect($redirect);
                exit();
            }
        }
---------------- CUT HERE ---------------- 

The function in both files is simply handling a shortcode and Elementor widget element. We can see that it passes $postID value to the generate_wire_invoice function and the value is coming from $_SESSION[‘post_id’]. We can set the value via the lp_form_handler function:

function lp_form_handler($__POST, $__GET){

    session_start();

---------------- CUT HERE ---------------- 

    $method = $__POST['method'];
    $post_id = $__POST['post_id'];

---------------- CUT HERE ---------------- 

    if( !empty($method) && $method=="wire" ){
            //updating payment method
            $date = date(get_option('date_format'));
            $update_data = array('status' => 'pending', 'date' => $date, 'price' => $plan_price, 'tax' => $plan_taxPrice);
            if(!empty($new_plan_id)){
                $where = array('post_id' => $post_id, 'order_id' => $ord_num);
            }else{
                $where = array('post_id' => $post_id);
            }

            $update_format = array('%s', '%s', '%s', '%s');
            $wpdb->update($dbprefix.'listing_orders', $update_data, $where, $update_format);

            $_SESSION['post_id'] = $post_id;
---------------- CUT HERE ---------------- 

Back to the generate_wire_invoice function, since there is no proper escaping process on the $postid variable, users are able to perform SQL Injection.

ListingPro Plugin: Subscriber+ SQL Injection

The underlying vulnerable code exists on the lp_get_admin_invoice_details function:

add_action('wp_ajax_lp_get_admin_invoice_details', 'lp_get_admin_invoice_details');
if (!function_exists('lp_get_admin_invoice_details')) {
    function lp_get_admin_invoice_details()
    {
        global $wpdb;
        $invoiceid   =   $_POST['invoiceid'];
        $invoicetype   =   $_POST['invoicetype'];
        $tableName = 'listing_orders';
        if ($invoicetype == "listing") {

            $dbprefix = $wpdb->prefix;
            $myInvoice = $wpdb->get_row("SELECT * FROM " . $dbprefix . $tableName . " WHERE main_id = $invoiceid");

---------------- CUT HERE ---------------- 

        } elseif ($invoicetype == 'ads') {
            $output = null;
            $tableName = 'listing_campaigns';

            $dbprefix = $wpdb->prefix;
            $myInvoice = $wpdb->get_row("SELECT * FROM " . $dbprefix . $tableName . " WHERE main_id = $invoiceid");

---------------- CUT HERE ---------------- 

The above function is hooked to the wp_ajax_lp_get_admin_invoice_details action without any permission and nonce check. There is also no proper escaping on the $invoiceid variable which can make any authenticated users able to perform SQL Injection.

ListingPro Plugin: Unauthenticated SQL Injection

The underlying vulnerable code exists on the listingpro_save_stripe function:

add_action('wp_ajax_listingpro_save_stripe', 'listingpro_save_stripe');
add_action('wp_ajax_nopriv_listingpro_save_stripe', 'listingpro_save_stripe');

if (!function_exists('listingpro_save_stripe')) {
    function listingpro_save_stripe() {
        include_once (WP_PLUGIN_DIR ."/listingpro-plugin/inc/stripe/stripe-php/init.php");
        global $wpdb, $listingpro_options, $wp_rewrite;
        $lpURLChar = '?';
        if ($wp_rewrite->permalink_structure == '') {
            $lpURLChar = '&';
        }

---------------- CUT HERE ---------------- 

        if (isset($_POST['taxrate']) && !empty($_POST['taxrate'])) {
            $taxrate = $_POST['taxrate'];
        }
        $listing = $_POST['listing'];
        $token = $_POST['token'];
        $subsrID = '';

---------------- CUT HERE ---------------- 

        if ($charge['amount_refunded'] == 0 && $charge['failure_code'] == null && $charge['captured'] == true) {

---------------- CUT HERE ---------------- 

            $thepostt = $wpdb->get_results("SELECT * FROM " . $dbprefix . "listing_orders WHERE post_id = $listing");

---------------- CUT HERE ---------------- 

The above function is hooked to the wp_ajax_nopriv_listingpro_save_stripe action. There is also no proper escaping on the $listing variable which can make any unauthenticated users able to perform SQL Injection.

The patch

The developer decided to use a proper prepared statement and integer cast to the affected variables to prevent SQL Injection. The patch can be seen in the below diff image:

Conclusion

For the SQL query process, always do a safe escape and format for the user’s input before performing a query. The best practice is always to use a prepared statement and also cast each of the used variables to its intended usage, for example, always cast a variable to an integer if the intended value of the variable should be an integer value.

Want to learn more about finding and fixing vulnerabilities?

Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.

Timeline

31 March, 2024We found the vulnerabilities and started to create a report.
1 April, 2024We reach out to the vendor regarding the vulnerabilities.
22 July, 2024Published the vulnerabilities to the Patchstack vulnerability database (No reply from vendor).
30 August, 2024ListingPro theme and plugin version 2.9.5 released to patch the reported issues.
11 September, 2024Security advisory article publicly released.

Help us make the Internet a safer place

Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.

  • If you’re a plugin developer, join our mVDP program that makes it easier to report, manage and address vulnerabilities in your software.
  • If you’re a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.

The latest in Security advisories

Looks like your browser is blocking our support chat widget. Turn off adblockers and reload the page.
crossmenu