Security Risk: Low
Exploitation Level: Hard / Requires at least Contributor privileges
DREAD Score: 4/10
Vulnerability: Stored XSS
Patched Version: 4.7.3
As you might remember, we recently blogged about a critical Content Injection Vulnerability in WordPress which allowed attackers to deface vulnerable websites. While our original disclosure only described one vulnerability, we actually reported two to the WordPress team. As it turns out, it was possible to leverage the content injection issue to achieve a stored cross-site scripting attack. This issue was patched in WordPress 4.7.3.
Are You at Risk?
This vulnerability has been present in WordPress for quite a while, well before 4.7.
Combined with the recent content injection vulnerability we found, it’s possible for a remote attacker to deface a random post on the site and store malicious Javascript code in it. This code would be executed when a visitors view the post and when anyone edits the post from the WordPress dashboard. As a result, an administrator tries to fix the defaced post, the would unknowingly trigger the malicious script, which could then be used to put a backdoor on the site and create new admin users.
The content injection vulnerability was patched in version 4.7.2, and as such, the risk posed by this XSS vulnerability is much less severe. In addition, only users with certain privileges, like contributors, can currently exploit the issue.
Technical Details
Being able to edit any single post on a site is a serious vulnerability by itself, but there is a notable drawback for attackers. Thanks to the limitations of the WordPress function wp_kses(), a malicious actor was very limited in terms of the HTML tags that could be inserted in the post.
For example, if an attacker injected a post containing a simple alert script like:
<script>alert(1);</script>
This would result in a post only containing the text “alert(1)”.
We felt it was our duty as security professionals to dig deeper and see how far an attacker could go with this vulnerability. In the process, we discovered the embed shortcode could produce a more dangerous scenario.
In a nutshell, the embed shortcode takes an src (source link) attribute and tries several regular expressions in order to find out which handler method the script should use so the source link can be embedded properly. The one that caught our attention was youtube_embed_url which was registered with the following regex:
The only important detail here is that the last capturing group will catch anything that isn’t a “slash” character; this could include minus-than and greater-than characters too. We checked what the wp_embed_handler_youtube callback function did with this chunk of text. What we found is that it basically generates a Youtube URL and concatenates the last capturing group – https://youtube.com/watch?v={$LASTCAPTUREGROUP}.
After doing this, it pushes the generated link to wp_embed and its autoembed()method.
That’s where things gets really interesting. If $content (the embed URL) contains something like this:
https://youtube[.]com/watch?v=abc<svg onload=alert(1)>
…none of these regular expressions will match, so autoembed() simply returns the $content URL.
In an attack scenario, the story would end here, but things got a tad more complex when we remembered that everything we send to our post would be sanitized by the HTML sanitization function wp_kses().
Even if we tried sending a shortcode like the following:
It would be shrunk down to:
However, we also looked at the shortcode_parse_atts function, which is responsible for parsing shortcode parameters.
In short, it uses a regular expression that matches all pairs of keys and values in a shortcode, and then it creates an associative array out of it while ensuring each of the attributes values is passed through the stripcslashes function. This allows us to send escape sequences like \x41 (A), or \x3c (<)
All that was left in order to get our XSS working was combining all the previous advances into one simple shortcode:
With this, we would get a working stored XSS exploit.
In Conclusion
If you have disabled automatic updates on your website, update to WordPress 4.7.3 as soon as possible!
In the event where you cannot do this, we strongly recommend leveraging the Sucuri Firewall or equivalent technology to have the vulnerability patched virtually.