60

How to Verify WordPress Core Checksums for Site Security

Powered by WPCodeBox

Was your site hacked? Did you give access to your WP install to someone you don’t trust?

This snippet will check your WordPress core for files that shouldn’t exist or that were modified. It gets the checksums for your WordPress version from wp.org, and it matches the files on your system with that.

The code is adapted from WP CLI
‘s core checksum checker.


<?php 

/**
 * Adapted for WPCodeBox from https://github.com/wp-cli/checksum-command 
 * 
 * 
 */
class Checksum_Base_Command {

	/**
	 * Normalizes directory separators to slashes.
	 *
	 * @param string $path Path to convert.
	 *
	 * @return string Path with all backslashes replaced by slashes.
	 */
	public static function normalize_directory_separators( $path ) {
		return str_replace( '\', '/', $path );
	}

	/**
	 * Read a remote file and return its contents.
	 *
	 * @param string $url URL of the remote file to read.
	 *
	 * @return mixed
	 */
	protected static function _read( $url ) {
		$headers  = array( 'Accept' => 'application/json' );
		
		$response = wp_remote_get($url);

		echo "Error: Couldn't fetch response from {$url} (HTTP code {$response->status_code}).";
	}

	/**
	 * Recursively get the list of files for a given path.
	 *
	 * @param string $path Root path to start the recursive traversal in.
	 *
	 * @return array<string>
	 */
	protected function get_files( $path ) {
		$filtered_files = array();
		try {
			$files = new RecursiveIteratorIterator(
				new RecursiveCallbackFilterIterator(
					new RecursiveDirectoryIterator(
						$path,
						RecursiveDirectoryIterator::SKIP_DOTS
					),
					function ( $current, $key, $iterator ) use ( $path ) {
						return $this->filter_file( self::normalize_directory_separators( substr( $current->getPathname(), strlen( $path ) ) ) );
					}
				),
				RecursiveIteratorIterator::CHILD_FIRST
			);
			foreach ( $files as $file_info ) {
				if ( $file_info->isFile() ) {
					$filtered_files[] = self::normalize_directory_separators( substr( $file_info->getPathname(), strlen( $path ) ) );
				}
			}
		} catch ( Exception $e ) {
			echo "Error:".  $e->getMessage();
		}

		return $filtered_files;
	}

	/**
	 * Whether to include the file in the verification or not.
	 *
	 * Can be overridden in subclasses.
	 *
	 * @param string $filepath Path to a file.
	 *
	 * @return bool
	 */
	protected function filter_file( $filepath ) {
		return true;
	}
}

class Checksum_Core_Command extends Checksum_Base_Command {

