The WordPress Ultimate GDPR & CCPA Compliance Toolkit plugin, which has 6,000+ sales on Envato Market, was prone to a critical unauthenticated settings import and export vulnerability affecting version 2.4 and below that could allow an attacker to redirect traffic to a malicious site among other issues.
Unauthenticated Settings Import & Export
In the “ct-ultimate-gdpr/includes/controller/controller-admin.php” script, line 36, the plugin loads the admin_actions
function if the WordPress is_admin
function returns true:
public function __construct() { $this->register_menu_pages(); $this->register_option_fields(); $this->register_styles(); if ( is_admin() ) { $this->admin_actions(); }
The is_admin
function doesn’t guarantee that the user is logged-in or is an administrator. Instead, it will always return true whenever a user, authenticated or not, accesses some scripts inside the “/wp-admin/” folder such as “wp-admin/index.php” or “wp-admin/admin-ajax.php” for instance.
The admin_actions
function is used to load four different functions depending on the $_POST
variable:
private function admin_actions() { if ( $this->is_request_export_settings() ) { add_action( 'ct_ultimate_gdpr_after_controllers_registered', array( $this, 'export_settings' ) ); } if ( $this->is_request_import_settings() ) { add_action( 'ct_ultimate_gdpr_after_controllers_registered', array( $this, 'import_settings' ) ); } if ( $this->is_request_export_services() ) { add_action( 'ct_ultimate_gdpr_after_controllers_registered', array( $this, 'export_services' ) ); } if ( $this->is_request_import_services() ) { add_action( 'ct_ultimate_gdpr_after_controllers_registered', array( $this, 'import_services' ) ); } } /** * @return bool */ private function is_request_export_settings() { if ( ct_ultimate_gdpr_get_value( 'ct-ultimate-gdpr-export', $_POST ) ) { return true; } return false; } /** * @return bool */ private function is_request_import_settings() { if ( ct_ultimate_gdpr_get_value( 'ct-ultimate-gdpr-import', $_POST ) ) { return true; } return false; } /** * @return bool */ private function is_request_export_services() { if ( ct_ultimate_gdpr_get_value( 'ct-ultimate-gdpr-export-services', $_POST ) ) { return true; } return false; } /** * @return bool */ private function is_request_import_services() { if ( ct_ultimate_gdpr_get_value( 'ct-ultimate-gdpr-import-services', $_POST ) ) { return true; } return false; }
The import_settings
function, used to import all settings from a JSON-encoded file, doesn’t check for user capabilities and lacks a security nonce:
/** * Import from json file */ public function import_settings() { $import_file = isset( $_FILES['ct-ultimate-gdpr-settings-file']['tmp_name'] ) ? $_FILES['ct-ultimate-gdpr-settings-file']['tmp_name'] : ''; if ( empty( $import_file ) ) { $this->view_options['notices'] = array( esc_html__( 'Please upload a file to import', 'ct-ultimate-gdpr' ) ); return; } // Retrieve the settings from the file and convert the json object to an array. $settings = (array) json_decode( file_get_contents( $import_file ), true ); if ( empty( $settings ) ) { $this->view_options['notices'] = array( esc_html__( 'No options were imported', 'ct-ultimate-gdpr' ) ); return; } $updated = false; foreach ( $settings as $id => $options ) { $check_id = CT_Ultimate_GDPR::instance()->get_controller_by_id( $id ); if ( $check_id ) { // update controller options $updated = $updated || update_option( $id, $options ); } } $this->view_options['notices'] = $updated ? array( esc_html__( 'Settings imported successfully', 'ct-ultimate-gdpr' ) ) : array( esc_html__( 'Settings were not imported. Please check the import file.', 'ct-ultimate-gdpr' ) ); }
An unauthenticated user can change the plugin’s settings and redirect all traffic to an external malicious website.
This vulnerability is particularly damaging because the attacker can even choose which traffic should be redirected: guest users and/or authenticated users and/or admin users.
The redirection is performed via the Location
header:
$ curl 'http://example.com/' -I HTTP/1.1 302 Found Server: nginx/1.18.0 Date: Thu, 21 Jan 2021 12:42:46 GMT X-Redirect-By: WordPress Location: https://evilsite.com/
The export_settings
function, used to export the plugin’s settings, is too accessible to anyone:
public function export_settings() { $controllers = CT_Ultimate_GDPR::instance()->get_controllers(); $settings = array(); /** @var CT_Ultimate_GDPR_Controller_Abstract $controller */ foreach ( $controllers as $controller ) { $options = $controller->get_options_to_export(); if ( $options ) { $settings[ $controller->get_id() ] = $options; } } nocache_headers(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=ct-ultimate-gdpr-settings-export-' . date( 'm-d-Y' ) . '.json' ); header( "Expires: 0" ); echo json_encode( $settings ); exit; }
An unauthenticated user can export all settings, modify the data and reinject it using the previous vulnerability.
Additional Issues
The export_services
and import_services
functions used to import/export services from/to the Service Manager are also accessible to any user, authenticated or not.
Recommendations
Update immediately if you have version 2.4 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
The vulnerability was reported to the Envato team on January 22. A new version 2.5 (which restricts access to the vulnerable code to admin users only but still lacks a security nonce in the two import functions) was released on January 28.
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