From e420a582d23ddd9b389fdf6b82e1d67183a7163c Mon Sep 17 00:00:00 2001 From: Marco Cesarato Date: Sat, 24 Oct 2020 15:16:24 +0200 Subject: [PATCH] feat: wordpress plugins checksum verifier --- src/Module/Wordpress.php | 169 ++++++++++++++++++++++++++++++++++++--- src/Modules.php | 5 +- src/Scanner.php | 3 +- 3 files changed, 161 insertions(+), 16 deletions(-) diff --git a/src/Module/Wordpress.php b/src/Module/Wordpress.php index 11ca866..8fd5ea8 100644 --- a/src/Module/Wordpress.php +++ b/src/Module/Wordpress.php @@ -2,12 +2,14 @@ namespace marcocesarato\amwscan\Module; +use GlobIterator; use marcocesarato\amwscan\Console; use marcocesarato\amwscan\VerifierInterface; class Wordpress implements VerifierInterface { protected static $checksums = array(); + protected static $pluginsChecksums = array(); protected static $roots = array(); protected static $DS = DIRECTORY_SEPARATOR; @@ -21,12 +23,16 @@ public static function init($path) if (self::isRoot($path)) { $version = self::getVersion($path); if ($version && !empty($version) && !isset(self::$roots[$path])) { - Console::writeLine('Found WordPress ' . $version . ' at "' . $path . '"', 1, 'green'); + $locale = self::getLocale($path); + Console::writeLine('Found WordPress ' . $version . ' (' . $locale . ') at "' . $path . '"', 1, 'green'); + + $plugins = self::getPlugins($path); self::$roots[$path] = array( 'path' => $path, 'version' => $version, + 'locale' => $locale, + 'plugins' => $plugins, ); - self::getChecksums($version); } } } @@ -45,7 +51,6 @@ public static function isRoot($path) is_dir($path . self::$DS . 'wp-admin') && is_dir($path . self::$DS . 'wp-content') && is_dir($path . self::$DS . 'wp-includes') && - is_file($path . self::$DS . 'wp-config.php') && is_file($path . self::$DS . 'wp-includes' . self::$DS . 'version.php') ; } @@ -62,7 +67,7 @@ public static function getVersion($root) $versionFile = $root . self::$DS . 'wp-includes' . self::$DS . 'version.php'; if (is_file($versionFile)) { $versionContent = file_get_contents($versionFile); - preg_match('/\$wp_version[\s]*=[\s]*[\'"]([0-9.]+)[\'"]/', $versionContent, $match); + preg_match('/\$wp_version[\s]*=[\s]*[\'"]([0-9.]+)[\'"]/m', $versionContent, $match); $version = trim($match[1]); if (!empty($version)) { return $version; @@ -72,19 +77,97 @@ public static function getVersion($root) return null; } + /** + * Get locale. + * + * @param $root + * + * @return string + */ + public static function getLocale($root) + { + $versionFile = $root . self::$DS . 'wp-includes' . self::$DS . 'version.php'; + if (is_file($versionFile)) { + $versionContent = file_get_contents($versionFile); + preg_match('/\$wp_local_package[\s]*=[\s]*[\'"]([A-Za-z_-]+)[\'"]/m', $versionContent, $match); + $locale = trim($match[1]); + if (!empty($locale)) { + return $locale; + } + } + + return 'en_US'; + } + + /** + * Get plugins. + * + * @param $root + * + * @return string[] + */ + public static function getPlugins($root) + { + $plugins = array(); + $files = new GlobIterator($root . self::$DS . 'wp-content' . self::$DS . 'plugins' . self::$DS . '*' . self::$DS . '*.php'); + foreach ($files as $cur) { + if ($cur->isFile()) { + $headers = self::getPluginHeaders($cur->getPathname()); + if (!empty($headers['domain']) && !empty($headers['version'])) { + if (empty($headers['domain'])) { + $headers['domain'] = $cur->getBasename('.' . $cur->getExtension()); + } + $headers['path'] = $cur->getPath(); + $plugins[$cur->getPath()] = $headers; + Console::writeLine('Found WordPress Plugin ' . $headers['name'] . ' ' . $headers['version'], 1, 'green'); + } + } + } + + return $plugins; + } + + /** + * Get file headers. + * + * @param $file + * + * @return string[] + */ + public static function getPluginHeaders($file) + { + $headers = array('name' => 'Plugin Name', 'version' => 'Version', 'domain' => 'Text Domain'); + $file_data = file_get_contents($file); + $file_data = str_replace("\r", "\n", $file_data); + foreach ($headers as $field => $regex) { + if (preg_match('/^[ \t\/*#@]*' . preg_quote($regex, '/') . ':(.*)$/mi', $file_data, $match) && $match[1]) { + $headers[$field] = trim(preg_replace('/\s*(?:\*\/|\?>).*/', '', $match[1])); + } else { + $headers[$field] = ''; + } + } + + return $headers; + } + /** * Get checksums. * * @param $version + * @param string $locale + * @param array $plugins * - * @return array + * @return array|false */ - public static function getChecksums($version) + public static function getChecksums($version, $locale = 'en_US') { - if (empty(self::$checksums[$version])) { - $checksums = file_get_contents('https://api.wordpress.org/core/checksums/1.0/?version=' . $version); - $checksums = json_decode($checksums, true); - $versionChecksums = $checksums['checksums'][$version]; + if (!isset(self::$checksums[$version])) { + Console::writeLine('Retrieving checksums of Wordpress ' . $version, 1, 'grey'); + $checksums = self::getData('https://api.wordpress.org/core/checksums/1.0/?version=' . $version . '&locale=' . $locale); + if (!$checksums) { + return false; + } + $versionChecksums = $checksums['checksums']; self::$checksums[$version] = array(); // Sanitize paths and checksum foreach ($versionChecksums as $filePath => $checksum) { @@ -96,6 +179,39 @@ public static function getChecksums($version) return self::$checksums[$version]; } + /** + * Get checksums. + * + * @param $version + * @param string $locale + * @param array $plugins + * + * @return array|false + */ + public static function getPluginsChecksums($plugins = array()) + { + foreach ($plugins as $plugin) { + if (!isset(self::$pluginsChecksums[$plugin['domain']][$plugin['version']])) { + Console::writeLine('Retrieving checksums of Wordpress Plugin ' . $plugin['name'] . ' ' . $plugin['version'], 1, 'grey'); + $checksums = self::getData('https://downloads.wordpress.org/plugin-checksums/' . $plugin['domain'] . '/' . $plugin['version'] . '.json'); + if (!$checksums) { + self::$pluginsChecksums[$plugin['domain']][$plugin['version']] = array(); + continue; + } + $pluginChecksums = $checksums['files']; + foreach ($pluginChecksums as $filePath => $checksum) { + $path = $plugin['path'] . self::$DS . $filePath; + $root = self::getRoot($path); + $sanitizePath = str_replace($root, '', $path); + $sanitizePath = self::sanitizePath($sanitizePath); + self::$pluginsChecksums[$plugin['domain']][$plugin['version']][$sanitizePath] = strtolower($checksum['md5']); + } + } + } + + return self::$pluginsChecksums; + } + /** * Is verified file. * @@ -113,16 +229,28 @@ public static function isVerified($path) if (!empty($root)) { $comparePath = str_replace($root['path'], '', $path); $comparePath = self::sanitizePath($comparePath); - $checksums = self::getChecksums($root['version']); + $checksums = self::getChecksums($root['version'], $root['locale']); + $pluginsChecksums = self::getPluginsChecksums($root['plugins']); if (!$checksums) { return false; } + // Core if (!empty($checksums[$comparePath])) { $checksum = md5_file($path); $checksum = strtolower($checksum); return $checksums[$comparePath] === $checksum; } + // Plugins + foreach ($root['plugins'] as $plugin) { + $checksums = $pluginsChecksums[$plugin['domain']][$plugin['version']]; + if (!empty($pluginsChecksums[$plugin['domain']][$plugin['version']]) && !empty($checksums[$comparePath])) { + $checksum = md5_file($path); + $checksum = strtolower($checksum); + + return $checksums[$comparePath] === $checksum; + } + } } return false; @@ -162,4 +290,23 @@ public static function sanitizePath($path) return $sanitized; } + + /** + * HTTP request get data. + * + * @param $url + * + * @return mixed|null + */ + protected static function getData($url) + { + $headers = get_headers($url); + if (substr($headers[0], 9, 3) != '200') { + return null; + } + + $content = @file_get_contents($url); + + return @json_decode($content, true); + } } diff --git a/src/Modules.php b/src/Modules.php index 8e47e8b..3323f74 100644 --- a/src/Modules.php +++ b/src/Modules.php @@ -25,9 +25,6 @@ public static function init($path) */ public static function isVerified($path) { - $result = false; - $result = (Wordpress::isVerified($path) || $result); - - return $result; + return Wordpress::isVerified($path); } } diff --git a/src/Scanner.php b/src/Scanner.php index e8ebbee..fac921f 100644 --- a/src/Scanner.php +++ b/src/Scanner.php @@ -273,7 +273,8 @@ public function run($args = null) Console::writeLine('Scanning ' . self::$pathScan, 2); // Mapping files - Console::writeLine('Mapping and verifying files. It may take a while, please wait...'); + Console::writeLine('Mapping, retrieving checksums and verifying files'); + Console::displayLine('It may take a while, please wait...'); $iterator = $this->mapping(); // Counting files