Finding Vulnerabilities in Core WordPress: A Bug Hunter’s Trilogy, Part III – Ultimatum
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 III – Ultimatum” will describe and analyze CVE-2015-5714 and CVE-2015-5715, allowing XSS attacks, as well as another privilege escalation. Both vulnerabilities are now patched, please ensure you upgrade to WordPress 4.3.1 as soon as possible.
In Part I, we showed a privilege escalation vulnerability, allowing any Subscriber to create and edit trashed posts.
In Part II, we discussed how an attacker can use this new surface to perform SQL injection on the attacked server.
For the third and final act, we turn to find a flaw in WordPress page generation, with the intention of injecting JS code to any rendered object on the page. WordPress displays many objects, including different headers and templates, media files, calendar object, news updates, social media widgets, and more.
However, we want our malicious code to display at all times, not as any optional element.
Wait, our privilege escalation from Part I allows us to control post contents – that would be a great target. But isn’t that strictly filtered?
Let’s take a better look.
WordPress allows the use of HTML tags, as well as ‘shortcodes’ – alternative tags that provide easier embedding of dynamic content into the post.
In order to provide this functionality securely, WordPress filters HTML tags using a variant of ‘KSES’ (recursive acronym for ‘KSES Strips Evil Scripts’), which is responsible for allowing a set of white-listed tags, enclosing a set of white listed attributes, in all post content. Any tag or attribute not specifically allowed will get stripped from the submitted content.
‘KSES’ does a pretty good job at that, and disallows all the known evasion tricks. We need to go deeper. What about those shortcodes, then?
Shortcodes are non-HTML tags (enclosed by square brackets ‘[]’) that are designed to enable macro content in posts. WP has several built-in shortcodes to embed galleries, videos, audio and so on. This content is inserted into the post content, replacing the shortcode tag with ‘regular’ HTML tags containing the required functionality.
The resulting HTML generated by these built in shortcodes is parsed and validated. Again, WP does this well. We need to think outside the box for this one.
While reading the code, we noticed an interesting disparity: ‘KSES’ filtering is performed prior to the insertion of data into the DB, and shortcode parsing is performed when printing it to responses.
Let’s make it more visual. A link inside a post can look like this:
while a shortcode can look like this:
Both types consist of a specified tag followed by optional attributes, consisting of <NAME>=<VALUE> pairs, where <VALUE> is delimited by apostrophes (‘) or quotation marks (“).
Remember how HTML and shortcode validation are performed separately? Can we exploit that fact? Time for some sleight of hand debauchery:
When ‘KSES’ will validate this string, it cares only about HTML correctness, which is the ‘<a>’ tag. In our case we have a valid tag with a valid ‘href’ attribute containing an apostrophe. No problems for ‘KSES’.
Let’s look at the same code from the shortcode validation point of view – the highlighted ‘caption’ string is a valid attribute value.
Notice the boundary game we’re playing here using both delimiters (apostrophe and quotation mark), we start the ‘<a>’ tag, and squeeze part of the ‘href’ attribute into the caption attribute, up to the first quotation mark.
But will it blend?
After all the parsing takes place, the final resulting HTML will look like this:
Yes! we caused the ‘href’ attribute to remain unclosed. What we need now is any quotation mark to close that attribute and we can inject an arbitrary HTML <a> attribute.
That’s quite easy, all we need is another ‘<a>’ tag (properly closed for KSES validation), containing our injection in one of its attributes:
generating the following HTML:
… and we have persistent XSS.
One more hurdle before we can truly pride in our 1337 h4x0r skillz – the posts we can control as Subscriber are still in the trash, where they are not being displayed to anyone unless they look for them. We need to somehow publish our controlled posts so that readers will render the injected JavaScript. Hopefully, the readers will even be privileged users.
WordPress validates whether we have permission to publish a post every time the post status is set to ‘publish’ or its relatives ‘future’ & ‘private’. Well, not every time.
Take a look at the XMLRPC method ‘mw_editPost()’, available for any user capable of editing posts (we can do that using the PE, of course):
This function cares to verify that the current user has the capability to publish posts, if the required post status is ‘publish’, denying the request if found incapable. But not all hope is lost, check out the flow for the ‘private’ post status – the same check is outstandingly missing. One forgotten check is all we needed to make our XSS render to all contributing users, including Administrators, of course.
But wait, there’s more! As a convenient bonus, the same goes for setting posts to ‘sticky’, in effect promoting our XSS’d post to always render at the top of the front page for every contributing user in the site.
POC
Post Status Change / Sticking
Post Content XSS
Epilogue
WordPress is the most popular web platform in the world. While it is generally well-secured, we could still find numerous flaws in its core, combined to cause critical implications on millions of web sites today.
These results reiterate an important security lesson; all software is bound to break, regardless of extraordinary popularity, a thousand committers and open source reviewers. If two thousand eyes failed to catch what our two have found, the ‘open source == secure’ argument becomes invalid.
Check Point continues to lead and perform cutting edge defensive and offensive research, responsibly disclosing issues as they arise, while providing early protections for our customers. All research results are shared back with the infosec community in top conferences throughout the year. We are proud of our contributions to public research, and will continue to do so.
If you are interested in joining these efforts, please visit careers.checkpoint.com.