A logic flaw in the way WordPress created blog posts allowed attackers to access features only administrators were supposed to have (CVE-2018-20152). This lead to a Stored XSS and Object Injection in the WordPress core and more severe vulnerabilities in WordPress’s most popular plugins Contact Form 7 and Jetpack.
WordPress is at the core a Blogging Software that allows user to create and publish posts. Over time, different post types were introduced, such as pages and media entries (images, videos etc.). Plugins can register new post types, such as products or contact forms. Depending on the purpose of the post type a plugin registers, it offers unique and new features. For example, a contact form plugin might allow to create a contact form with a file upload field (e.g. for resumès). The user creating the contact form can define which filetypes should be allowed. An evil user could also allow php files to be uploaded and then execute arbitrary code on his site. This is not an issue per se, as plugins can restrict access to the post types they register to administrators only and trust WordPress to handle that restriction for them. The privilege escalation discussed here allows lower privileged users to bypass the security checks implemented by WordPress and create posts of any type and misuse the features of custom post types. This leads to a Stored XSS and Object Injection in the WordPress core. Depending on the plugins installed, more severe vulnerabilities can be exploited. When for example WordPress’s most popular plugin, Contact Form 7, which has over 5 million active installs, was used, attackers were able to read the database credentials of the target Wordpress site. Most of the top WordPress plugins are vulnerable to this privilege escalation.
To register new post types, plugins make a call to
register_post_type() with the name of the new post type and some meta information.
How custom post types are secured
Each post type has its own editor page (e.g. example.com/wordpress/wp-admin/?page=example_post_type_editor).
If the plugin developer decides that only administrators should be allowed to use the post type of the plugin, he will simply check if the user is an administrator at the top of the page and end execution otherwise.
WordPress post submission
Although all registered post types have their own editor, they can all use the WordPress post submission API and insert and update the posts with the WordPress function
wp_write_post(). The function takes user input such as
$_POST['post_content'] so it knows how to process the post.
In the first step of WordPress’s post submission process, WordPress has to know if the user wants to edit an existing post or create a new one. To do this, WordPress checks if the user has sent an ID of a post. WordPress will allow either
$_POST['post_ID']. If an ID is set, the user wants to edit an existing post with that ID. Otherwise the user wants to create a new post.
In the next step, WordPress has to determine which post type the user is trying to create. If a post ID has been sent, WordPress will pull the
post_type column from the database from the
wp_posts table. If the user wants to create a new post, the target post type will be
Once WordPress knows the post type of the post the user is trying to create or edit, it will check if the user is actually allowed to use that post type. WordPress does this by verifying a nonce that can only be obtained from the editor page of the post type in question.
To do the nonce verification, WordPress will utilize the following code:
$post_type was a post, the
$nonce_name would be add-post. If
$post_type was example_post_type, the
$nonce_name would be add-example_post_type. This nonce can only be obtained by users that have the capability to create these post types, because only these users can access the editor page of that post type, which is the only way to get the nonce.
Although lower privileged attackers, such as attackers in the contributor role, can’t access the page and nonce of the example post type, he can always get the nonce of a normal post, which has the simple internal post type post. This means he could simply set the post ID to a post with the post type post. This would allow him to pass the nonce verification.
However, this method only allows updating an existing post and it is not possible to overwrite the
post_type of a post. If a post ID is set, WordPress will remove the
post_type from the parameters before updating the post.
However, WordPress will only remove the
$post_type parameter if
$_POST['post_ID'] is set. An attacker can send a post ID via
$_GET['post']. If an attacker sends a post ID via
$_GET['post'] the following will happen:
- WordPress sees that a post ID is set and pulls its post type from the database.
- WordPress checks if the attacker sent a valid nonce for that post type (which he can always get for a normal
- Once the nonce check is passed WordPress determines if it should call either
wp_insert_post(). It does this by checking if
$_POST['post_ID']is set. If it is,
wp_update_postwill be called and the
$post_typeparameter will be removed, thus not allowing the attacker to overwrite the post type. If it is not set, WordPress will call
$_POST['post_type']as the post type of the new post.
Because WordPress forgets to also check
$_GET['post'] in the third step, an attacker can pass the nonce verification and create a new post with an arbitrary post type. The code snippets shown are simplified and abstracted, the real code spans across multiple files and function calls, which makes the process prone to such flaws.
Exploitation: Reading the wp-config.php via Contact Forms 7
By now you should understand that lower privileged users can abuse this bug to create posts of any type and that the impact on a target site depends on what plugins are installed and what features the post types that come with the installed plugins offer.
To give a concrete example, it was possible for attackers in the role of a contributor to abuse a feature in WordPress’s most popular plugin, Contact Form 7, to read the contents of the wp-config.php file of the target site. This file contains database credentials and encryption keys.
Up to version 5.0.3 of Contact Forms 7, it was possible to set local file attachments. When an admin creates a contact form and a visitor of the page contacts him through it, an email is sent to the administrator with all the data the user has entered. Local file attachments are a setting for a contact form where administrators can define local files to be sent as an attachment with each email.
This means an attacker could simply create a new contact form, set the local file attachment to
../wp-config.php and set the email to which the data should be sent to his own, submit the form and then read the contents of the most important WordPress file.
Fix for plugin developers
Plugin developers should further tighten the security of their plugins by explicitly setting the
capability_type parameters when calling
register_post_type(). In the WordPress documentation you can find more information on securing post types with
XMLRPC and REST API of WordPress
It is possible to create posts via the XMLRPC and the REST API of WordPress, which do not perform nonce verification for a specific post type. However, when creating posts via these APIs, it is not possible to set arbitrary
post meta fields. Most vulnerabilities in plugins that we have discovered are only exploitable if users can set these post meta fields.
|2018/08/31||Reported the vulnerability to Contact Form 7 via the contact form on their website|
|2018/09/02||Reported the vulnerability to WordPress on Hackerone|
|2018/09/04||Contact Form 7 fixes the vulnerability|
|2018/09/27||WordPress security team triages the vulnerability on Hackerone|
|2018/10/12||WordPress proposes a patch on Hackerone|
|2018/10/18||We verify the patch|
|2018/12/13||WordPress releases a patch in version 5.0.1|