'New', '1'=>'OK', '2'=>'Changed', '3'=>'Missing', '4'=>'Added', '98'=>'Not Watched', '99'=>'Checking...' ); function wordpress_sentinel() { $this->parse_uri(); $this->wp_theme_dir = get_theme_root(); $this->wp_plugin_dir = WP_PLUGIN_DIR; $this->wp_wordpress_dir = dirname(WP_CONTENT_DIR); } function site_check() { // Check for issues and display admin message if(!preg_match("/page=wordpress_sentinel/", $_SERVER['REQUEST_URI'])) { $this->build_section_list(); global $wpdb; $changed = $wpdb->get_var("SELECT COUNT(*) FROM ".$wpdb->prefix."wordpresssentinel_section WHERE status = ".WPS_STATE_CHANGED); $new = $wpdb->get_var("SELECT COUNT(*) FROM ".$wpdb->prefix."wordpresssentinel_section WHERE status = ".WPS_STATE_NEW); if($changed || $new) add_action('admin_notices', 'wordpress_sentinel_admin_warning'); } // Periodic (mock cron) checking for changes $action_interval = 5; // minutes $check_interval = 2; // hours $site_check = intval(get_site_option('wordpress_sentinel_site_check')); if($site_check + ($delay * 60) < time()) { $this->build_section_list(); $all_sections = array_merge($this->base, $this->themes, $this->plugins); foreach($all_sections as $section) { if($section->state == WPS_STATE_NEW) continue; $delta_time = time() - strtotime($section->last_checked); $threshold = 60 * 60 * $check_interval; if($delta_time > $threshold) { $this->check_section($section->id, false); break; } } delete_option('wordpress_sentinel_site_check'); add_option('wordpress_sentinel_site_check', time()); } } function admin_panel() { $this->build_section_list(); print '

'; print '

Wordpress Sentinel

'; print "
"; print "'all'))."' class='button'>". "Snapshot Everything"; print "    "; print "'all'))."' class='button'>Check Everything"; print "    "; print "How Does This Work?"; print "
"; $admin_home = true; $action = $this->value($this->args, 'action'); $section_id = $this->value($this->args, 'section'); if($action == 'snapshot') { if($section_id == 'all') $this->build_all_snapshots(); else $this->build_snapshot($section_id); } else if($action == 'details' && $section_id > 0) { $admin_home = $this->view_details($section_id); } else if($action == 'check') { if($section_id == 'all') $this->check_all_sections(); else $this->check_section($section_id); } else if($action == 'help') { $this->show_help(); $admin_home = false; } if ($admin_home) { $this->build_section_list(); $this->display_sections(); } } function display_sections() { $this->display_section("Wordpress", $this->base); $this->display_section("Themes", $this->themes); $this->display_section("Plugins", $this->plugins); } function display_section($title, $sections) { print "

$title

"; print ''; $columns = array('Name', 'Location', 'Files/Snapshot Date', 'Status'); $header = ''; foreach(array_values($columns) as $column) $header .= ""; print "$header"; foreach($sections as $section) { print ""; print ""; print ""; print ""; print ""; print ""; } print "
$column
".$this->display_section_name($section)."".$section->location."".$section->files."
".$section->snapshot_made."
".$this->states[$section->state]."
"; } function display_section_name($section) { $output = "".$section->name."
( "; if($section->state != WPS_STATE_NEW) $output .= "$section->id))."'>Details | "; $snap = ($section->state == WPS_STATE_NEW ? "Create Snapshot" : "Refresh Snapshot"); $output .= "$section->id))."'>$snap | "; $output .= "$section->id))."'>Perform Check"; $output .= " )"; return $output; } function show_help() { ?>
« Back

How does this thing work?

As Wordpress grows in popularity, it also becomes a bigger target for the hacking community. It is hard to think of anything more frustrating than finding that your site is redirecting or displaying content which is not your own.

If you are hacked, there are four questions that you have to address:

  1. How did they get in?
  2. What did they change?
  3. How do I undo the damage that was done?
  4. How do I prevent them from getting in again?