	/**
	 * Verifies WordPress files against WordPress.org's checksums.
	 *
	 * Downloads md5 checksums for the current version from WordPress.org, and
	 * compares those checksums against the currently installed files.
	 *
	 * For security, avoids loading WordPress when verifying checksums.
	 *
	 * If you experience issues verifying from this command, ensure you are
	 * passing the relevant `--locale` and `--version` arguments according to
	 * the values from the `Dashboard->Updates` menu in the admin area of the
	 * site.
	 *
	 * ## OPTIONS
	 *
	 * [--version=<version>]
	 * : Verify checksums against a specific version of WordPress.
	 *
	 * [--locale=<locale>]
	 * : Verify checksums against a specific locale of WordPress.
	 *
	 * [--insecure]
	 * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack.
	 *
	 * ## EXAMPLES
	 *
	 *     # Verify checksums
	 *     $ wp core verify-checksums
	 *     Success: WordPress installation verifies against checksums.
	 *
	 *     # Verify checksums for given WordPress version
	 *     $ wp core verify-checksums --version=4.0
	 *     Success: WordPress installation verifies against checksums.
	 *
	 *     # Verify checksums for given locale
	 *     $ wp core verify-checksums --locale=en_US
	 *     Success: WordPress installation verifies against checksums.
	 *
	 *     # Verify checksums for given locale
	 *     $ wp core verify-checksums --locale=ja
	 *     Warning: File doesn't verify against checksum: wp-includes/version.php
	 *     Warning: File doesn't verify against checksum: readme.html
	 *     Warning: File doesn't verify against checksum: wp-config-sample.php
	 *     Error: WordPress installation doesn't verify against checksums.
	 *
	 * @when before_wp_load
	 */
	public function execute( $args, $assoc_args ) {
		$wp_version = '';
		$locale     = '';

		if ( ! empty( $assoc_args['version'] ) ) {
			$wp_version = $assoc_args['version'];
		}

		if ( ! empty( $assoc_args['locale'] ) ) {
			$locale = $assoc_args['locale'];
		}

		if ( empty( $wp_version ) ) {
			$details    = self::get_wp_details();
			$wp_version = $details['wp_version'];

			if ( empty( $locale ) ) {
				$locale = $details['wp_local_package'];
			}
		}

		$insecure   = false;

		try {
			$checksums = get_core_checksums( $wp_version, empty( $locale ) ? 'en_US' : $locale );
		} catch ( Exception $exception ) {
			print ( $exception );
		}

		if ( ! is_array( $checksums ) ) {
			print( "<span style='color: red;'>Couldn't get checksums from WordPress.org.</span>" );
		}

		$has_errors = false;
		foreach ( $checksums as $file => $checksum ) {
			// Skip files which get updated
			if ( 'wp-content' === substr( $file, 0, 10 ) ) {
				continue;
			}

			if ( ! file_exists( ABSPATH . $file ) ) {
				print( "<span style='color: orange;'>File doesn't exist: {$file}</span></br>" );
				$has_errors = true;
				continue;
			}

			$md5_file = md5_file( ABSPATH . $file );
			if ( $md5_file !== $checksum ) {
				print( "<span style='color: orange;'>File doesn't verify against checksum: {$file} </span><br/>" );
				$has_errors = true;
			}
		}

		$core_checksums_files = array_filter( array_keys( $checksums ), [ $this, 'filter_file' ] );
		$core_files           = $this->get_files( ABSPATH );
		$additional_files     = array_diff( $core_files, $core_checksums_files );

		if ( ! empty( $additional_files ) ) {
			foreach ( $additional_files as $additional_file ) {
				print( "<span style='color: orange;'>File should not exist: {$additional_file}</span> <br/>" );
			}
		}

		if ( ! $has_errors ) {
			print( '<br/><span style="color: green;">WordPress installation verifies against checksums.</span>' );
		} else {
			print( '<br/><span style="color: orange;">WordPress installation doesn't verify against checksums.</span>' );
		}
	}

	/**
	 * Whether to include the file in the verification or not.
	 *
	 * @param string $filepath Path to a file.
	 *
	 * @return bool
	 */
	protected function filter_file( $filepath ) {
		return ( 0 === strpos( $filepath, 'wp-admin/' )
			|| 0 === strpos( $filepath, 'wp-includes/' )
			|| 1 === preg_match( '/^wp-(?!config.php)([^/]*)$/', $filepath )
		);
	}

	/**
	 * Gets version information from `wp-includes/version.php`.
	 *
	 * @return array {
	 *     @type string $wp_version The WordPress version.
	 *     @type int $wp_db_version The WordPress DB revision.
	 *     @type string $tinymce_version The TinyMCE version.
	 *     @type string $wp_local_package The TinyMCE version.
	 * }
	 */
	private static function get_wp_details() {
		$versions_path = ABSPATH . 'wp-includes/version.php';

		if ( ! is_readable( $versions_path ) ) {
			print(
				"This does not seem to be a WordPress install.n" .
				'Pass --path=`path/to/wordpress` or run `wp core download`.'
			);
		}

		$version_content = file_get_contents( $versions_path, null, null, 6, 2048 );

		$vars   = [ 'wp_version', 'wp_db_version', 'tinymce_version', 'wp_local_package' ];
		$result = [];

		foreach ( $vars as $var_name ) {
			$result[ $var_name ] = self::find_var( $var_name, $version_content );
		}

		return $result;
	}

	/**
	 * Searches for the value assigned to variable `$var_name` in PHP code `$code`.
	 *
	 * This is equivalent to matching the `$VAR_NAME = ([^;]+)` regular expression and returning
	 * the first match either as a `string` or as an `integer` (depending if it's surrounded by
	 * quotes or not).
	 *
	 * @param string $var_name Variable name to search for.
	 * @param string $code PHP code to search in.
	 *
	 * @return int|string|null
	 */
	private static function find_var( $var_name, $code ) {
		$start = strpos( $code, '$' . $var_name . ' = ' );

		if ( ! $start ) {
			return null;
		}

		$start = $start + strlen( $var_name ) + 3;
		$end   = strpos( $code, ';', $start );

		$value = substr( $code, $start, $end - $start );

		return trim( $value, "'" );
	}
}

$checksum_instance = new Checksum_Core_Command();
$checksum_instance->execute([],[]);

Other Snippets

WPCodeBox is a WordPress Code Snippets Manager that allows you to share your WordPress Code Snippets across your sites.