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
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.