The purpose of this plugin is to alert you when you have been hacked and to address questions 2 & 3. Wordpress Sentinel acts as a watchdog that knows how your install is supposed to look and then alert you when something gets changed.

How do I use it?

First, install the plugin and go to the Wordpress Sentinel option under Settings. It should list content under Wordpress, Themes and Plugins.

Second, click the "Snapshot Everything" button, and every file in your Wordpress install, as well as installed Themes and Plugins will be catalogued.

Periodically, the plugin will check a portion of the items for which snapshots have been taken. If any changes are detected, an administrative message will be displayed in Wordpress Admin. If this happens, go back to the Wordpress Sentinel option under Settings. The offending item will be marked as "Changed" in Red. If you click details, you can see what files have been changed and you can determine if this was a valid change or an intrusion and take the appropriate action.

What if I'm the one making changes?

Obviously, the plugin isn't watching what you are doing, so if you make changes to a Theme or install a new Plugin, or even Upgrade Wordpress to a newer version, it is going to notice that something changed and let you know. When this happens (and it will happen), just go to the Wordpress Sentinel option, find the item that you changed or added, and Refresh the Snapshot. (The "Snapshot Everything" button should never be used except when the plugin is first installed.)

What do I do if I really have been hacked?

The first thing to do is to look at the Wordpress Sentinel page and figure out what items have been changed. Take a screenshot and then look at the details of those items to see what files have been affected. If Wordpress is changed, you need to replace every file that is changed, although usually removing the existing install and replacing it with a clean install is the best course.

If a plugin has been corrupted, it needs to be completely removed and reinstalled. Just updating over the existing install is not advised, as any malicious files that have been added would remain.

If a theme has been corrupted, then things may get complicated. If it is a stock theme that can be removed and reinstalled, then do that. If it is a custom theme, then every modified file needs to be carefully examined and cleaned up. You may need someone with advanced skills in site development to help separate the template content from the injected code.

How do I stop the hacker from getting back in?

That is really beyond the scope of this plugin. The best course of action is to keep Wordpress as well as all plugins and themes up to date. If you know the time the hack occurred (and this plugin helps you determine that) then it is also a good idea to have an Analyst look through your server logs and try to isolate the entry point.



Hope this plugin helps, and if you do need advanced help recovering from a hack or any other Wordpress assistance, feel free to contact me at ed.blogrescue@gmail.com.
base = array(); array_push($this->base, new wordpresssentinel_section('Wordpress Root','wp_root',$this->wp_wordpress_dir, WPS_BASE, WPS_MODE_DIRECTORY)); array_push($this->base, new wordpresssentinel_section('Wordpress Includes','wp_includes', $this->wp_wordpress_dir.DIRECTORY_SEPARATOR.'wp-includes', WPS_BASE)); array_push($this->base, new wordpresssentinel_section('Wordpress Admin','wp_admin', $this->wp_wordpress_dir.DIRECTORY_SEPARATOR.'wp-admin', WPS_BASE)); $this->themes = array(); $themes = get_themes(); foreach($themes as $k=>$v) { $section = new wordpresssentinel_section($v["Name"], $v["Template"], $v["Template Dir"], WPS_THEME); array_push($this->themes, $section); } $this->plugins = array(); $plugins = get_plugins(); foreach($plugins as $k=>$v) { $dir = $this->wp_plugin_dir . (dirname($k)== '.' ? '' : DIRECTORY_SEPARATOR.dirname($k)); $section = new wordpresssentinel_section($v["Name"], basename($k), $dir, WPS_PLUGIN, (dirname($k)== '.' ? WPS_MODE_FILE : WPS_MODE_RECURSIVE)); array_push($this->plugins, $section); } } function view_details($section_id) { global $wpdb; $sql = "SELECT * FROM ".$wpdb->prefix."wordpresssentinel_section WHERE section_id = $section_id"; $section_row = $wpdb->get_row($wpdb->prepare($sql, $this->type, $this->name, $this->location)); if($section_row == null) { print "
"; print "

Cannot display details for section '$section_id' - Section Not Found

"; return; } if(isset($this->args['watch'])) $this->set_file_watch_status($this->args['watch'], 1); if(isset($this->args['nowatch'])) $this->set_file_watch_status($this->args['nowatch'], 0); print "« Back"; print "

Snapshot Details for: ".$section_row->name."

"; print ''; $columns = array('File', 'Size', 'Modified', 'Status'); $header = ''; foreach(array_values($columns) as $column) $header .= ""; print "$header"; $sql = "SELECT * FROM ".$wpdb->prefix."wordpresssentinel_file WHERE section_id = $section_id"; $section_files = $wpdb->get_results($sql); foreach($section_files as $section_file) { $icon = $section_file->watch ? "watch.png" : "nowatch.png"; $action = $section_file->watch ? "nowatch" : "watch"; $watch = "$section_id,$action=>$section_file->file_id))."'>". ""; print ""; print ""; print ""; print ""; print ""; print ""; } print "$header"; print "
$column
$watch  ".$section_file->location."".$section_file->size."".$section_file->update_date."".$this->states[$section_file->status]."
"; } function set_file_watch_status($file_id, $watch_status) { global $wpdb; $wpdb->update($wpdb->prefix.'wordpresssentinel_file', array('watch'=>$watch_status), array('file_id'=>$file_id)); } function build_all_snapshots() { $all_sections = array_merge($this->base, $this->themes, $this->plugins); foreach($all_sections as $section) { $this->build_snapshot($section->id, false); } print "

