Wednesday, September 9, 2009

Hack Cleanup

A while back one of my shared servers got hacked. The hacker added php error pages via .htaccess files. The code in the PHP was encoded so that it was difficult to read exactly what was happening, but basically they were trying to advertise on my dime.

This site happens to run on Joomla!, Flyspray and Magento. Each of them have many subfolders and I have no shell access. Recursively searching through the filesystem couldn't simply be performed by good old CLI tools like grep, find, sed, etc...

I found this php script for recursively going through the filesystem and modified it to look through all the .htaccess files and suspicious files matching the pattern of the hack (numbered php files like 23546.php) as well as an empty WP file. I also added the ability to set the depth of recursion and directories to exclude.

I also added some sorting functions, specifically one by modification time (recent first) so that at a glance, I can determine any files that have been compromised.

Options:

Mostly I expect you'll want to edit the code for your modifications, however I have exposed 2 options to the query string:
depth=n

Set the recursion depth to n, where n is an integer (-1 = bottomless, 0 = none [default], 1 = immediate subfolders only, etc...)

modify=1

The script runs in read only mode unless you add modify=1. If you only want a report of what it expects it will delete, leave modify=1 off the querystring.

Example: /hack-cleanup.php?depth=-1&modify=1

Inspect all subfolders until there are no more subfolders and delete anything that matches the defined pattern. Look at the code, specifically the unlink() calls.
Here's the code, hope it helps:


header('Content-Type: text/plain');

$exclude_dirs = array();
$root_dir = "";
$ro = TRUE;

function process_dir($dir, $depth = 0) {
if (! is_dir($dir)) return FALSE;
global $exclude_dirs, $root_dir, $ro;
if ($dir[strlen($dir)-1] != '/') $dir .= '/';

if (!$root_dir) {
$root_dir = $dir;
if ($ro)
echo "##########\n#" .
"\n# Operating in READ ONLY mode. Nothing will be deleted." .
"\n# Add modify=1 to query string to modify." .
"\n#\n##########\n";
else
echo "##########\n#" .
"\n# Modifications will be made!" .
"\n# Files MAY BE DELETED!" .
"\n#\n##########\n";
echo "\nRoot directory: $dir, depth: $depth";
echo "\n-- Excluded directories: ";
print_r($exclude_dirs);
}

$rel_dir = substr($dir, strlen($root_dir));

$dirs = explode('/', $rel_dir);

if (!strlen($rel_dir)) $current_depth = 0;
else if ( strpos($rel_dir,'/') < 0 ) $current_depth = 1;
else $current_depth = count($dirs);

$current_dir = end($dirs);

$list = array();

for ($handle = opendir($dir); (FALSE !== ($file = readdir($handle)));) {
$path = $dir . $file;
if (($file != '.' && $file != '..') && (file_exists($path))) {
if (is_dir($path) &&
($depth < 0 || $depth && $current_depth <= $depth)) {
if (in_array($path, $exclude_dirs) || in_array($file, $exclude_dirs)) {
echo "\n-- Skipping Excluded Directory: $path";
} else {
// echo "\n++ Recursing into: $path ++";
$list = array_merge($list, process_dir($path, $depth));
}
} else {
$entry = array('filename' => $file, 'dirpath' => $dir, 'path' => $path);

//---------------------------------------------------------//
// - SECTION 1 - //
// Actions to be performed on ALL ITEMS //
//----------------- Begin Editable ------------------//

$entry['modtime'] = filemtime($path);
$top_dir = $dirs[1];

//----------------- End Editable ------------------//
do if (!is_dir($path)) {
//---------------------------------------------------------//
// - SECTION 2 - //
// Actions to be performed on FILES ONLY //
//----------------- Begin Editable ------------------//
$entry['size'] = filesize($path);
if (strstr(pathinfo($path,PATHINFO_BASENAME),'log')) {
if (!$entry['handle'] = fopen($path,r)) $entry['handle'] = "FAIL";
}
if ($file == 'WP') {
echo "\n%% Deleting WP: " . $path;
if (!$ro) unlink($path);
}
if ($file == '.htaccess') {
$data = fread(fopen($path,r),filesize($path));
fclose($path);
if (preg_match('(/(\w+/)*\d+\.php)',$data,$matches)) {
echo "\n%% Deleting bad .htaccess in: " . $dir;
echo "\n%% Deleting: $dir" . $matches[0];
if (!$ro) unlink($path);
if (!$ro) unlink("." . $matches[0]);
} else {
echo "\n:: Inspected clean .htaccess: $path";
}
}
if (preg_match('/^\d{4,}(\.php)$/',$file)) {
echo "\n%% Deleting suspicious file: $file, $path";
if (!$ro) unlink($path);
}

//----------------- End Editable ------------------//
break;
} else {
//---------------------------------------------------------//
// - SECTION 3 - //
// Actions to be performed on DIRECTORIES ONLY //
//----------------- Begin Editable ------------------//

//----------------- End Editable ------------------//
break;
} while (FALSE);
$list[] = $entry;
}
}
}
closedir($handle);
return $list;
}

