Skip to content

Commit e420a58

Browse files
committed
feat: wordpress plugins checksum verifier
1 parent fd11825 commit e420a58

File tree

3 files changed

+161
-16
lines changed

3 files changed

+161
-16
lines changed

src/Module/Wordpress.php

+158-11
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace marcocesarato\amwscan\Module;
44

5+
use GlobIterator;
56
use marcocesarato\amwscan\Console;
67
use marcocesarato\amwscan\VerifierInterface;
78

89
class Wordpress implements VerifierInterface
910
{
1011
protected static $checksums = array();
12+
protected static $pluginsChecksums = array();
1113
protected static $roots = array();
1214
protected static $DS = DIRECTORY_SEPARATOR;
1315

@@ -21,12 +23,16 @@ public static function init($path)
2123
if (self::isRoot($path)) {
2224
$version = self::getVersion($path);
2325
if ($version && !empty($version) && !isset(self::$roots[$path])) {
24-
Console::writeLine('Found WordPress ' . $version . ' at "' . $path . '"', 1, 'green');
26+
$locale = self::getLocale($path);
27+
Console::writeLine('Found WordPress ' . $version . ' (' . $locale . ') at "' . $path . '"', 1, 'green');
28+
29+
$plugins = self::getPlugins($path);
2530
self::$roots[$path] = array(
2631
'path' => $path,
2732
'version' => $version,
33+
'locale' => $locale,
34+
'plugins' => $plugins,
2835
);
29-
self::getChecksums($version);
3036
}
3137
}
3238
}
@@ -45,7 +51,6 @@ public static function isRoot($path)
4551
is_dir($path . self::$DS . 'wp-admin') &&
4652
is_dir($path . self::$DS . 'wp-content') &&
4753
is_dir($path . self::$DS . 'wp-includes') &&
48-
is_file($path . self::$DS . 'wp-config.php') &&
4954
is_file($path . self::$DS . 'wp-includes' . self::$DS . 'version.php')
5055
;
5156
}
@@ -62,7 +67,7 @@ public static function getVersion($root)
6267
$versionFile = $root . self::$DS . 'wp-includes' . self::$DS . 'version.php';
6368
if (is_file($versionFile)) {
6469
$versionContent = file_get_contents($versionFile);
65-
preg_match('/\$wp_version[\s]*=[\s]*[\'"]([0-9.]+)[\'"]/', $versionContent, $match);
70+
preg_match('/\$wp_version[\s]*=[\s]*[\'"]([0-9.]+)[\'"]/m', $versionContent, $match);
6671
$version = trim($match[1]);
6772
if (!empty($version)) {
6873
return $version;
@@ -72,19 +77,97 @@ public static function getVersion($root)
7277
return null;
7378
}
7479

80+
/**
81+
* Get locale.
82+
*
83+
* @param $root
84+
*
85+
* @return string
86+
*/
87+
public static function getLocale($root)
88+
{
89+
$versionFile = $root . self::$DS . 'wp-includes' . self::$DS . 'version.php';
90+
if (is_file($versionFile)) {
91+
$versionContent = file_get_contents($versionFile);
92+
preg_match('/\$wp_local_package[\s]*=[\s]*[\'"]([A-Za-z_-]+)[\'"]/m', $versionContent, $match);
93+
$locale = trim($match[1]);
94+
if (!empty($locale)) {
95+
return $locale;
96+
}
97+
}
98+
99+
return 'en_US';
100+
}
101+
102+
/**
103+
* Get plugins.
104+
*
105+
* @param $root
106+
*
107+
* @return string[]
108+
*/
109+
public static function getPlugins($root)
110+
{
111+
$plugins = array();
112+
$files = new GlobIterator($root . self::$DS . 'wp-content' . self::$DS . 'plugins' . self::$DS . '*' . self::$DS . '*.php');
113+
foreach ($files as $cur) {
114+
if ($cur->isFile()) {
115+
$headers = self::getPluginHeaders($cur->getPathname());
116+
if (!empty($headers['domain']) && !empty($headers['version'])) {
117+
if (empty($headers['domain'])) {
118+
$headers['domain'] = $cur->getBasename('.' . $cur->getExtension());
119+
}
120+
$headers['path'] = $cur->getPath();
121+
$plugins[$cur->getPath()] = $headers;
122+
Console::writeLine('Found WordPress Plugin ' . $headers['name'] . ' ' . $headers['version'], 1, 'green');
123+
}
124+
}
125+
}
126+
127+
return $plugins;
128+
}
129+
130+
/**
131+
* Get file headers.
132+
*
133+
* @param $file
134+
*
135+
* @return string[]
136+
*/
137+
public static function getPluginHeaders($file)
138+
{
139+
$headers = array('name' => 'Plugin Name', 'version' => 'Version', 'domain' => 'Text Domain');
140+
$file_data = file_get_contents($file);
141+
$file_data = str_replace("\r", "\n", $file_data);
142+
foreach ($headers as $field => $regex) {
143+
if (preg_match('/^[ \t\/*#@]*' . preg_quote($regex, '/') . ':(.*)$/mi', $file_data, $match) && $match[1]) {
144+
$headers[$field] = trim(preg_replace('/\s*(?:\*\/|\?>).*/', '', $match[1]));
145+
} else {
146+
$headers[$field] = '';
147+
}
148+
}
149+
150+
return $headers;
151+
}
152+
75153
/**
76154
* Get checksums.
77155
*
78156
* @param $version
157+
* @param string $locale
158+
* @param array $plugins
79159
*
80-
* @return array
160+
* @return array|false
81161
*/
82-
public static function getChecksums($version)
162+
public static function getChecksums($version, $locale = 'en_US')
83163
{
84-
if (empty(self::$checksums[$version])) {
85-
$checksums = file_get_contents('https://api.wordpress.org/core/checksums/1.0/?version=' . $version);
86-
$checksums = json_decode($checksums, true);
87-
$versionChecksums = $checksums['checksums'][$version];
164+
if (!isset(self::$checksums[$version])) {
165+
Console::writeLine('Retrieving checksums of Wordpress ' . $version, 1, 'grey');
166+
$checksums = self::getData('https://api.wordpress.org/core/checksums/1.0/?version=' . $version . '&locale=' . $locale);
167+
if (!$checksums) {
168+
return false;
169+
}
170+
$versionChecksums = $checksums['checksums'];
88171
self::$checksums[$version] = array();
89172
// Sanitize paths and checksum
90173
foreach ($versionChecksums as $filePath => $checksum) {
@@ -96,6 +179,39 @@ public static function getChecksums($version)
96179
return self::$checksums[$version];
97180
}
98181

182+
/**
183+
* Get checksums.
184+
*
185+
* @param $version
186+
* @param string $locale
187+
* @param array $plugins
188+
*
189+
* @return array|false
190+
*/
191+
public static function getPluginsChecksums($plugins = array())
192+
{
193+
foreach ($plugins as $plugin) {
194+
if (!isset(self::$pluginsChecksums[$plugin['domain']][$plugin['version']])) {
195+
Console::writeLine('Retrieving checksums of Wordpress Plugin ' . $plugin['name'] . ' ' . $plugin['version'], 1, 'grey');
196+
$checksums = self::getData('https://downloads.wordpress.org/plugin-checksums/' . $plugin['domain'] . '/' . $plugin['version'] . '.json');
197+
if (!$checksums) {
198+
self::$pluginsChecksums[$plugin['domain']][$plugin['version']] = array();
199+
continue;
200+
}
201+
$pluginChecksums = $checksums['files'];
202+
foreach ($pluginChecksums as $filePath => $checksum) {
203+
$path = $plugin['path'] . self::$DS . $filePath;
204+
$root = self::getRoot($path);
205+
$sanitizePath = str_replace($root, '', $path);
206+
$sanitizePath = self::sanitizePath($sanitizePath);
207+
self::$pluginsChecksums[$plugin['domain']][$plugin['version']][$sanitizePath] = strtolower($checksum['md5']);
208+
}
209+
}
210+
}
211+
212+
return self::$pluginsChecksums;
213+
}
214+
99215
/**
100216
* Is verified file.
101217
*
@@ -113,16 +229,28 @@ public static function isVerified($path)
113229
if (!empty($root)) {
114230
$comparePath = str_replace($root['path'], '', $path);
115231
$comparePath = self::sanitizePath($comparePath);
116-
$checksums = self::getChecksums($root['version']);
232+
$checksums = self::getChecksums($root['version'], $root['locale']);
233+
$pluginsChecksums = self::getPluginsChecksums($root['plugins']);
117234
if (!$checksums) {
118235
return false;
119236
}
237+
// Core
120238
if (!empty($checksums[$comparePath])) {
121239
$checksum = md5_file($path);
122240
$checksum = strtolower($checksum);
123241

124242
return $checksums[$comparePath] === $checksum;
125243
}
244+
// Plugins
245+
foreach ($root['plugins'] as $plugin) {
246+
$checksums = $pluginsChecksums[$plugin['domain']][$plugin['version']];
247+
if (!empty($pluginsChecksums[$plugin['domain']][$plugin['version']]) && !empty($checksums[$comparePath])) {
248+
$checksum = md5_file($path);
249+
$checksum = strtolower($checksum);
250+
251+
return $checksums[$comparePath] === $checksum;
252+
}
253+
}
126254
}
127255

128256
return false;
@@ -162,4 +290,23 @@ public static function sanitizePath($path)
162290

163291
return $sanitized;
164292
}
293+
294+
/**
295+
* HTTP request get data.
296+
*
297+
* @param $url
298+
*
299+
* @return mixed|null
300+
*/
301+
protected static function getData($url)
302+
{
303+
$headers = get_headers($url);
304+
if (substr($headers[0], 9, 3) != '200') {
305+
return null;
306+
}
307+
308+
$content = @file_get_contents($url);
309+
310+
return @json_decode($content, true);
311+
}
165312
}

src/Modules.php

+1-4
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ public static function init($path)
2525
*/
2626
public static function isVerified($path)
2727
{
28-
$result = false;
29-
$result = (Wordpress::isVerified($path) || $result);
30-
31-
return $result;
28+
return Wordpress::isVerified($path);
3229
}
3330
}

src/Scanner.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,8 @@ public function run($args = null)
273273
Console::writeLine('Scanning ' . self::$pathScan, 2);
274274

275275
// Mapping files
276-
Console::writeLine('Mapping and verifying files. It may take a while, please wait...');
276+
Console::writeLine('Mapping, retrieving checksums and verifying files');
277+
Console::displayLine('It may take a while, please wait...');
277278
$iterator = $this->mapping();
278279

279280
// Counting files

0 commit comments

Comments
 (0)