Snapshots Updated for Everything

"; } function build_snapshot($section_id, $display_message = true) { global $wpdb; $sql = "SELECT * FROM ".$wpdb->prefix."wordpresssentinel_section WHERE section_id = $section_id"; $section_row = $wpdb->get_row($wpdb->prepare($sql, $this->type, $this->name, $this->location)); if($section_row == null) { print "

Cannot generate snapshot for section '$section_id' - Section Not Found

"; return; } $this->reset_section($section_id); $file_list = $this->get_files($section_row); foreach($file_list as $file_key=>$file_object) { $this->add_section_file($section_id, $file_key, $file_object); } $updates = array( 'status'=>WPS_STATE_OK, 'files'=>count($file_list), 'snapshot_made'=>date('Y-m-d H:i:s'), 'last_checked'=>date('Y-m-d H:i:s') ); $this->update_section_record($section_id, $updates); if($display_message) print "

Snapshot Updated for ".$section_row->name. "

"; } function add_section_file($section_id, $file_key, $file_object) { global $wpdb; $mysql_date = date("Y-m-d H:i:s", $file_object->date); $fields = array('section_id'=>$section_id, 'file_ref'=>$file_key, 'location'=>$file_object->file, 'size'=>$file_object->size, 'update_date' => $mysql_date, 'checksum'=>$file_object->checksum, 'status'=>WPS_STATE_OK); $formats = array('%d', '%s', '%s', '%d', '%s', '%s', '%d'); $result = $wpdb->insert($wpdb->prefix.'wordpresssentinel_file', $fields, $formats); } function set_section_status($section_id, $status) { global $wpdb; $wpdb->update($wpdb->prefix.'wordpresssentinel_section', array('status'=>$status), array('section_id'=>$section_id)); } function reset_section_settings($section_id) { global $wpdb; } function reset_section($section_id) { global $wpdb; $wpdb->update($wpdb->prefix.'wordpresssentinel_section', array('status'=>WPS_STATE_NEW,'files'=>0,'snapshot_made'=>0,'last_checked'=>0), array('section_id'=>$section_id)); $wpdb->query("DELETE FROM ".$wpdb->prefix."wordpresssentinel_file WHERE section_id = $section_id"); } function check_all_sections() { $all_sections = array_merge($this->base, $this->themes, $this->plugins); $all_errors = 0; foreach($all_sections as $section) { $all_errors += $this->check_section($section->id, false); } if($all_errors) { print "

Check Performed for Everything (Possible Issues Found)

"; } else { print "

Check Performed for Everything

