WordPress ListingPro theme fixed a critical vulnerability.

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