Security vulnerabilities in UserPro plugin for WordPress

UserPro plugin for WordPress versions up to 2.28 have multiple security vulnerabilities that expose the website they are installed on to a wide scope of attack vectors. The plugin has 27 occurrences a procedure call that is extremely insecure (extract($_POST)) and a futher 57 probably insecure uses of extract().

Upon discovering these security vulnerabilies I reported them to Envato, who sells the plugin on behalf of the author on CodeCanyon.net. I gave Envato a three month deadline for responsible disclosure. Envato promptly took the plugin off their store and contacted the author. The author promptly fixed all the known vulnerabilities and released a new secure version.

I have not reviewed the newest version. I did review a work in progress and was happy with the author’s progress and improvements.

The report that I sent to to Envato follows.

Want a security audit or review of your own website or code?


This report documents multiple security vulnerabilities in the UserPro plugin for WordPress.

The scope of attack vectors is extremely wide. The plugin has 27 occurrences a procedure call that is certainly extremely insecure (extract($_POST)). It has a futher 57 uses of extract(), many of which look to be approximately equally dangerous after a quick review.

For this report, only one instance of extract($_POST) was investigated more thoroughly; userpro_process_form(). That occurrence alone has an extremely wide scope of attack vectors involving 500+ lines of code.

This report documents just three specific reproducible exploits that allow an untrusted user to:

  1. Delete any user
  2. Publish a post, specifying the title, body and any author
  3. Make themselves an administrator

However there are so many ways to exploit UserPro plugin that it is probably not possible to document them all. In order to be secured, a large portion (at least) of the plugin would need rewriting.

Impact

Most (if not all) WordPress websites that have installed, activated and configured (in the common/usual way) the UserPro module are probably vulnerable. According to the plugin’s main distribution page as of 25 March 2015, it has been sold 8,292 times, and 8,753 times on 1 May 2015.

Of these sales, some licences are probably used for multiple websites (even though the license is only for one website). Other licenses may not be in use any longer.

A reasonable estimate of the vulnerable websites might be five to fifteen thousand.

Responsible disclosure

In the best interests of the security of WordPress website users and owners, this report will be published publicly if a fix is not available by 24 June. That is three months after the vulnerabilities were reported. This is consistent with two-way responsible disclosure.

The vulnerabilities were discovered and researched by Bevan Rudge. The exploits, this report and the accompanying patch were also developed by Bevan Rudge. [email protected] www.JS.geek.nz

The author of UserPro plugin is Deluxe Themes: userproplugin.com. Deluxe Themes has no public security policy of its own.

WordPress has a procedure for reporting security vulnerabilities in WordPress plugins. As per that policy, [email protected] was notified on Tuesday 24 March of these vulnerabilities (without detail). However because that team only has capacity to deal with WordPress plugins hosted on WordPress.org, it was referred on to Envato.

The UserPro plugin is sold on the codecanyon Envato Market. Envato has a procedure for reporting security vulnerabilities of products it sells, called the Helpful Hacker Program.

These vulnerabilities were reported (without detail initially) to Envato’s Helpful Hacker Program on Wednesday 25 March.

Dates are NZDT, UTC+13.

Delete any user

This exploit requires only basic knowledge of how forms work on the internet, and how to use a web browser’s web dev tools to modify them.

  1. Register two new users that are not administrators; “Andy Attacker” and “Vicky Victim”.
  2. Note Vicky’s user ID.
  3. Install and activate the “UserPro” plugin.
  4. Create a page “UserPro profile/login” with shortcode “[userpro template=view]” and note its URL.
  5. Login as “Andy Attacker”.
  6. Navigate to the “UserPro profile/login” page.
  7. Press “Delete Profile”.
  8. Use browser tools to find element like <input type="hidden" name="user_id-N" id="user_id-N" value="ID">.
  9. Change the value="" attribute to the Vicky’s user ID.
  10. Press “Yes, delete this profile!”.
  11. Press “Confirm Deletion”.
    • Expected behavior: A generic validation error.
    • Actual behavior: A message “This account has been deleted successfully” is briefly displayed then the browser is redirected to the homepage.
  12. Press the browser’s “back” button.
  13. Press the browser’s “reload” button.
    • Expected behaviour: User is not logged in.
    • Actual behaviour: User is still logged in as Andy.
  14. Login as Admin.
  15. Navigate to “All Users”
    • Expected behaviour: “Vicky Victim” should be a user. “Andy Attacker” should not be a user.
    • Actual behaviour: “Vicky Victim” is not a user. “Andy Attacker” is a user.

cURL exploits

The other two exploits are more elaborate and would be difficult to reproduce manually in a web browser.

