Unauthenticated options change vulnerability in WordPress WP Private Content Plus plugin.

[+] Last revision: August 31st, 2019

The WordPress WP Private Content Plus plugin, which has 9,000+ active installations, was prone to an unauthenticated options change vulnerability in version 1.31 and below.

Reference

CVE-2019-15816

Summary

WP Private Content Plus is a plugin used to protect important site content from specific user roles or group of selected users. It was prone to an unauthenticated options change vulnerability that could lead to website redirection, stored XSS (front-end and back-end), information disclosure and denial of service.

Unauthenticated options change

In the “classes/class-wppcp-settings.php” script, line 17, the plugin registers the save_settings_page function via the init hook, which fires after WordPress has finished loading:

add_action('init', array($this,'save_settings_page') );

This function is located lines 75-94:

/*  Save settings tabs */
public function save_settings_page(){

    if(!is_admin())
        return;
        
    $wppcp_settings_pages = array('wppcp-settings','wppcp-search-settings-page','wppcp-password-settings-page','wppcp-global-restrictions','wppcp-upme-settings',
        'wppcp-security-settings-page');
    if(isset($_POST['wppcp_tab']) && isset($_GET['page']) && in_array($_GET['page'],$wppcp_settings_pages)){
        $tab = '';
        if ( isset ( $_POST['wppcp_tab'] ) )
           $tab = $_POST['wppcp_tab']; 

        if($tab != ''){
            $func = 'save_'.$tab;
                
            if(method_exists($this,$func))
                $this->$func();
        }
    }
}

The code will check if a “save_ . $_POST['wppcp_tab']” function exists in the current class and, if any, will call it. There’s no capability check and no security nonce. We can see that the plugin seems to rely only on the unfamous WordPress is_admin function that, unlike its name tends to suggest, does not check whether the user is an administrator or not.

There are 13 other “save_*” functions in the script that can be called from the above code and that are used to save the plugin settings. They all lack capability check too. Here are two of them, for instance:

public function save_wppcp_section_global_post(){
    global $wppcp;

    if(isset($_POST['wppcp_global_post_restriction'])){
        foreach($_POST['wppcp_global_post_restriction'] as $k=>$v){
            $this->settings[$k] = $v;
        }      
               
    }
        
    $wppcp_options = get_option('wppcp_options');
    $wppcp_options['global_post_restriction'] = $this->settings;

    update_option('wppcp_options',$wppcp_options);
    add_action( 'admin_notices', array( $this, 'admin_notices' ) ); 
}

public function save_wppcp_section_global_page(){
    global $wppcp;

    if(isset($_POST['wppcp_global_page_restriction'])){
        foreach($_POST['wppcp_global_page_restriction'] as $k=>$v){
            $this->settings[$k] = $v;
        }      
               
    }
        
    $wppcp_options = get_option('wppcp_options');
    $wppcp_options['global_page_restriction'] = $this->settings;
    update_option('wppcp_options',$wppcp_options);
    add_action( 'admin_notices', array( $this, 'admin_notices' ) ); 
}

An unauthenticated user can modify all settings, which can lead to:

∙ Information disclosure, by gaining access to all protected content.

∙ Redirecting visitors to an external website by enabling the redirection from the “Security Settings” menu. Redirection is performed via the “Location:” header:

HTTP/1.1 302 Found
Server: nginx/1.14.2
Date: Thu, 23 Aug 2019 11:35:28 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Redirect-By: WordPress
Location: https://evil.com/

∙ Denial of service, by setting $_POST['wppcp_tab'] to “settings_page”, the save_settings_page function will keep calling itself in a loop that will only break when PHP will run out of memory and crash:

[23-Aug-2019 11:45:59 UTC] PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 262144 bytes)

∙ Stored XSS in the front-end and back-end by enabling the plugin “Password Settings” option:
In the “templates/global-password-form.php” script, $protected_form_header and $protected_form_message are echoed without being sanitized lines 52 and 57 respectively:

<div class="wppcp_panel_title"><?php echo $protected_form_header; ?></div>
<?php if($password_protect_error != ''){ ?>
   <div class="wppcp_panel_error"><?php echo $password_protect_error; ?></div>
<?php } ?>

<div class="wppcp_panel_message"><?php echo $protected_form_message; ?></div>

An attacker can use either field to injected Javascript code on the blog front-end:

In the “admin/templates/password-global-settings.php” script, $password_form_title and $protected_form_message are echoed without being sanitized lines 40 and 50 respectively:

<tr>
   <th><label for=""><?php echo __('Password Form Title','wppcp'); ?></label></th>
   <td style="width:500px;">
      <input type='text' name="wppcp_password_global[password_form_title]" id="wppcp_password_form_title"  value="<?php echo $password_form_title; ?>" />
                            
      <div class='wppcp-settings-help'><?php _e('This setting is used to define the title of Password Entering form for users.','wppcp'); ?></div>
   </td>
                    
</tr>

<tr>
   <th><label for=""><?php echo __('Password Form Message','wppcp'); ?></label></th>
   <td style="width:500px;">
      <textarea style='width:100%;min-height:100px;' name="wppcp_password_global[password_form_message]" id="wppcp_password_form_message"  ><?php echo $password_form_message; ?></textarea>
                            
      <div class='wppcp-settings-help'><?php _e('This setting is used to define the message of Password Entering form for users.','wppcp'); ?></div>
   </td>
                    
</tr>

This can be used to target the administrator in the back-end:

Hacked?

If you were hacked because of that vulnerability, you need to delete the content of the wppcp_options row in the WordPress wp_options* table.

*If you changed your WordPress database prefix, replace wp_ with the correct one.

Timeline

The vulnerability was discovered and reported to the wordpress.org team on August 23, 2019.

Recommendations

Update as soon as possible if you have version 1.31 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.

Stay informed about the latest vulnerabilities in WordPress plugins and themes: @nintechnet