Security Risk: Easy / Remote
DREAD Score: 8.4
Vulnerability: SQL Injection / PHP Object Injection
Patched Version: 3.4
While investigating the Duplicate Page plugin, we have discovered a dangerous SQL Injection vulnerability.
Though the plugin wasn’t abused externally, the vulnerability impacted over 800,000 sites. Its urgency is defined by the associated DREAD score that looks at damage, reproducibility, exploitability, affected users, and discoverability.
Key contributors to the criticalness of this vulnerability are that it’s easy to exploit and by any user with an account on the vulnerable site (regardless of the privileges they have – e.g., subscribers).
Current State of the Vulnerability
Duplicate-Page, as its name implies, is a plugin that makes it easy to duplicate pages on a site.
The bug has been fixed on the 3.4 release. If you use an older version, you should update as soon as possible.
Successful exploitation of this bug may allow attackers to steal sensitive user information like password hashes and in certain scenarios, lead to a complete compromise of your WordPress installation. Depending on what other plugins are active on the site, bad actors may escalate this bug into a PHP Object Injection vulnerability.
- March 22nd, 2019 – Attempt to contact the author
- March 22nd, 2019 – Author answers, we disclose the vulnerability to them
- March 25th, 2019 – Author sends the patch for review, which we do on the same day
- April 1st, 2019 – Fix is available on wordpress.org
If activated, the Duplicate-Page plugin adds a new link to the Pages list options, named “Duplicate this”. A quick look at its destination URL shows that it points to http://[vulnerable ite]/wp-admin/admin.php?action=dt_duplicate_post_as_draft&post=[the post id].
The action parameter is generally used by the WordPress admin_action_ hook to determine what action hook to run, very similarly to how the wp_ajax_ hook works.
The admin_action_ hook is executed at the very end of wp-admin/admin.php. This file is included on practically all pages in /wp-admin/ that provides a user interface, including wp-admin/index.php. As a result, it may be executed when any kind of users are on their dashboard or even editing their personal information.
Plugins relying on this feature to perform strictly administrative tasks should always be doing additional privilege and nonce check in order to make sure it is not executed by less privileged malicious users.
With that in mind, let’s look at how the action hook is implemented. From the screenshot above, we understand that the method being hooked here is dt_duplicate_post_as_draft.
Investigating that method reveals a few interesting points:
- It doesn’t apply any privilege or nonce check.
- It can use either of $_GET[‘post’] or $_POST[‘post’] to grab the ID of the post we want to duplicate.
- The only check being applied is on get_post()’s return value. If it returned anything other than NULL, it is assumed that everything worked properly and we can continue the page duplication routine.
Unfortunately, get_post() internally uses the get_instance() method of the WP_Post class to find posts using their IDs, which as you may see from this snippet, casts $post_id to an integer before querying the database!
This means that no matter what $post_id contains, it will always retrieve a valid post if the variable starts with that post’s ID. For example: “123 hello world” would grab a post whose ID is 123.
If this technique sounds familiar to you, we used a very similar trick in the WordPress Content Injection vulnerability patched on version 4.7.2.
Continuing our analysis of the dt_duplicate_post_as_draft method, we see that $post_id is directly appended to the $wpdb->get_results() query. Since $post_id contains unsanitized user input, this leads to a SQL Injection vulnerability.
Looking even further, we also see that the values returned by this vulnerable query are reused in the foreach() loop and re-appended in the subsequent INSERT SQL query. Because we can control the $meta_key variable by using SQL UNION statements in the first vulnerable query and it is not escaped by addslashes(), we may inject arbitrary values in the postmeta table.
This is a huge deal since all of the post meta in there can be unserialized when retrieved using the get_post_meta() function, which would lead to a PHP Object Injection attack under certain circumstances.
If you are using vulnerable versions of this plugin, updating it should become your priority. Users of the Sucuri Firewall were protected from these issues from the very beginning.