As in the “Delete any user” exploit, these validate the security nonce for the user “delete” action (known as $template in code). Unlike the “Delete any user” exploit, they manipulate the vulnerable UserPro code into taking a different action instead of deleting a user.

cURL template

Both exploits use this template for a curl command:

curl "http://${HOST}/wp-admin/admin-ajax.php" -H "Cookie: wordpress_logged_in_${HASH}=${COOKIE}" --data "action=userpro_process_form&_myuserpro_nonce=${NONCE}&unique_id=${ID}&template=delete&template-9=${ACTION}&${DATA}"

Note that template-9 can actually be called anything matching regex template-.+.

Note the lack of wordpress_HASH and PHPSESSID cookies. This may or may not be a security issue. If it is, it may or may not be related. This report did not investigate that.

Template variables

  • HOST: The hostname of the website to be exploited.
  • HASH: The has component of the wordpress_logged_in_* cookie.
  • COOKIE: The value of that cookie, for any logged in user, such as “Andy Attacker”.
  • NONCE and ID: The nonce and unique ID can be retrieved from the delete form (as in the “Delete any user” exploit) for the logged in user. The names are _myuserpro_nonce and unique_id respectively.
  • ACTION: provided by each exploit’s example code below. See below.
  • DATA: Form data, provided by each exploit’s example code below.

Values for HASH, COOKIE, NONCE and ID can all be retrieved from cookies and the form on the “UserPro profile/login” > “Delete” page for any logged in user.

Exploitable values for ACTION are:

  • publish
  • delete
  • change
  • reset
  • login
  • edit
  • register

Publish new content

  • ACTION: publish
  • DATA: user_id=1&post_title=Hacked&userpro_editor=This post was created by a user without content creation priveleges.

Afterwards, a new post “Hacked” with content “This post was created by a user without content creation priveleges.” can be seen at /wp-admin/edit.php (Admin > “Posts”).

All variables in DATA can be set to any value that would normally validate. Additional variables are also supported.

Role escalation

  • ACTION: edit
  • DATA: user_id=${UID}&role=administrator

Afterwards, the specified user is an administrator.

UID must be set to the user ID corresponding to the nonce and cookie. Additional variables are also supported.

Explanation

The steps above show just some of many possible exploits of UserPro plugin. These three examples all exploit function userpro_process_form(). There are many other exploitable vectors. Only userpro_process_form() is investigated and explained in this report.

userpro_process_form() is hooked to the wp_ajax_nopriv_userpro_process_form and wp_ajax_userpro_process_form actions. It checks a nonce to prevent some very basic exploits, then executes the following dangerous code (reformatted):

extract($_POST);
foreach ($_POST as $key => $val) {
    $key = explode('-', $key);
    $key = $key[0];
    $form[$key] = $val;
}
extract($form);

This causes all posted values to become local variables in the userpro_process_form() function. For example, using the cURL template with DATA of a=b&c=d&c-3=e is equivalent to $a = 'b'; $c = 'd'; when the first extract() executes, then $a = 'b'; $c = 'e'; when the second extract executes.

Extracting untrusted data is explicitly warned against in the PHP documentation for extract():

Warning: Do not use extract() on untrusted data, like user input (i.e. $_GET, $_FILES, etc.). If you do, for example if you want to run old code that relies on register_globals temporarily, make sure you use one of the non-overwriting flags values such as EXTR_SKIP and be aware that you should extract in the same order that’s defined in variables_order within the php.ini.

To make matters worse;

  • This code extracts the untrusted data twice, allowing attackers to pass security validation, while subsequently still manipulating all variables in scope.
  • The global variable $userpro can be overwritten.
  • Arrays can be specified using the cURL template with data like my_array[key1]=value1&my_array[key2]=value2
  • $form can be overwritten completely. E.g. form[key1]=value1&form[key2]=value2
  • Superglobals like $_POST and $_SERVER can probably be overwritten.
  • The function is more than 500 lines long, meaning that the affected scope has a very wide range of possible effects. (Multiple smaller functions would help to limit the severity of the vulnerability be reducing the amount of code using the spoilt scope.)

A quick review of some of the other 82 uses of extract() in UserPro plugin suggests there are dozens of similarly-dangerous uses of extract().

Distinct vulnerabilities

The “Delete any user” exploit is technically a distinct security vulnerability, separate from the dangerous extract() calls and the other two example exploits. It could be addressed independently of the dangerous calls to extract().

Specifically, get_userdata($user_id) is equivalent of get_userdata($_POST['user_id']). So the solution could be to use global $current_user instead, and remove the user_id from the form.

However all vulnerabilities discussed in this report should be solved together to minimize fallout from any announcement or security release, since publication of either vulnerability makes all of the others easy to discover.