"; } } function check_section($section_id, $display_message = true) { global $wpdb; $sql = "SELECT * FROM ".$wpdb->prefix."wordpresssentinel_section WHERE section_id = $section_id"; $section_row = $wpdb->get_row($wpdb->prepare($sql, $this->type, $this->name, $this->location)); if($section_row == null) { print "

Cannot perform check for section '$section_id' - Section Not Found

"; return; } $error_count = 0; $this->set_section_status_before_check($section_id); $section_actual_files = $this->get_files($section_row); $section_snapshot_files = $wpdb->get_results("SELECT * FROM ".$wpdb->prefix."wordpresssentinel_file WHERE section_id = $section_id"); foreach($section_snapshot_files as $snapshot_file) { $file_id = $snapshot_file->file_id; $updates = array(); if(!isset($section_actual_files[sha1($snapshot_file->location)])) { $updates['status'] = WPS_STATE_MISSING; $error_count++; } else { $actual_file = $section_actual_files[sha1($snapshot_file->location)]; $difference = ($snapshot_file->size != $actual_file->size) || ($snapshot_file->update_date != date("Y-m-d H:i:s", $actual_file->date)) || ($snapshot_file->checksum != $actual_file->checksum); if($difference) { if($snapshot_file->watch == 0) { $updates['status'] = WPS_STATE_ALLOWED; } else { $updates['status'] = WPS_STATE_CHANGED; $updates['changed_size'] = $actual_file->size; $updates['changed_date'] = date("Y-m-d H:i:s", $actual_file->date); $updates['changed_checksum'] = $actual_file->checksum; $error_count++; } } else { $updates['status'] = WPS_STATE_OK; } unset($section_actual_files[sha1($snapshot_file->location)]); } $this->update_file_record($file_id, $updates); } foreach($section_actual_files as $extra_file) { $data = array('location'=>$extra_file->file, 'changed_size'=>$extra_file->size, 'changed_date'=>date('Y-m-d H:i:s', $extra_file->date), 'changed_checksum'=>$extra_file->checksum, 'status'=>WPS_STATE_ADDED,'section_id'=>$section_id); $format = array('%s', '%d', '%s', '%s', '%d'); $wpdb->insert($wpdb->prefix.'wordpresssentinel_file', $data, $format); $error_count++; } $updates = array('status'=>($error_count ? WPS_STATE_CHANGED : WPS_STATE_OK), 'last_checked'=>date('Y-m-d H:i:s')); $this->update_section_record($section_id, $updates); if($display_message) { if($error_count) { print "

Check Performed for ".$section_row->name. " (Possible Issues Found)

"; } else { print "

Check Performed for ".$section_row->name."

