Analysis of WordPress SQL Injection and Privilege Escalation Vulnerability

Analysis of WordPress SQL Injection and Privilege Escalation Vulnerability

September 24, 2015 | NSFOCUS

By: Junli Shen, Network Offensive and Defensive Researcher, NSFOCUS

Analysis of Core WordPress SQL Injection Vulnerability

As a Threat Response Center (TRC) researcher, I conducted a thorough analysis on the “Core WordPress SQL Injection Vulnerability” (CVE-2015-5623 and CVE-2015-2213).

Vulnerability Overview

Previously, I read a tweet about the SQL injection vulnerability found in the core function of WordPress. I was curious and tried to dig the code but only found that the author of the tweet exaggerated its severity. It is true that the application is prone to an injection vulnerability, which, however, can never be triggered by a low-privilege user like “Subscriber” as claimed by the author in his tweet.

This series of blog posts concerning this vulnerability found in WordPress currently consists of two parts: One is about how to write a post marked as trash by a user with Subscriber permissions by bypassing access restrictions, and the other is about how to exploit this SQL injection vulnerability. Phithon from TRSC had explained the two vulnerabilities in his recent article. However, I would like to describe in more details regarding how the two vulnerabilities formed and how they were exploited as well as other details that had not been mentioned by Phithon.

1 Article Submissions by Using Escalated Privilege

Retrieval of _wpnonce

First of all, let’s get to know how the _wpnonce parameter is used in the WordPress’ backend. The parameter is a token used to protect against cross-site request forgery (CSRF) attacks. Most sensitive functions in the backend generate tokens based on the current user information, function name, and operation object ID. Therefore, these functions can hardly work without tokens. The CSRF protection mechanism indirectly makes it almost impossible for a low-privilege user to trigger the SQL injection vulnerability as no token is visible. I will explain more in details in the other follow sections.

The reason we need to talk about _wpnonce first is that we need a token that can be tampered with. This token can be retrieved from post-quickdraft-save in post.php in the backend. Strictly speaking, this method of retrieving tokens is an information disclosure vulnerability, which has been fixed by the vendor in the latest version. Here are the reasons that how this token can be disclosed. A section of code is as follows:

code1From the preceding code, we can see that this function, when finding an error, prints related error information via the wp_dashboard_quick_press function. There is a line in the code on the page generated by this function:


A _wpnonce with the add-post function is generated. Therefore, even if we perform certain prohibited operations, the _wpnonce still appears on the return page.

Submission with Escalated Privilege and Race Condition

In Phithon’s article, it said that authentication bypass vulnerability exists due to the chaotic GET/POST logic. Let’s see the following code in post.php to checkwhethera post exists.


Obviously, post information is obtained first by extracting the post ID from the “post” parameter in GET. If this parameter was unavailable, then the post_ID parameter in POST was extracted. Note that the check on whether a user has the permission to edit the post is conducted in edit_post with the parameter extracted from POST:


The “if” decision at the bottom of this section of code shows that whether the current user is permitted to edit the post. The final operation for this decision is performed in map_meta_cap:


It is evident that if the post does not exist, this breaks the switch and the $caps variable is returned when the function ends. However, $caps is defined as an empty array when the function begins, so the array returned here is also empty. Now let’s return to the has_cap function that calls map_meta_cap to see subsequent operations:


The foreach statement in line 20 checks whether all elements in $caps exist in $capabilities. If there is any missing in $capabilities, the result is “return false”. However, as $caps is an empty array, we can easily obtain a “return true” result, thus successfully bypassing permission checks. Now we know that we can try to update a nonexistent post by exploiting this flaw.

However, the problem is that it is meaningless to update a nonexistent post because the database will definitely report an error when executing SQL statements. Then how can we successfully create a post?

Between the permission check and the database’s execution of SQL statements, the following code exists in post.php:


If an array named tax_input exists in POST, values in the parameter are separated with commas. For each separated value, a “select” query is performed. Imagine what would happen when we used the ID of the latest post + 1 as the current post_ID and add a lot of information in tax_input. This would cause repeated “select” queries. At this time, we can insert a post (whose ID is the ID of the latest post + 1) and now the subsequent update operation becomes quite meaningful.

Here comes the last question: How can we insert a post? Anyone still remember the function of post-quickdraft-save? It can be used to quickly save a draft.

The following is an image taken from Phithon’s article which could familiarised with the process.



