The WordPress ListingPro theme, which has 19,000+ sales on Envato Market, fixed a critical vulnerability that could allow an unauthenticated user to install any file on the blog, among other issues. Affected versions are 2.5.13 and below and, to some extend, versions 2.5.14 and 2.6 (see Timeline below for more details).
Unauthenticated Plugin Installation
In order to run, the theme requires several plugins that are all included in the package: ListingPro Plugin, Listingpro ADs and Listingpro Reviews. The ListingPro Plugin has a “Command Center” menu that is used to install and activate them as well as some additional plugins:
The code used to handle that part is located inside the “listingpro-plugin/inc/command-center/commandcenter-function.php” script:
235 add_action( 'wp_ajax_lp_cc_addons_actions', 'lp_cc_addons_actions' ); 236 add_action( 'wp_ajax_nopriv_lp_cc_addons_actions', 'lp_cc_addons_actions' );
It registers the lp_cc_addons_actions
AJAX action and, although it should only be accessible to an admin, makes it accessible to unauthenticated users as well (wp_ajax_nopriv_*
).
It loads the “lp_cc_addons_actions” function, which accepts 3 actions (ccAction
): activate
, deactivate
and install
:
65 if(!function_exists('lp_cc_addons_actions')) { 66 function lp_cc_addons_actions() { 67 68 $return = array(); 69 if( isset($_REQUEST) ) { 70 $ccAction = $_REQUEST['ccAction']; 71 $ccDestin = $_REQUEST['ccDestin']; 72 $ccFile = $_REQUEST['ccFile']; 73 74 75 if($ccAction == 'activate') { 76 77 $plugin = $ccFile; 78 $current = get_option( 'active_plugins' ); 79 $plugin = plugin_basename( trim( $plugin ) ); 80 81 if ( !in_array( $plugin, $current ) ) { 82 $current[] = $plugin; 83 sort( $current ); 84 do_action( 'activate_plugin', trim( $plugin ) ); 85 update_option( 'active_plugins', $current ); 86 do_action( 'activate_' . trim( $plugin ) ); 87 do_action( 'activated_plugin', trim( $plugin) ); 88 89 $return['status'] = 'success'; 90 } 91 92 } 93 if($ccAction == 'deactivate') { 94 deactivate_plugins( $ccFile ); 95 96 $return['status'] = 'success'; 97 } 98 if($ccAction == 'install') { 99 if( $ccDestin == 'own' ) { ... ... ... ... ... ... 132 } 133 134 if($ccDestin == 'external') { 135 $ccFileUrl = $_REQUEST['ccFileUrl']; 136 137 $file_Arr = explode('/', $ccFile); 138 $file_zip = $ccFileUrl; 139 $destin_path = ABSPATH . 'wp-content/plugins/'.$file_Arr['0'].'.zip'; 140 141 if(file_exists($destin_path)) { 142 unlink($destin_path); 143 } 144 if(!file_exists($destin_path)){ 145 if(copy($file_zip, $destin_path)){ 146 WP_Filesystem(); 147 $unzipfile = unzip_file( $destin_path, ABSPATH . 'wp-content/plugins/'); 148 if(!is_wp_error($unzipfile)){ 149 unlink($destin_path); 150 $plugin = $ccFile; 151 152 $current = get_option( 'active_plugins' ); 153 $plugin = plugin_basename( trim( $plugin ) ); 154 155 if ( !in_array( $plugin, $current ) ) { 156 $current[] = $plugin; 157 sort( $current ); 158 do_action( 'activate_plugin', trim( $plugin ) ); 159 update_option( 'active_plugins', $current ); 160 do_action( 'activate_' . trim( $plugin ) ); 161 do_action( 'activated_plugin', trim( $plugin) ); 162 } 163 } 164 } 165 } 166 $return['status'] = 'success'; 167 } 168 }
The function doesn’t check the user capabilities and lacks a security nonce.
Using the deactivate
action, an unauthenticated user could deactivate plugins (e.g., firewall, antispam, two-factor authentication), but the most interesting action is install
. It can either install plugins that are bundled with ListingPro and located inside the “wp-content/themes/listingpro/include/plugins” folder, or remotely hosted on another server when $_REQUEST['ccDestin']
is set to external
. The URL of the remote file to install, which must be a ZIP archive, has to be assigned to the $_REQUEST['ccFileUrl']
input. It will be downloaded and its content extracted into the “wp-content/plugins” folder. If it is a plugin, it will be automatically activated.
An unauthenticated attacker could install any type of file, script or plugin, on the website.
Unauthenticated Information Disclosure
In the “listingpro-plugin/functions.php” script, which is loaded via the init
hook, i.e., each time the blog loads, the plugin has a feature to export users’ data in CSV format:
2661 if( is_admin() && isset( $_GET['download-lp-users'] ) && $_GET['download-lp-users'] == 'yes' ) 2662 { 2663 $users = get_users(); 2664 2665 $user_type = 'all'; 2666 if( isset( $_GET['user-type'] ) ) 2667 { 2668 $user_type = $_GET['user-type']; 2669 } 2670 2671 $users_data = array( 2672 array('UserName','Full Name','Email', 'Phone', 'Listings'), 2673 ); 2674 if( $user_type == 'listing_owners' ) 2675 { 2676 2677 foreach ( $users as $user ) 2678 { 2679 $posts_count = count_user_posts( $user->ID , 'listing' ); 2680 2681 if( $posts_count > 0 ) 2682 { 2683 $user_email = $user->user_email; 2684 $username = $user->user_login; 2685 $full_name = $user->first_name.' '.$user->last_name; 2686 $phone = get_user_meta( $user->ID, 'phone', true ); 2687 $user_post_count = count_user_posts( $user->ID , 'listing' ); 2688 2689 $users_data[] = array( $username, $full_name, $user_email, $phone, $user_post_count ); 2690 } 2691 } 2692 2693 } ... ... ... ... 2712 if( $user_type == 'all' ) 2713 { 2714 foreach ( $users as $user ) 2715 { 2716 $user_email = $user->user_email; 2717 $username = $user->user_login; 2718 $full_name = $user->first_name.' '.$user->last_name; 2719 $phone = get_user_meta( $user->ID, 'phone', true ); 2720 $user_post_count = count_user_posts( $user->ID , 'listing' ); 2721 2722 $users_data[] = array( $username, $full_name, $user_email, $phone, $user_post_count ); 2723 } 2724 } 2725 2726 2727 2728 2729 users_to_csv_download($users_data, "listingpro-users.csv"); 2730 }
Access to that code relies only on the infamous WordPress is_admin
function that, unlike its name tends to suggest, does not check whether the user is an administrator or not.
An unauthenticated user can send a /wp-admin/index.php?download-lp-users=yes
request and download the list of all users that includes the login name, first and last name, email address and, if applicable, the phone number.
Recommendations
Update immediately if you have version 2.6 or below installed. If you are using our web application firewall for WordPress, NinjaFirewall WP Edition (free) and NinjaFirewall WP+ Edition (premium), you are protected against this vulnerability.
Timeline
Due to unsuccessful attempts to contact the authors on November 25, 2020, the issue was escalated to the Envato team on November 26 and again on December 03. New versions 2.5.14 and 2.6 were released on December 07 but the main vulnerability was still exploitable by authenticated users. The issue was fixed in version 2.6.1 released on December 10.
Stay informed about the latest vulnerabilities
- Running WordPress? You can get email notifications about vulnerabilities in the plugins or themes installed on your blog.
- On Twitter: @nintechnet