In this series of blog posts, Check Point vulnerability researcher Netanel Rubin tells a story in three acts – describing his long path of discovered flaws and vulnerabilities in core WordPress, leading him from a read-only ‘Subscriber’ user, through creating, editing and deleting posts, and all the way to performing SQL injection and persistent XSS attacks on 20% of the popular web.
“Part II – Supremacy” will describe and analyze CVE-2015-2213, a SQL injection vulnerability recently patched in WordPress 4.2.4.
In “Part I – Identity”, we showed how any Subscriber user could bypass multiple permission checks and access code to create and edit posts, utilizing a deterministically executed race condition. With our new, vastly broadened, elevated permission attack surface, we continue our hunt for flaws allowing further exploitation.
Examining the new surface, we can now reach the functionality of editing all existing comments on our newly created post. As authors of the post, the ‘editedcomment’ action in comment.php conveniently allows us to change most of the comment’s DB fields, including the comment’s author, content, and even its approval status.
Before we can do that, we first have to create a comment for our post. This seemingly simple task proves to be quite difficult. Our post is created and edited in the trashed status (which is one of the conditions for our privilege escalation to work – see Part I), while WordPress strictly disallows commenting on trashed posts.
Such validation code can be found in many places, including:
The code first checks whether the post we’re trying to comment on exists, then whether we can edit it, and finally verifies that it’s not a draft, pending, or trashed post. In order to bypass this check, we need to get creative again. For that, let us introduce you to post revisions.
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.
Now that we have a comment, we are able to edit comment fields, as mentioned previously. Although the code allows editing almost all comment fields, our input gets strictly filtered and sanitized against various attacks such as SQL injections and Cross-Site Scripting (XSS) attacks, effectively blocking the “standard” attack vectors. We need to go deeper.
Let’s examine the process of trashing and un-trashing a post. These are performed using calls to ‘wp_trash_post()’ and ‘wp_untrash_post()’. Let’s begin with trashing:
First, the code checks if the post exists and ensures it’s not already trashed. Then, it changes its status to ‘trash’ and updates the post in the DB. Finally, it trashes the post comments. That looks pretty straightforward. But what does ‘wp_trash_post_comments()’ do?
The code extracts all the post comments from the DB and stores all comments statuses (such as ‘approved’, ‘spam’, and so on) in an array inside a post “metadata” field named ‘_wp_trash_meta_comments_status’.
This functionality is useful in case a post’s author regrets the trashing of the post and wants to restore it. In this case WordPress re-sets the comments statuses to their previous status prior to the trashing. But how is that performed, exactly? For that, let’s check out ‘wp_untrash_post_comments()’:
Whoah! a surprising unprepared SQL UPDATE statement, right in core WordPress! This statement restores the ‘comment_approved’ field to its previous value prior to the trashing operation. But do we control that value? As seen above, it is extracted directly from the ‘_wp_trash_meta_comments_status’ metadata field. Hold on, wasn’t that one escaped before? Well, yes, when it was inserted. The get_post_data() DB retrieval returns our data, unescaped, directly into the UPDATE statement.
That escalated quickly.
We now have a SQL injection in a convenient update statement. Still, in order to trigger it, we need to figure out – how do we trash an already trashed post?
Actually, we don’t need to. All we have to do is somehow create the ‘wp_trash_meta_comments_status’ metadata attached to our post containing our comment status injection, and then un-trash it.
For that, we are going to reuse the same trick we used to comment on our post – we’re going to use a puppet revision. When we comment on our puppet, the comment’s post ID is our revision’s post ID, so when we trash our revision its own comments are in fact the ones being trashed and inserted into the ‘wp_trash_meta_comments_status’ metadata. And yet, when WordPress inserts metadata to a post revision, it follows to insert that metadata under the revision post parent, which is our original post, allowing us to un-trash it and trigger the SQLi. QED
The security implications for this SQL injection are severe. Simple exploits could have an attacker approve any comment, including reassigning it to a different post. A more destructive option would be rendering the commenting system useless by adding a comment with a malicious ID that breaks the auto-increment ID assignment. With some effort, this can also be converted into an XSS in comment display.
This vulnerability was assigned CVE-2015-2213, and was fixed in WordPress 4.2.4.
Our next and final piece for this trilogy will describe a distinct Cross-Site Scripting (XSS) attack in post content, which is still a 0-day (unpatched) as of this writing, affecting every WordPress installation.
We are working in coordination with WordPress core developers, and intend to release this information only after a patch is made available in the next WordPress security release (expected this week).
We would like to thank the WordPress core developer team for their responsible response and open communication efforts; we wish all vendors responded with similar diligence.
Create a Comment
Edit a Comment