'GET', 'callback' => array( __CLASS__, 'report' ), 'permission_callback' => array( 'Gralus_Connection', 'verify_request' ) ) ); } public static function report() { if ( function_exists( 'set_time_limit' ) ) { set_time_limit( 60 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- the scan reads many files; some hosts stop PHP at 30 s. } global $wp_version; return array( 'wpVersion' => (string) $wp_version, 'core' => self::core_files(), 'admins' => self::admins(), 'muPlugins' => self::mu_plugins(), 'uploadsPhp' => self::uploads_php(), 'checkedAt' => gmdate( 'c' ), ); } /** Administrators with when they were added. */ private static function admins() { $out = array(); foreach ( get_users( array( 'role' => 'administrator', 'number' => 200 ) ) as $u ) { $out[] = array( 'login' => (string) $u->user_login, 'email' => (string) $u->user_email, 'registered' => (string) $u->user_registered, ); } return $out; } /** Files in wp-content/mu-plugins (must-use plugins load on every page and are hidden from the plugin list). */ private static function mu_plugins() { $out = array(); if ( ! defined( 'WPMU_PLUGIN_DIR' ) || ! is_dir( WPMU_PLUGIN_DIR ) ) { return $out; } foreach ( (array) scandir( WPMU_PLUGIN_DIR ) as $f ) { if ( '.' === $f || '..' === $f || 'index.php' === $f ) { continue; } $out[] = (string) $f; if ( count( $out ) >= self::LIST_MAX ) { break; } } sort( $out ); return $out; } /** Core files that differ from the official checksums, and extra files inside wp-admin and wp-includes. */ private static function core_files() { global $wp_version, $wp_local_package; $result = array( 'checked' => 0, 'modified' => array(), 'unknown' => array(), 'error' => null ); if ( ! function_exists( 'get_core_checksums' ) ) { require_once ABSPATH . 'wp-admin/includes/update.php'; } $locale = isset( $wp_local_package ) ? $wp_local_package : 'en_US'; $checksums = get_core_checksums( $wp_version, $locale ); if ( ! is_array( $checksums ) && 'en_US' !== $locale ) { $checksums = get_core_checksums( $wp_version, 'en_US' ); } if ( ! is_array( $checksums ) || ! $checksums ) { $result['error'] = 'Could not get the official checksums from wordpress.org.'; return $result; } foreach ( $checksums as $file => $md5 ) { // wp-content holds themes and plugins that sites change on purpose. if ( 0 === strpos( $file, 'wp-content/' ) ) { continue; } $path = ABSPATH . $file; if ( ! file_exists( $path ) ) { continue; // Missing files are not a sign of a hack (hosts strip readme files, etc.). } ++$result['checked']; if ( md5_file( $path ) !== $md5 && count( $result['modified'] ) < self::LIST_MAX ) { $result['modified'][] = $file; } } // Extra PHP files inside wp-admin and wp-includes are a classic backdoor location. foreach ( array( 'wp-admin', 'wp-includes' ) as $dir ) { $base = ABSPATH . $dir; if ( ! is_dir( $base ) ) { continue; } $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base, FilesystemIterator::SKIP_DOTS ) ); foreach ( $it as $f ) { if ( ! $f->isFile() || ! preg_match( '/\.(php|phtml|phar|php\d)$/i', $f->getFilename() ) ) { continue; } $rel = str_replace( '\\', '/', substr( $f->getPathname(), strlen( ABSPATH ) ) ); if ( ! isset( $checksums[ $rel ] ) ) { $result['unknown'][] = $rel; if ( count( $result['unknown'] ) >= self::LIST_MAX ) { break 2; } } } } return $result; } /** PHP files anywhere under wp-content/uploads (should only hold media). */ private static function uploads_php() { $out = array(); $uploads = wp_get_upload_dir(); $base = isset( $uploads['basedir'] ) ? $uploads['basedir'] : ''; if ( ! $base || ! is_dir( $base ) ) { return $out; } $seen = 0; $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base, FilesystemIterator::SKIP_DOTS ) ); foreach ( $it as $f ) { if ( ++$seen > self::UPLOADS_SCAN_MAX ) { break; } if ( $f->isFile() && preg_match( '/\.(php|phtml|phar|php\d|pht)$/i', $f->getFilename() ) && 'index.php' !== $f->getFilename() ) { $out[] = str_replace( '\\', '/', substr( $f->getPathname(), strlen( $base ) + 1 ) ); if ( count( $out ) >= self::LIST_MAX ) { break; } } } return $out; } }