function modtcpare($a, $b) {
$at = $a['modtime'];
$bt = $b['modtime'];
return $bt - $at;
}

function pathcpare($a, $b) {
$at = $a['path'];
$bt = $b['path'];
return strcmp($at,$bt);
}

// done with function definitions, now run it...

// exclude all directories named 'cache' like this:
// $exclude_dirs[] = 'cache'
$exclude_dirs[] = './cache';

$desired_depth = $_GET['depth'];
if (!$desired_depth) $desired_depth = 0;
$modify = $_GET['modify'];
if (isset($modify)) $ro = ! $modify;

$result = process_dir('.', $desired_depth);

echo "\n\n-----\nEntries ordered by Path: " . count($result);
usort($result, 'pathcpare');
$cnt = 1;
foreach ($result as $file) {
echo "\n" . str_pad($cnt++, 6, " ", STR_PAD_LEFT) . ": " . $file['path'];
}


echo "\n\n-----\nEntries ordered by Modification Time: " . count($result);
usort($result, 'modtcpare');

$cnt = 1;
foreach ($result as $file) {
$mtime = date('Y-m-d H:i:s', $file['modtime']);
echo "\n" . str_pad($cnt++, 6, " ", STR_PAD_LEFT) . ": (" . $mtime .") " . $file['path'];
}

?>

Thursday, January 15, 2009

Installing Python 2.4 and libxml2 on Mac OS X 10.5 (Leopard) for Plone

I've had quite the struggle with getting libxml2 and related libraries installed on my Mac OS X (10.5 - Leopard) . Thanks to Brent Lambert of enPraxis recommending MacPorts, I have finally managed to stumble my way through the hoops and hurdles to have a successful installation with all the pieces working for developing with Plone on my Mac.

I come from a background of working on and programming websites and think Plone is a fascinating, sophisticated, top-notch solution for content management. I also think it's a great platform for developing web applications, but find the initial learning curve to be almost prohibitively steep. I see the promised land on the other side of the ridge, but am still struggling to get there. Maybe you can relate.

I'm proficient on Unix and with simple shell usage, commands, etc... but I've never fully followed the gory details of compiling libraries and the like. Generally I issue the ./configure, make, sudo make install commands, cross my fingers and hope for the best. I follow-up on any error messages I can decode, but there's still a seemingly black-hole of mystery surrounding the compliation process. At this point I've actually lost track of the number of Python installations on my system (OS X 10.5 seems to come with Python 2.3 and 2.5, the Plone distribution for Mac comes with Python 2.4, I compiled one, found another download, etc...) and I had a similar experience with downloading, compiling and installing libxml2.

Since Zope requires Python 2.4 and Plone is built on Zope, there's a version induced nightmare (well, maybe that's stating it a little strongly, but you get the gist, yes?). The version needs were particularly problematic since Leopard comes with an older version of libxml2 and a newer version of Python, etc...

This is still a bit of a mystery to me, but rather than detail all the things that didn't work, here's what worked for me:
  1. Install Apple's Xcode (required by MacPorts).
  2. Install MacPorts.
  3. Run these commands, roughly in the below order:
sudo port install python24
sudo port install python_select
sudo python_select python24
sudo port install libxml2
sudo port install libxslt
sudo port install py-libxml2
Other commands I ran which may be necessary for Plone:
sudo port install py-xml
sudo port install py-elementtree
sudo port install py-setuptools
sudo port install py-lxml
sudo port install py-pil
I know it's a bit of a shotgun approach and since I had already made a mess of my system I had to issue some relinking to make Python 2.4 the default (ln -sf /opt/local/bin/python2.4 /usr/local/bin/python, for example). A lot of this is voodoo to me. If the commands work, great, if not, I follow up with the erorr messages in google and attempt to piece it all together. I'm not uber-concerned with what side-effects this may have on my system. I just know it now works for me.

I hope this helped!