What the preceding debugging impresses me the most is race condition, which is how to insert and update a post at an appropriate time. If it was an early insertion of the post, it would cause the user to fail the permission check. When it was a late insertion, it would make the update operation meaningless. According to my experience, I would suggest to leave as long time as possible for the process to complete permission checks and to put as much information as possible in tax_input. Therefore you can successfully insert a post between the permission check and the database’s execution of SQL statements.

There is also another factor that needs to be considered. Each user can save only one draft. The vulnerability reporter suggests waiting for a week for the draft to be automatically deleted. Luckily, _wpnonces can be stored for one more day, which allowing us to retrieve a _wpnonce a day before the expected deletion date. Phithon provides a better suggestion which is to use two accounts, one for inserting a post and the other for updating the post.

Privilege escalation vulnerability consists of three flaws: information disclosure, permission check bypass, and program execution time manipulation, which are closely linked with each other.

2 SQL Injection Vulnerability

Revision Trick

The vulnerability reporter first mentioned in his article a trick regarding revisions, which explains how further attacks are launched after a post is written with escalated privileges. However, as the reporter did not explain this trick thoroughly, it is impossible to trigger the SQL injection vulnerability with a subscriber account in which the trick also wasn’t mention in Phithon’s article. Here, I would like to explain how this trick works.

The following is an excerpt from the vulnerability reporter’s article:

“Revisions are records of drafts or published updates to any post. Internally, WordPress implements revisions as complete posts and stores them in the posts database table with ‘post_type’ set to ‘revision’. Each revision has a ‘post_parent’ field, pointing to the original post the revision is based on.

When attempting to edit a revision, the validation check is actually made following the ‘post_parent’ pointer, instead of the revision itself. Turns out, this provides the unique property we were after; if we create a child revision in addition to our original post, we can set its status to anything other than ‘trash’, while keeping the original post in the trash.

Using this trick, we can edit this ‘puppet revision’ and freely add comments, while the original trashed post in the one being checked to allow our actions.”

Based on the preceding sections about the write of a post with escalated privileges, we can see that the author described this trick for the purpose of editing the revision of the trashed post and manipulating comments on this post. This is because a post, even if submitted by the current Subscriber user, cannot be edited by this user, while the revision can. Therefore, according to the reporter, we can use this trick to continue subsequent operations.

Cause of the Vulnerability

This vulnerability is, in nature, second-order SQL injection, which occurs because comments of a post are restored together with the trashed post and the comment restoring code contains directly spliced contents that are user-controllable. The following is the code in question:


From the code, we can see that the $status and $comments values are spliced into the SQL statement and they can be manipulated by users. User-supplied data is stored in the database after being sanitized. Then such data can be directly taken from the database and spliced into the SQL statement. Therefore, if the attack statement is restored when extracted from the database, this is a standard second-order SQL injection.

Vulnerability Exploitation

After understanding the trick of revisions and the cause of the vulnerability, we find it quite easy to exploit this vulnerability:

Step 1 Comment on a revision of a post.

Step 2 Edit the comment status by inserting an injection attack statement.

Step 3 Trash the revision.

Step 4 Restore the original post of the revision.

All seems to be normal. However, do you still remember the _wpnonce parameter I mentioned previously? Yes, that’s it! The author did not say anything about how to retrieve this _wpnonce, which makes steps 1, 2, and 4 impossible to process.


If you consider it a direct method to conduct a second-order injection by reading the stored content failing to be sanitized from the database, I believe that the author’s idea of launching an attack by editing revisions of a trashed post will definitely wow you.

The author claimed that the status of revisions could be set to anything other than “trash”. However, according to my research analysis it is not true. The status of a post can be “publish”, “future”, “private”, “inherit”, “auto-draft”, “attachment”, “draft”, “pending”, or “trash”. We can change the status of revisions only to “inherit”, “pending”, or “draft”. If we could change the revision status to “private”, steps 1 and 2 would be able to be performed in the foreground.

3 Conclusions

Through analysis of this vulnerability, I realize that the token mechanism of WordPress is really effective, which can protect against not only CSRF but also other types of attacks.

The logic of GET/POST operations is determined by web code and tends to become chaotic, as is the case with the code of WordPress, especially when permission checks need to be performed.

The second-order injection is an issue frequently talked about. It happens because some systems trust recorded data unconditionally and finally even forget that certain entries were actually written by users.

4 References

[1] Finding Vulnerabilities in Core WordPress: A Bug Hunter’s Trilogy, Part I

[2] Finding Vulnerabilities in Core WordPress: A Bug Hunter’s Trilogy, Part II – Supremacy

[3] Analysis of WordPress 4.2.3 Privilege Escalation and SQL Injection Vulnerability (CVE-2015-5623)