"; } } return $error_count; } function update_section_record($section_id, $updates) { global $wpdb; $wpdb->update($wpdb->prefix.'wordpresssentinel_section', $updates, array('section_id'=>$section_id)); } function update_file_record($file_id, $updates) { global $wpdb; $wpdb->update($wpdb->prefix.'wordpresssentinel_file', $updates, array('file_id'=>$file_id)); } function set_section_status_before_check($section_id) { global $wpdb; $wpdb->update($wpdb->prefix.'wordpresssentinel_section', array('status'=>WPS_STATE_CHECK), array('section_id'=>$section_id)); $wpdb->query("DELETE FROM ".$wpdb->prefix."wordpresssentinel_file WHERE status = ".WPS_STATE_ADDED); $data = array('status'=>WPS_STATE_CHECK, 'changed_size'=>0, 'changed_date'=>'', 'changed_checksum'=>''); $where = array('section_id'=>$section_id); $wpdb->update($wpdb->prefix.'wordpresssentinel_file', $data, $where); } function get_files($section_row) { $files = array(); if($section_row->mode == WPS_MODE_FILE) { $file_location = $section_row->location . DIRECTORY_SEPARATOR . $section_row->slug; $files[sha1($file_location)] = new wordpresssentinel_file($file_location); } else { $this->process_directory($files, $section_row->mode, $section_row->location); } return $files; } function process_directory(&$files, $mode, $path) { $handle = opendir($path); $skip = array('.', '..'); while($entry = readdir($handle)) { if(in_array($entry, $skip)) continue; $resource = $path.DIRECTORY_SEPARATOR.$entry; if(is_dir($resource)) { if($mode == WPS_MODE_RECURSIVE) $this->process_directory($files, $mode, $resource); } else { $files[sha1($resource)] = new wordpresssentinel_file($resource); } } closedir($handle); } function snap_file($section_id, $resource) { } function url($action='', $url_args=array()) { $url_result = $this->uri; $url_result .= "?page=".$this->admin_page; if(!empty($action)) { $url_result .= "&action=$action"; $skip = array('page', 'action'); foreach($url_args as $key=>$val) { if(!in_array($key, $skip)) $url_result .= "&$key=$val"; } } return $url_result; } function value($data, $key, $default='') { return is_array($data) ? (isset($data[$key]) ? $data[$key] : $default) : $default; } function parse_uri() { $this->uri = preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']); parse_str($_SERVER['QUERY_STRING'], $this->args); } } class wordpresssentinel_section { var $name; var $slug; var $location; var $type; var $state = WPS_STATE_NEW; var $mode = WPS_MODE_RECURSIVE; var $id = 0; var $files = 0; var $snapshot_made = "no snapshot"; var $last_checked = "never checked"; function wordpresssentinel_section($name, $slug, $location, $type, $mode=WPS_MODE_RECURSIVE) { global $wpdb; $this->name = $name; $this->slug = $slug; $this->location = $location; $this->type = $type; $this->mode = $mode; $this->set_section_id(); } function set_section_id() { global $wpdb; $sql = "SELECT section_id, status, files, snapshot_made, last_checked FROM ". $wpdb->prefix."wordpresssentinel_section WHERE type = %s AND name = %s AND location = %s"; $result = $wpdb->get_row($wpdb->prepare($sql, $this->type, $this->name, $this->location)); if($result == null) { $fields = array('type'=>$this->type, 'name'=>$this->name, 'slug'=>$this->slug, 'location'=>$this->location, 'mode' => $this->mode, 'status'=>WPS_STATE_NEW, 'snapshot_made'=>'0000-00-00 00:00:00', 'last_checked'=>'0000-00-00 00:00:00'); $formats = array('%d', '%s', '%s', '%s', '%d', '%d', '%s', '%s'); $result = $wpdb->insert($wpdb->prefix.'wordpresssentinel_section', $fields, $formats); $this->id = $wpdb->insert_id; } else { $this->id = $result->section_id; $this->state = $result->status; $this->files = $result->files; $this->snapshot_made = $result->snapshot_made == '0000-00-00 00:00:00' ? 'no snapshot' : $result->snapshot_made; $this->last_checked = $result->last_checked == '0000-00-00 00:00:00' ? 'never checked' : $result->last_checked; } } } class wordpresssentinel_file { var $file; var $size; var $checksum; var $date; function wordpresssentinel_file($location) { $this->file = $location; $this->size = filesize($location); $this->checksum = sha1_file($location); $this->date = filemtime($location); } } function wordpress_sentinel_update_db_check() { global $wordpress_sentinel_db_version; if (get_site_option('wordpress_sentinel_db_version') != $wordpress_sentinel_db_version) wordpress_sentinel_install(); } function wordpress_sentinel_check() { global $wordpress_sentinel; $wordpress_sentinel->site_check(); } function wordpress_sentinel_admin() { global $wordpress_sentinel; $wordpress_sentinel->admin_panel(); } function wordpress_sentinel_add_menu() { add_submenu_page('options-general.php', 'Wordpress Sentinel', 'Wordpress Sentinel', 'manage_options', 'wordpress_sentinel', 'wordpress_sentinel_admin'); } function wordpress_sentinel_admin_warning() { print "

Wordpress Sentinel: "; print "Install files have changed. (Details)

"; } global $wordpress_sentinel; $wordpress_sentinel = new wordpress_sentinel(); add_action('admin_menu', 'wordpress_sentinel_add_menu'); add_action('admin_init', 'wordpress_sentinel_check'); add_action('plugins_loaded', 'wordpress_sentinel_update_db_check');