WordPress < 4.1.2 Stored XSS vulnerability
WordPress 4.1.2 is available as of April 21, 2015. WordPress versions 4.1.1 and earlier are affected by a critical cross-site scripting vulnerability, which could enable anonymous users to compromise a site (WP blog).
tldr; mysql → special characters → truncation → input validation → output sanitisation → xss → time to update WordPress.
Introduction: MySQL strict mode
this last year, I was reading a blog post about how MySQL’s utf8 charsets only support 3-byte characters. I recommend reading the article if you like PILE OF POOs (by which I mean the character of course). In short: a character that is encoded in UTF-8 can technically be between one and four bytes in length as per RFC3629. You would need a utf8mb4 charset if you’d actually want to store 4-byte characters.
When you insert a string containing a 4-byte character into a utf8 column, the default MySQL behaviour is to truncate the rest of the string after (and including) the occurence of the 4-byte character. This is prevented by using MySQL’s strict mode which ensures that the data is valid before it gets stored.
Strict mode controls how MySQL handles invalid or missing values in data-change statements such as INSERT or UPDATE. A value can be invalid for several reasons. For example, it might have the wrong data type for the column, or it might be out of range. If strict mode is not in effect, MySQL inserts adjusted values for invalid or missing values and produces warnings.
The adjusted value in this case is the truncated string. I should mention that MySQL displays this truncation behaviour not only for 4-byte characters, but also invalid byte sequences for all character sets, which means that utf8mb4 is just as vulnerable, as well as more exotic charsets. The only exception is latin1, which accepts just about anything. To prevent these truncations issues, you would have to enable MySQL strict mode manually when establishing a connection to the MySQL server, as it is not enabled by default.
- Example (before insert): ex𝌆tic
- Example (after insert): ex
I was wondering how this knowledge could be used as a possible attack vector. The behaviour of MySQL when it is not in strict mode reminded me a lot of null-byte vulnerabilities that popped up in the past. Things go wrong when data truncation is in play.
How this affects WordPress
I started picking web applications which would be my victims to try this behaviour on. Some projects enabled MySQL strict mode when establishing the database connection. WordPress did not have strict mode enabled. Perfect.
I did not have to look far. WordPress’s core comment functionality appeared to truncate my crafted strings! By default, placing comments is handled in the WP core, although plug-ins such as Disqus can replace this functionality.
Additionally, you are allowed use a limited set of HTML tags in your comments: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
- A nice user could comment: <abbr title='Web log'>blog!</abbr>
- A not so nice user could comment: <abbr title='Web𝌆log'>blog!</abbr>
Both comments pass through the input validator of WordPress, but only the first one gets stored correctly. The second one is stored as <abbr title='Web. This comment effectively breaks the well-formedness of the HTML page, since the closing quote and tag token are not there.
Now imagine what would happen if you made another comment under a crafted username such as: cedric' onmouseover='alert(1)' style='position:fixed;top:0;left:0;width:100%;height:100%'
After successful submission of both comments, the HTML output would look as follows:
<div class="comment" id="comment-1">
<div class="comment" id="comment-2">
cedric' onmouseover='alert(1)' style='position:fixed;top:0;left:0;width:100%;height:100%'
You get the following HTML as a result:
<abbr title='Web......cedric' onmouseover='alert(1)' style='position:fixed;top:0;left:0;width:100%;height:100%'>
This piece of code will effectively result in script execution as soon as the victim hovers the element. This technique works only when there are no other single quotes between the comment text and the author name, and is thus dependent on the current WordPress theme.
Now, if the above was too difficult, there appears to be another way. As long as your comment contains a newline, some sort of output sanitisation will happen which will replace the quote character with “.
This means that submitting the following payload would be enough:
<blockquote cite='x onmouseover=alert(1) 𝌆'>
We now have script execution that is no longer dependent on the current WordPress theme since quotes get escaped.
<blockquote cite=“x onmouseover=alert(1) ...>
Payloads can be crafted in a way that they are invisible to the victim. When the victim now reaches the comments page, script execution will occur. As a proof of concept, I created a payload that adds a new user to the adminstration panel, meaning you need an administrator to visit the injected comment page. The same could be done to download and install a plug-in that executes malicious server-side code. I will not share the PoC right now as it got lost (see below…).
Some limiting factors
The vulnerability only affects certain character sets. If you’re using the latin1 charset for columns in your database, you are likely to be safe. However, utf8 was the default in WordPress, which has now changed to utf8mb4 and has several advantages.
It’s worth noting that in the default setup, comments need approval from the admin. Although (also by default), when your first comment gets approved, you won’t need approval for your next comment 😉 This leaves ample opportunity to first convince the admin that you are prim and proper, and then go scriptkiddy on him.
Be aware that data truncation not only affects the comment functionality but really all of the WordPress core and plug-ins. You should upgrade your WordPress version either way. Many other yet undiscovered attack vectors may exist because of this. For instance, while writing this blog post, I accidentally truncated half of the post by inserting an invalid byte sequence *sigh*.
Fixing this issue
Fixing this issue was a long process. As it affected the WordPress core at the database layer, the fix had to be tested thoroughly to make sure that websites in all kinds of set-ups would still work as expected, regardless of the charset they are using. WordPress is written to work on any system that has PHP and a webserver, regardless of installed features. As a consequence, installing WordPress is a breeze, but development requires the introduction of complex cases to ensure (backwards-) compatibility. Tests were done to see if it was possible to enable strict mode in WordPress, but this was reverted later on because it broke compatibility with too many plug-ins.
Bits and pieces of the patched code were being pushed to the VCS trunk as soon as October 2014. Meaning that it is unlikely that you can conduct a successful XSS on version 4.1.1, the previous WordPress version.
I believe a lot of websites were vulnerable for this stored XSS vulnerability. The Forbes website mentions that there were 60 million WordPress websites in 2012, and that number is increasing each day. If only one out of 60 is vulnerable, then still more than a million websites are open for exploitation. Thankfully, WordPress has an auto-update mechanism in place, so you are probably already patched up by now if you’re a blog owner.
I’d like to thank Andrew Nacin, Gary Pendergast and Mike Adams from the WordPress team, and everyone else who worked on patching this issue. I hope to see some more details on the patching process from their side on the WordPress blog or at a conference in the future.
|23 Feb 2014||Responsible disclosure to WP team|
|31 Mar 2014||Issue acknowleged|
|7 May 2014||Initial patch received from WP team|
|6 Sep 2014||More details sent to WP team|
|20 Oct 2014||Things are happening in the background|
|20 Feb 2015||More things are happening in the background|
|21 April 2015||WordPress 4.1.2 security release|
|23 April 2015||Blog post made public|