PluginProbe ʕ •ᴥ•ʔ
Wordfence Security – Firewall, Malware Scan, and Login Security / 7.1.11
Wordfence Security – Firewall, Malware Scan, and Login Security v7.1.11
8.2.2 8.2.1 8.2.0 3.7.1 3.7.2 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.8.8 3.8.9 3.9.1 4.0.1 4.0.2 4.0.3 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.0.9 5.1.1 5.1.2 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.1 5.3.10 5.3.11 5.3.12 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.3.8 5.3.9 6.0.1 6.0.10 6.0.11 6.0.12 6.0.14 6.0.15 6.0.16 6.0.17 6.0.18 6.0.19 6.0.2 6.0.20 6.0.21 6.0.22 6.0.23 6.0.24 6.0.25 6.0.3 6.0.4 6.0.5 6.0.6 6.0.7 6.0.8 6.0.9 6.1.1 6.1.10 6.1.11 6.1.12 6.1.14 6.1.15 6.1.16 6.1.17 6.1.2 6.1.3 6.1.4 6.1.5 6.1.6 6.1.7 6.1.8 6.1.9 6.2.0 6.2.1 6.2.10 6.2.2 6.2.3 6.2.4 6.2.5 6.2.6 6.2.7 6.2.8 6.2.9 6.3.0 6.3.1 6.3.10 6.3.11 6.3.12 6.3.14 6.3.15 6.3.16 6.3.17 6.3.18 6.3.19 6.3.2 6.3.20 6.3.21 6.3.22 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.3.8 6.3.9 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1.0 7.1.1 7.1.10 7.1.11 7.1.12 7.1.14 7.1.15 7.1.16 7.1.17 7.1.18 7.1.19 7.1.2 7.1.20 7.1.3 7.1.4 7.1.5 7.1.6 7.1.7 7.1.8 7.1.9 7.10.0 7.10.1 7.10.2 7.10.3 7.10.4 7.10.5 7.10.6 7.10.7 7.11.0 7.11.1 7.11.2 7.11.3 7.11.4 7.11.5 7.11.6 7.11.7 7.2.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3.1 7.3.2 7.3.3 7.3.4 7.3.5 7.3.6 7.4.0 7.4.1 7.4.10 7.4.11 7.4.12 7.4.14 7.4.2 7.4.3 trunk 7.4.4 1.1 7.4.5 1.2 7.4.6 1.3 7.4.7 1.3.1 7.4.8 1.3.2 7.4.9 1.3.3 7.5.0 1.4.2 7.5.1 1.4.3 7.5.10 1.4.4 7.5.11 1.4.5 7.5.2 1.4.6 7.5.3 1.4.7 7.5.4 1.4.8 7.5.5 1.5.1 7.5.6 1.5.2 7.5.7 1.5.3 7.5.8 1.5.4 7.5.9 1.5.5 7.6.0 1.5.6 7.6.1 2.0.1 7.6.2 2.0.2 7.7.0 2.0.3 7.7.1 2.0.5 7.8.0 2.0.6 7.8.1 2.0.7 7.8.2 2.1.0 7.9.0 2.1.1 7.9.1 2.1.2 7.9.2 2.1.3 7.9.3 2.1.4 8.0.0 2.1.5 8.0.1 3.0.2 8.0.2 3.0.3 8.0.3 3.0.4 8.0.4 3.0.5 8.0.5 3.0.6 8.1.0 3.0.7 8.1.1 3.0.8 8.1.2 3.0.9 8.1.3 3.1.0 8.1.4 3.1.1 v1.4.1 3.1.2 3.1.4 3.1.6 3.2.1 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.1 3.4.4 3.4.5 3.5.1 3.5.2 3.6.1 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.6.9
wordfence / lib / wordfenceHash.php
wordfence / lib Last commit date
Diff 8 years ago dashboard 7 years ago .htaccess 8 years ago Diff.php 14 years ago GeoLite2-Country.mmdb 7 years ago IPTraf.php 8 years ago IPTrafList.php 8 years ago compat.php 8 years ago conntest.php 7 years ago cronview.php 8 years ago dbview.php 8 years ago diffResult.php 8 years ago email_genericAlert.php 7 years ago email_newIssues.php 7 years ago email_unlockRequest.php 8 years ago email_unsubscribeRequest.php 7 years ago live_activity.php 8 years ago menu_dashboard.php 8 years ago menu_dashboard_options.php 8 years ago menu_firewall.php 8 years ago menu_firewall_blocking.php 7 years ago menu_firewall_blocking_options.php 8 years ago menu_firewall_waf.php 8 years ago menu_firewall_waf_options.php 8 years ago menu_options.php 7 years ago menu_scanner.php 8 years ago menu_scanner_credentials.php 8 years ago menu_scanner_options.php 8 years ago menu_support.php 7 years ago menu_tools.php 8 years ago menu_tools_commentSpam.php 8 years ago menu_tools_diagnostic.php 7 years ago menu_tools_livetraffic.php 7 years ago menu_tools_twoFactor.php 8 years ago menu_tools_whois.php 8 years ago sysinfo.php 8 years ago unknownFiles.php 8 years ago viewFullActivityLog.php 8 years ago wf503.php 7 years ago wfAPI.php 7 years ago wfActivityReport.php 7 years ago wfAdminNoticeQueue.php 8 years ago wfArray.php 7 years ago wfBrowscap.php 8 years ago wfBrowscapCache.php 8 years ago wfBulkCountries.php 8 years ago wfCache.php 9 years ago wfConfig.php 7 years ago wfCountryMap.php 8 years ago wfCrawl.php 8 years ago wfCredentialsController.php 8 years ago wfCrypt.php 8 years ago wfDB.php 8 years ago wfDashboard.php 8 years ago wfDateLocalization.php 8 years ago wfDiagnostic.php 8 years ago wfDict.php 8 years ago wfDirectoryIterator.php 7 years ago wfHelperBin.php 11 years ago wfHelperString.php 11 years ago wfIPWhitelist.php 9 years ago wfImportExportController.php 8 years ago wfIssues.php 7 years ago wfLockedOut.php 7 years ago wfLog.php 7 years ago wfMD5BloomFilter.php 8 years ago wfNotification.php 8 years ago wfOnboardingController.php 7 years ago wfPersistenceController.php 8 years ago wfRESTAPI.php 9 years ago wfScan.php 7 years ago wfScanEngine.php 7 years ago wfSchema.php 7 years ago wfStyle.php 8 years ago wfSupportController.php 7 years ago wfUnlockMsg.php 7 years ago wfUpdateCheck.php 8 years ago wfUtils.php 7 years ago wfVersionCheckController.php 8 years ago wfView.php 10 years ago wfViewResult.php 8 years ago wordfenceClass.php 7 years ago wordfenceConstants.php 7 years ago wordfenceHash.php 8 years ago wordfenceScanner.php 7 years ago wordfenceURLHoover.php 7 years ago
wordfenceHash.php
841 lines
1 <?php
2 require_once('wordfenceClass.php');
3 class wordfenceHash {
4 private $engine = false;
5 private $db = false;
6 private $startTime = false;
7
8 //Begin serialized vars
9 public $striplen = 0;
10 public $totalFiles = 0;
11 public $totalDirs = 0;
12 public $totalData = 0; //To do a sanity check, don't use 'du' because it gets sparse files wrong and reports blocks used on disk. Use : find . -type f -ls | awk '{total += $7} END {print total}'
13 public $stoppedOnFile = false;
14 private $coreEnabled = false;
15 private $pluginsEnabled = false;
16 private $themesEnabled = false;
17 private $malwareEnabled = false;
18 private $coreUnknownEnabled = false;
19 private $knownFiles = false;
20 private $malwareData = "";
21 private $coreHashesData = '';
22 private $haveIssues = array();
23 private $status = array();
24 private $possibleMalware = array();
25 private $path = false;
26 private $only = false;
27 private $totalForks = 0;
28 private $alertedOnUnknownWordPressVersion = false;
29 private $foldersEntered = array();
30 private $foldersProcessed = array();
31 private $suspectedFiles = array();
32 private $indexed = false;
33 private $indexSize = 0;
34 private $currentIndex = 0;
35
36 /**
37 * @param string $striplen
38 * @param string $path
39 * @param array $only
40 * @param array $themes
41 * @param array $plugins
42 * @param wfScanEngine $engine
43 * @throws Exception
44 */
45 public function __construct($striplen, $path, $only, $themes, $plugins, $engine, $malwarePrefixesHash, $coreHashesHash, $scanMode) {
46 $this->striplen = $striplen;
47 $this->path = $path;
48 $this->only = $only;
49 $this->engine = $engine;
50
51 $this->startTime = microtime(true);
52
53 $options = $this->engine->scanController()->scanOptions();
54 if ($options['scansEnabled_core']) { $this->coreEnabled = true; }
55 if ($options['scansEnabled_plugins']) { $this->pluginsEnabled = true; }
56 if ($options['scansEnabled_themes']) { $this->themesEnabled = true; }
57 if ($options['scansEnabled_malware']) { $this->malwareEnabled = true; }
58 if ($options['scansEnabled_coreUnknown']) { $this->coreUnknownEnabled = true; }
59
60 $this->db = new wfDB();
61
62 //Doing a delete for now. Later we can optimize this to only scan modified files.
63 //$this->db->queryWrite("update " . wfDB::networkTable('wfFileMods') . " set oldMD5 = newMD5");
64 $this->db->truncate(wfDB::networkTable('wfFileMods'));
65 $this->db->truncate(wfDB::networkTable('wfKnownFileList'));
66 $this->db->truncate(wfDB::networkTable('wfPendingIssues'));
67 $fetchCoreHashesStatus = wfIssues::statusStart("Fetching core, theme and plugin file signatures from Wordfence");
68 try {
69 $this->knownFiles = $this->engine->getKnownFilesLoader()->getKnownFiles();
70 } catch (wfScanKnownFilesException $e) {
71 wfIssues::statusEndErr();
72 throw $e;
73 }
74 wfIssues::statusEnd($fetchCoreHashesStatus, wfIssues::STATUS_SUCCESS);
75 if ($this->malwareEnabled) {
76 $malwarePrefixStatus = wfIssues::statusStart("Fetching list of known malware files from Wordfence");
77
78 $stored = wfConfig::get_ser('malwarePrefixes', array(), false);
79 if (is_array($stored) && isset($stored['hash']) && $stored['hash'] == $malwarePrefixesHash && isset($stored['prefixes']) && wfWAFUtils::strlen($stored['prefixes']) % 4 == 0) {
80 wordfence::status(4, 'info', "Using cached malware prefixes");
81 }
82 else {
83 wordfence::status(4, 'info', "Fetching fresh malware prefixes");
84
85 $malwareData = $engine->api->getStaticURL('/malwarePrefixes.bin');
86 if (!$malwareData) {
87 wfIssues::statusEndErr();
88 throw new Exception("Could not fetch malware signatures from Wordfence servers.");
89 }
90
91 if (wfWAFUtils::strlen($malwareData) % 4 != 0) {
92 wfIssues::statusEndErr();
93 throw new Exception("Malware data received from Wordfence servers was not valid.");
94 }
95
96 $stored = array('hash' => $malwarePrefixesHash, 'prefixes' => $malwareData);
97 wfConfig::set_ser('malwarePrefixes', $stored, true, wfConfig::DONT_AUTOLOAD);
98 }
99
100 $this->malwareData = $stored['prefixes'];
101 wfIssues::statusEnd($malwarePrefixStatus, wfIssues::STATUS_SUCCESS);
102 }
103
104 if ($this->coreUnknownEnabled) {
105 $coreHashesStatus = wfIssues::statusStart("Fetching list of known core files from Wordfence");
106
107 $stored = wfConfig::get_ser('coreHashes', array(), false);
108 if (is_array($stored) && isset($stored['hash']) && $stored['hash'] == $coreHashesHash && isset($stored['hashes']) && wfWAFUtils::strlen($stored['hashes']) > 0 && wfWAFUtils::strlen($stored['hashes']) % 32 == 0) {
109 wordfence::status(4, 'info', "Using cached core hashes");
110 }
111 else {
112 wordfence::status(4, 'info', "Fetching fresh core hashes");
113
114 $coreHashesData = $engine->api->getStaticURL('/coreHashes.bin');
115 if (!$coreHashesData) {
116 wfIssues::statusEndErr();
117 throw new Exception("Could not fetch core hashes from Wordfence servers.");
118 }
119
120 if (wfWAFUtils::strlen($coreHashesData) % 32 != 0) {
121 wfIssues::statusEndErr();
122 throw new Exception("Core hashes data received from Wordfence servers was not valid.");
123 }
124
125 $stored = array('hash' => $coreHashesHash, 'hashes' => $coreHashesData);
126 wfConfig::set_ser('coreHashes', $stored, true, wfConfig::DONT_AUTOLOAD);
127 }
128
129 $this->coreHashesData = $stored['hashes'];
130 wfIssues::statusEnd($coreHashesStatus, wfIssues::STATUS_SUCCESS);
131 }
132
133 if($this->path[strlen($this->path) - 1] != '/'){
134 $this->path .= '/';
135 }
136 if(! is_readable($path)){
137 throw new Exception("Could not read directory " . $this->path . " to do scan.");
138 }
139 $this->haveIssues = array(
140 'core' => wfIssues::STATUS_SECURE,
141 'coreUnknown' => wfIssues::STATUS_SECURE,
142 'themes' => wfIssues::STATUS_SECURE,
143 'plugins' => wfIssues::STATUS_SECURE,
144 'malware' => wfIssues::STATUS_SECURE,
145 );
146 if($this->coreEnabled){ $this->status['core'] = wfIssues::statusStart("Comparing core WordPress files against originals in repository"); $this->engine->scanController()->startStage(wfScanner::STAGE_FILE_CHANGES); } else { wfIssues::statusDisabled("Skipping core scan"); }
147 if($this->themesEnabled){ $this->status['themes'] = wfIssues::statusStart("Comparing open source themes against WordPress.org originals"); $this->engine->scanController()->startStage(wfScanner::STAGE_FILE_CHANGES); } else { wfIssues::statusDisabled("Skipping theme scan"); }
148 if($this->pluginsEnabled){ $this->status['plugins'] = wfIssues::statusStart("Comparing plugins against WordPress.org originals"); $this->engine->scanController()->startStage(wfScanner::STAGE_FILE_CHANGES); } else { wfIssues::statusDisabled("Skipping plugin scan"); }
149 if($this->malwareEnabled){ $this->status['malware'] = wfIssues::statusStart("Scanning for known malware files"); $this->engine->scanController()->startStage(wfScanner::STAGE_MALWARE_SCAN); } else { wfIssues::statusDisabled("Skipping malware scan"); }
150 if($this->coreUnknownEnabled){ $this->status['coreUnknown'] = wfIssues::statusStart("Scanning for unknown files in wp-admin and wp-includes"); $this->engine->scanController()->startStage(wfScanner::STAGE_FILE_CHANGES); } else { wfIssues::statusDisabled("Skipping unknown core file scan"); }
151
152 if ($options['scansEnabled_fileContents']) { $this->engine->scanController()->startStage(wfScanner::STAGE_MALWARE_SCAN); }
153 if ($options['scansEnabled_fileContentsGSB']) { $this->engine->scanController()->startStage(wfScanner::STAGE_CONTENT_SAFETY); }
154
155 if ($this->coreUnknownEnabled && !$this->alertedOnUnknownWordPressVersion && empty($this->knownFiles['core'])) {
156 require(ABSPATH . 'wp-includes/version.php'); //defines $wp_version
157 $this->alertedOnUnknownWordPressVersion = true;
158 $added = $this->engine->addIssue(
159 'coreUnknown',
160 2,
161 'coreUnknown' . $wp_version,
162 'coreUnknown' . $wp_version,
163 'Unknown WordPress core version: ' . $wp_version,
164 "The core files scan will not be run because this version of WordPress is not currently indexed by Wordfence. This may be due to using a prerelease version or because the servers are still indexing a new release. If you are using an official WordPress release, this issue will automatically dismiss once the version is indexed and another scan is run.",
165 array()
166 );
167
168 if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $this->haveIssues['coreUnknown'] = wfIssues::STATUS_PROBLEM; }
169 else if ($this->haveIssues['coreUnknown'] != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $this->haveIssues['coreUnknown'] = wfIssues::STATUS_IGNORED; }
170 }
171 }
172 public function __sleep(){
173 return array('striplen', 'totalFiles', 'totalDirs', 'totalData', 'stoppedOnFile', 'coreEnabled', 'pluginsEnabled', 'themesEnabled', 'malwareEnabled', 'coreUnknownEnabled', 'knownFiles', 'haveIssues', 'status', 'possibleMalware', 'path', 'only', 'totalForks', 'alertedOnUnknownWordPressVersion', 'foldersProcessed', 'suspectedFiles', 'indexed', 'indexSize', 'currentIndex', 'foldersEntered');
174 }
175 public function __wakeup(){
176 $this->db = new wfDB();
177 $this->startTime = microtime(true);
178 $this->totalForks++;
179
180 $stored = wfConfig::get_ser('malwarePrefixes', array(), false);
181 if (!isset($stored['prefixes'])) {
182 $stored['prefixes'] = '';
183 }
184 $this->malwareData = $stored['prefixes'];
185
186 $stored = wfConfig::get_ser('coreHashes', array(), false);
187 if (!isset($stored['hashes'])) {
188 $stored['hashes'] = '';
189 }
190 $this->coreHashesData = $stored['hashes'];
191 }
192 public function getSuspectedFiles() {
193 return array_keys($this->suspectedFiles);
194 }
195 public function run($engine){ //base path and 'only' is a list of files and dirs in the bast that are the only ones that should be processed. Everything else in base is ignored. If only is empty then everything is processed.
196 if($this->totalForks > 1000){
197 throw new Exception("Wordfence file scanner detected a possible infinite loop. Exiting on file: " . $this->stoppedOnFile);
198 }
199 $this->engine = $engine;
200 wordfence::status(4, 'info', "Indexing files for scanning");
201 if (!$this->indexed) {
202 $start = microtime(true);
203 $indexedFiles = array();
204
205 if (count($this->only) > 0) {
206 $files = $this->only;
207 }
208 else {
209 $files = scandir($this->path);
210 }
211
212 foreach ($files as $file) {
213 if ($file == '.' || $file == '..') { continue; }
214 $file = $this->path . $file;
215 $this->_dirIndex($file, $indexedFiles);
216 }
217 $this->_serviceIndexQueue($indexedFiles, true);
218 $this->indexed = true;
219 unset($this->foldersEntered); $this->foldersEntered = array();
220 unset($this->foldersProcessed); $this->foldersProcessed = array();
221 $end = microtime(true);
222 wordfence::status(4, 'info', "Index time: " . ($end - $start));
223 }
224
225 $this->_checkForTimeout('');
226
227 wordfence::status(4, 'info', "Beginning file hashing");
228 while ($file = $this->_nextFile()) {
229 $this->processFile($file);
230 $this->_checkForTimeout($file);
231 }
232
233 wordfence::status(4, 'info', "Processing pending issues");
234 $this->_processPendingIssues();
235
236 wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data.");
237 if($this->coreEnabled){ wfIssues::statusEnd($this->status['core'], $this->haveIssues['core']); $this->engine->scanController()->completeStage(wfScanner::STAGE_FILE_CHANGES, $this->haveIssues['core']); }
238 if($this->themesEnabled){ wfIssues::statusEnd($this->status['themes'], $this->haveIssues['themes']); $this->engine->scanController()->completeStage(wfScanner::STAGE_FILE_CHANGES, $this->haveIssues['themes']); }
239 if($this->pluginsEnabled){ wfIssues::statusEnd($this->status['plugins'], $this->haveIssues['plugins']); $this->engine->scanController()->completeStage(wfScanner::STAGE_FILE_CHANGES, $this->haveIssues['plugins']); }
240 if($this->coreUnknownEnabled){ wfIssues::statusEnd($this->status['coreUnknown'], $this->haveIssues['coreUnknown']); $this->engine->scanController()->completeStage(wfScanner::STAGE_FILE_CHANGES, $this->haveIssues['coreUnknown']); }
241 if(sizeof($this->possibleMalware) > 0){
242 $malwareResp = $engine->api->binCall('check_possible_malware', json_encode($this->possibleMalware));
243 if($malwareResp['code'] != 200){
244 wfIssues::statusEndErr();
245 throw new Exception("Invalid response from Wordfence API during check_possible_malware");
246 }
247 $malwareList = json_decode($malwareResp['data'], true);
248 if(is_array($malwareList) && sizeof($malwareList) > 0){
249 for($i = 0; $i < sizeof($malwareList); $i++){
250 $file = $malwareList[$i][0];
251 $md5 = $malwareList[$i][1];
252 $name = $malwareList[$i][2];
253 $added = $this->engine->addIssue(
254 'file',
255 1,
256 $this->path . $file,
257 $md5,
258 'This file is suspected malware: ' . $file,
259 "This file's signature matches a known malware file. The title of the malware is '" . $name . "'. Immediately inspect this file using the 'View' option below and consider deleting it from your server.",
260 array(
261 'file' => $file,
262 'cType' => 'unknown',
263 'canDiff' => false,
264 'canFix' => false,
265 'canDelete' => true
266 )
267 );
268
269 if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $this->haveIssues['malware'] = wfIssues::STATUS_PROBLEM; }
270 else if ($this->haveIssues['malware'] != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $this->haveIssues['malware'] = wfIssues::STATUS_IGNORED; }
271 }
272 }
273 }
274 if($this->malwareEnabled){ wfIssues::statusEnd($this->status['malware'], $this->haveIssues['malware']); $this->engine->scanController()->completeStage(wfScanner::STAGE_MALWARE_SCAN, $this->haveIssues['malware']); }
275 unset($this->knownFiles); $this->knownFiles = false;
276 }
277 private function _dirIndex($path, &$indexedFiles) {
278 if (substr($path, -3, 3) == '/..' || substr($path, -2, 2) == '/.') {
279 return;
280 }
281 if (!is_readable($path)) { return; } //Applies to files and dirs
282 if (!$this->_shouldProcessPath($path)) { return; }
283 if (is_dir($path)) {
284 $realPath = realpath($path);
285 if (!$this->stoppedOnFile && isset($this->foldersEntered[$realPath])) { //Not resuming and already entered this path
286 return;
287 }
288
289 $this->foldersEntered[$realPath] = 1;
290
291 $this->totalDirs++;
292 if ($path[strlen($path) - 1] != '/') {
293 $path .= '/';
294 }
295 $cont = scandir($path);
296 for ($i = 0; $i < sizeof($cont); $i++) {
297 if ($cont[$i] == '.' || $cont[$i] == '..') { continue; }
298 $file = $path . $cont[$i];
299 if (is_file($file)) {
300 $relativeFile = substr($file, $this->striplen);
301 if ($this->stoppedOnFile && $relativeFile != $this->stoppedOnFile) {
302 continue;
303 }
304
305 if (preg_match('/\.suspected$/i', $relativeFile)) { //Already iterating over all files in the search areas so generate this list here
306 wordfence::status(4, 'info', "Found .suspected file: {$relativeFile}");
307 $this->suspectedFiles[$relativeFile] = 1;
308 }
309
310 $this->_checkForTimeout($file, $indexedFiles);
311 if ($this->_shouldHashFile($file)) {
312 $indexedFiles[] = $relativeFile;
313 }
314 else {
315 wordfence::status(4, 'info', "Skipping unneeded hash: {$file}");
316 }
317 $this->_serviceIndexQueue($indexedFiles);
318 } else if (is_dir($file)) {
319 $this->_dirIndex($file, $indexedFiles);
320 }
321 }
322
323 $this->foldersProcessed[$realPath] = 1;
324 unset($this->foldersEntered[$realPath]);
325 }
326 else {
327 if (is_file($path)) {
328 $relativeFile = substr($path, $this->striplen);
329 if ($this->stoppedOnFile && $relativeFile != $this->stoppedOnFile) {
330 return;
331 }
332
333 if (preg_match('/\.suspected$/i', $relativeFile)) { //Already iterating over all files in the search areas so generate this list here
334 wordfence::status(4, 'info', "Found .suspected file: {$relativeFile}");
335 $this->suspectedFiles[$relativeFile] = 1;
336 }
337
338 $this->_checkForTimeout($path, $indexedFiles);
339 if ($this->_shouldHashFile($path)) {
340 $indexedFiles[] = substr($path, $this->striplen);
341 }
342 else {
343 wordfence::status(4, 'info', "Skipping unneeded hash: {$path}");
344 }
345 $this->_serviceIndexQueue($indexedFiles);
346 }
347 }
348 }
349 private function _serviceIndexQueue(&$indexedFiles, $final = false) {
350 $payload = array();
351 if (count($indexedFiles) > 500) {
352 $payload = array_splice($indexedFiles, 0, 500);
353 }
354 else if ($final) {
355 $payload = $indexedFiles;
356 $indexedFiles = array();
357 }
358
359 if (count($payload) > 0) {
360 global $wpdb;
361 $table_wfKnownFileList = wfDB::networkTable('wfKnownFileList');
362 $query = substr("INSERT INTO {$table_wfKnownFileList} (path) VALUES " . str_repeat("('%s'), ", count($payload)), 0, -2);
363 $wpdb->query($wpdb->prepare($query, $payload));
364 $this->indexSize += count($payload);
365 wordfence::status(2, 'info', "{$this->indexSize} files indexed");
366 }
367 }
368 private function _nextFile($advanceCursor = true) {
369 static $files = array();
370 if (count($files) == 0) {
371 global $wpdb;
372 $table_wfKnownFileList = wfDB::networkTable('wfKnownFileList');
373 $files = $wpdb->get_col($wpdb->prepare("SELECT path FROM {$table_wfKnownFileList} WHERE id > %d ORDER BY id ASC LIMIT 500", $this->currentIndex));
374 }
375
376 $file = null;
377 if ($advanceCursor) {
378 $file = array_shift($files);
379 $this->currentIndex++;
380 }
381 else if (count($files) > 0) {
382 $file = $files[0];
383 }
384
385 if ($file === null) {
386 return null;
387 }
388 return ABSPATH . $file;
389 }
390 private function _checkForTimeout($path, $indexQueue = false) {
391 $file = substr($path, $this->striplen);
392 if ((!$this->stoppedOnFile) && $this->engine->shouldFork()) { //max X seconds but don't allow fork if we're looking for the file we stopped on. Search mode is VERY fast.
393 if ($indexQueue !== false) {
394 $this->_serviceIndexQueue($indexQueue, true);
395 $this->stoppedOnFile = $file;
396 wordfence::status(4, 'info', "Forking during indexing: " . $path);
397 }
398 else {
399 wordfence::status(4, 'info', "Calling fork() from wordfenceHash with maxExecTime: " . $this->engine->maxExecTime);
400 }
401 $this->engine->fork();
402 //exits
403 }
404
405 if ($this->stoppedOnFile && $file != $this->stoppedOnFile && $indexQueue !== false) {
406 return;
407 }
408 else if ($this->stoppedOnFile && $file == $this->stoppedOnFile) {
409 $this->stoppedOnFile = false; //Continue indexing
410 }
411 }
412 private function _shouldProcessPath($path) {
413 $file = substr($path, $this->striplen);
414 $exclude = wordfenceScanner::getExcludeFilePattern(wordfenceScanner::EXCLUSION_PATTERNS_USER);
415 if ($exclude && preg_match($exclude, $file)) {
416 return false;
417 }
418
419 $realPath = realpath($path);
420 if (isset($this->foldersProcessed[$realPath])) {
421 return false;
422 }
423
424 return true;
425 }
426 private function processFile($realFile) {
427 $file = substr($realFile, $this->striplen);
428
429 if (wfUtils::fileTooBig($realFile)) {
430 wordfence::status(4, 'info', "Skipping file larger than max size: $realFile");
431 return;
432 }
433
434 if (function_exists('memory_get_usage')) {
435 wordfence::status(4, 'info', "Scanning: $realFile (Mem:" . sprintf('%.1f', memory_get_usage(true) / (1024 * 1024)) . "M)");
436 }
437 else {
438 wordfence::status(4, 'info', "Scanning: $realFile");
439 }
440
441 wfUtils::beginProcessingFile($file);
442 $wfHash = self::wfHash($realFile);
443 $this->engine->scanController()->incrementSummaryItem(wfScanner::SUMMARY_SCANNED_FILES);
444 if ($wfHash) {
445 $md5 = strtoupper($wfHash[0]);
446 $shac = strtoupper($wfHash[1]);
447 $knownFile = 0;
448 if($this->malwareEnabled && $this->isMalwarePrefix($md5)){
449 $this->possibleMalware[] = array($file, $md5);
450 }
451
452 $knownFileExclude = wordfenceScanner::getExcludeFilePattern(wordfenceScanner::EXCLUSION_PATTERNS_KNOWN_FILES);
453 $allowKnownFileScan = true;
454 if ($knownFileExclude) {
455 $allowKnownFileScan = !preg_match($knownFileExclude, $realFile);
456 }
457
458 if ($allowKnownFileScan) {
459 if (isset($this->knownFiles['core'][$file])) {
460 if (strtoupper($this->knownFiles['core'][$file]) == $shac) {
461 $knownFile = 1;
462 }
463 else {
464 if ($this->coreEnabled) {
465 $localFile = ABSPATH . '/' . preg_replace('/^[\.\/]+/', '', $file);
466 $fileContents = @file_get_contents($localFile);
467 if ($fileContents && (!preg_match('/<\?' . 'php[\r\n\s\t]*\/\/[\r\n\s\t]*Silence is golden\.[\r\n\s\t]*(?:\?>)?[\r\n\s\t]*$/s', $fileContents))) {
468 $this->engine->addPendingIssue(
469 'knownfile',
470 1,
471 'coreModified' . $file,
472 'coreModified' . $file . $md5,
473 'WordPress core file modified: ' . $file,
474 "This WordPress core file has been modified and differs from the original file distributed with this version of WordPress.",
475 array(
476 'file' => $file,
477 'cType' => 'core',
478 'canDiff' => true,
479 'canFix' => true,
480 'canDelete' => false,
481 'haveIssues' => 'core'
482 )
483 );
484 }
485 }
486 }
487 }
488 else if (isset($this->knownFiles['plugins'][$file])) {
489 if (in_array($shac, $this->knownFiles['plugins'][$file])) {
490 $knownFile = 1;
491 }
492 else {
493 if ($this->pluginsEnabled) {
494 $options = $this->engine->scanController()->scanOptions();
495 $shouldGenerateIssue = true;
496 if (!$options['scansEnabled_highSense'] && preg_match('~/readme\.(?:txt|md)$~i', $file)) { //Don't generate issues for changed readme files unless high sensitivity is on
497 $shouldGenerateIssue = false;
498 }
499
500 if ($shouldGenerateIssue) {
501 $itemName = $this->knownFiles['plugins'][$file][0];
502 $itemVersion = $this->knownFiles['plugins'][$file][1];
503 $cKey = $this->knownFiles['plugins'][$file][2];
504 $this->engine->addPendingIssue(
505 'knownfile',
506 2,
507 'modifiedplugin' . $file,
508 'modifiedplugin' . $file . $md5,
509 'Modified plugin file: ' . $file,
510 "This file belongs to plugin \"$itemName\" version \"$itemVersion\" and has been modified from the file that is distributed by WordPress.org for this version. Please use the link to see how the file has changed. If you have modified this file yourself, you can safely ignore this warning. If you see a lot of changed files in a plugin that have been made by the author, then try uninstalling and reinstalling the plugin to force an upgrade. Doing this is a workaround for plugin authors who don't manage their code correctly. [See our FAQ on www.wordfence.com for more info]",
511 array(
512 'file' => $file,
513 'cType' => 'plugin',
514 'canDiff' => true,
515 'canFix' => true,
516 'canDelete' => false,
517 'cName' => $itemName,
518 'cVersion' => $itemVersion,
519 'cKey' => $cKey,
520 'haveIssues' => 'plugins'
521 )
522 );
523 }
524 }
525
526 }
527 }
528 else if (isset($this->knownFiles['themes'][$file])) {
529 if (in_array($shac, $this->knownFiles['themes'][$file])) {
530 $knownFile = 1;
531 }
532 else {
533 if ($this->themesEnabled) {
534 $options = $this->engine->scanController()->scanOptions();
535 $shouldGenerateIssue = true;
536 if (!$options['scansEnabled_highSense'] && preg_match('~/readme\.(?:txt|md)$~i', $file)) { //Don't generate issues for changed readme files unless high sensitivity is on
537 $shouldGenerateIssue = false;
538 }
539
540 if ($shouldGenerateIssue) {
541 $itemName = $this->knownFiles['themes'][$file][0];
542 $itemVersion = $this->knownFiles['themes'][$file][1];
543 $cKey = $this->knownFiles['themes'][$file][2];
544 $this->engine->addPendingIssue(
545 'knownfile',
546 2,
547 'modifiedtheme' . $file,
548 'modifiedtheme' . $file . $md5,
549 'Modified theme file: ' . $file,
550 "This file belongs to theme \"$itemName\" version \"$itemVersion\" and has been modified from the original distribution. It is common for site owners to modify their theme files, so if you have modified this file yourself you can safely ignore this warning.",
551 array(
552 'file' => $file,
553 'cType' => 'theme',
554 'canDiff' => true,
555 'canFix' => true,
556 'canDelete' => false,
557 'cName' => $itemName,
558 'cVersion' => $itemVersion,
559 'cKey' => $cKey,
560 'haveIssues' => 'themes'
561 )
562 );
563 }
564 }
565
566 }
567 }
568 else if ($this->coreUnknownEnabled && !$this->alertedOnUnknownWordPressVersion) { //Check for unknown files in system directories
569 $restrictedWordPressFolders = array(ABSPATH . 'wp-admin/', ABSPATH . WPINC . '/');
570 $added = false;
571 foreach ($restrictedWordPressFolders as $path) {
572 if (strpos($realFile, $path) === 0) {
573 if ($this->isPreviousCoreFile($shac)) {
574 $added = $this->engine->addIssue(
575 'knownfile',
576 2,
577 'coreUnknown' . $file,
578 'coreUnknown' . $file . $md5,
579 sprintf(__('Old WordPress core file not removed during update: %s', 'wordfence'), $file),
580 __('This file is in a WordPress core location but is from an older version of WordPress and not used with your current version. Hosting or permissions issues can cause these files to get left behind when WordPress is updated and they should be removed if possible.', 'wordfence'),
581 array(
582 'file' => $file,
583 'cType' => 'core',
584 'canDiff' => false,
585 'canFix' => false,
586 'canDelete' => true,
587 )
588 );
589 }
590 else {
591 $added = $this->engine->addIssue(
592 'knownfile',
593 2,
594 'coreUnknown' . $file,
595 'coreUnknown' . $file . $md5,
596 'Unknown file in WordPress core: ' . $file,
597 "This file is in a WordPress core location but is not distributed with this version of WordPress. This is usually due to it being left over from a previous WordPress update, but it may also have been added by another plugin or a malicious file added by an attacker.",
598 array(
599 'file' => $file,
600 'cType' => 'core',
601 'canDiff' => false,
602 'canFix' => false,
603 'canDelete' => true,
604 )
605 );
606 }
607 }
608 }
609
610 if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $this->haveIssues['coreUnknown'] = wfIssues::STATUS_PROBLEM; }
611 else if ($this->haveIssues['coreUnknown'] != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $this->haveIssues['coreUnknown'] = wfIssues::STATUS_IGNORED; }
612 }
613 }
614 // knownFile means that the file is both part of core or a known plugin or theme AND that we recognize the file's hash.
615 // we could split this into files whose path we recognize and file's whose path we recognize AND who have a valid sig.
616 // But because we want to scan files whose sig we don't recognize, regardless of known path or not, we only need one "knownFile" field.
617 $fileModsTable = wfDB::networkTable('wfFileMods');
618 $this->db->queryWrite("INSERT INTO {$fileModsTable} (filename, filenameMD5, knownFile, oldMD5, newMD5, SHAC) VALUES ('%s', UNHEX(MD5('%s')), %d, '', UNHEX('%s'), UNHEX('%s')) ON DUPLICATE KEY UPDATE newMD5 = UNHEX('%s'), SHAC = UNHEX('%s'), knownFile = %d", $file, $file, $knownFile, $md5, $shac, $md5, $shac, $knownFile);
619
620 $this->totalFiles++;
621 $this->totalData += @filesize($realFile); //We already checked if file overflows int in the fileTooBig routine above
622 if($this->totalFiles % 100 === 0){
623 wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data so far");
624 }
625 } else {
626 //wordfence::status(2, 'error', "Could not gen hash for file (probably because we don't have permission to access the file): $realFile");
627 }
628 wfUtils::endProcessingFile();
629 }
630 private function _processPendingIssues() {
631 $fileModsTable = wfDB::networkTable('wfFileMods');
632
633 $count = $this->engine->getPendingIssueCount();
634 $offset = 0;
635 while ($offset < $count) {
636 $issues = $this->engine->getPendingIssues($offset);
637 if (count($issues) == 0) {
638 break;
639 }
640
641 //Do a bulk check of is_safe_file
642 $hashesToCheck = array();
643 foreach ($issues as &$i) {
644 $shac = $this->db->querySingle("SELECT HEX(SHAC) FROM {$fileModsTable} WHERE filename = '%s' AND isSafeFile = '?'", $i['data']['file']);
645 $shac = strtoupper($shac);
646 $i['shac'] = null;
647 if ($shac !== null) {
648 $shac = strtoupper($shac);
649 $i['shac'] = $shac;
650 $hashesToCheck[] = $shac;
651 }
652 }
653
654 $safeFiles = array();
655 if (count($hashesToCheck) > 0) {
656 $safeFiles = $this->isSafeFile($hashesToCheck);
657 }
658
659 //Migrate non-safe file issues to official issues
660 foreach ($issues as &$i) {
661 if (!in_array($i['shac'], $safeFiles)) {
662 $haveIssuesType = $i['data']['haveIssues'];
663 $added = $this->engine->addIssue(
664 $i['type'],
665 $i['severity'],
666 $i['ignoreP'],
667 $i['ignoreC'],
668 $i['shortMsg'],
669 $i['longMsg'],
670 $i['data'],
671 true //Prevent ignoreP and ignoreC from being hashed again
672 );
673 if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $this->haveIssues[$haveIssuesType] = wfIssues::STATUS_PROBLEM; }
674 else if ($this->haveIssues[$haveIssuesType] != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $this->haveIssues[$haveIssuesType] = wfIssues::STATUS_IGNORED; }
675 $this->db->queryWrite("UPDATE {$fileModsTable} SET isSafeFile = '0' WHERE SHAC = UNHEX('%s')", $i['shac']);
676 }
677 else {
678 $this->db->queryWrite("UPDATE {$fileModsTable} SET isSafeFile = '1' WHERE SHAC = UNHEX('%s')", $i['shac']);
679 }
680 }
681
682 $offset += count($issues);
683 $this->engine->checkForKill();
684 }
685 }
686 public static function wfHash($file){
687 $fp = @fopen($file, "rb");
688 if (!$fp) {
689 return false;
690 }
691 $md5Context = hash_init('md5');
692 $sha256Context = hash_init('sha256');
693 while (!feof($fp)) {
694 $data = fread($fp, 65536);
695 if ($data === false) {
696 return false;
697 }
698 hash_update($md5Context, $data);
699 hash_update($sha256Context, str_replace(array("\n","\r","\t"," "),"", $data));
700 }
701 $md5 = hash_final($md5Context, false);
702 $shac = hash_final($sha256Context, false);
703 return array($md5, $shac);
704 }
705 private function _shouldHashFile($fullPath) {
706 $file = substr($fullPath, $this->striplen);
707
708 //Core File, return true
709 if ((isset($this->knownFiles['core']) && isset($this->knownFiles['core'][$file])) ||
710 (isset($this->knownFiles['plugins']) && isset($this->knownFiles['plugins'][$file])) ||
711 (isset($this->knownFiles['themes']) && isset($this->knownFiles['themes'][$file]))) {
712 return true;
713 }
714
715 //Excluded file, return false
716 $excludePattern = wordfenceScanner::getExcludeFilePattern(wordfenceScanner::EXCLUSION_PATTERNS_USER | wordfenceScanner::EXCLUSION_PATTERNS_MALWARE);
717 if ($excludePattern && preg_match($excludePattern, $file)) {
718 return false;
719 }
720
721 //Unknown file in a core location
722 if ($this->coreUnknownEnabled && !$this->alertedOnUnknownWordPressVersion) {
723 $restrictedWordPressFolders = array(ABSPATH . 'wp-admin/', ABSPATH . WPINC . '/');
724 foreach ($restrictedWordPressFolders as $path) {
725 if (strpos($fullPath, $path) === 0) {
726 return true;
727 }
728 }
729 }
730
731 //Determine treatment
732 $fileExt = '';
733 if (preg_match('/\.([a-zA-Z\d\-]{1,7})$/', $file, $matches)) {
734 $fileExt = strtolower($matches[1]);
735 }
736 $isPHP = false;
737 if (preg_match('/\.(?:php(?:\d+)?|phtml)(\.|$)/i', $file)) {
738 $isPHP = true;
739 }
740 $isHTML = false;
741 if (preg_match('/\.(?:html?)(\.|$)/i', $file)) {
742 $isHTML = true;
743 }
744 $isJS = false;
745 if (preg_match('/\.(?:js|svg)(\.|$)/i', $file)) {
746 $isJS = true;
747 }
748
749 $options = $this->engine->scanController()->scanOptions();
750
751 //If scan images is disabled, only allow .js through
752 if (!$isPHP && preg_match('/^(?:jpg|jpeg|mp3|avi|m4v|mov|mp4|gif|png|tiff?|svg|sql|js|tbz2?|bz2?|xz|zip|tgz|gz|tar|log|err\d+)$/', $fileExt)) {
753 if (!$options['scansEnabled_scanImages'] && !$isJS) {
754 return false;
755 }
756 }
757
758 //If high sensitivity is disabled, don't allow .sql
759 if (strtolower($fileExt) == 'sql') {
760 if (!$options['scansEnabled_highSense']) {
761 return false;
762 }
763 }
764
765 //Treating as binary, return true
766 $treatAsBinary = ($isPHP || $isHTML || $options['scansEnabled_scanImages']);
767 if ($treatAsBinary) {
768 return true;
769 }
770
771 //Will be malware scanned, return true
772 if ($isJS) {
773 return true;
774 }
775
776 return false;
777 }
778 private function isMalwarePrefix($hexMD5){
779 $hasPrefix = $this->_binaryListContains($this->malwareData, wfUtils::hex2bin($hexMD5), 4);
780 return $hasPrefix !== false;
781 }
782 private function isPreviousCoreFile($hexContentsSHAC) {
783 $hasPrefix = $this->_binaryListContains($this->coreHashesData, wfUtils::hex2bin($hexContentsSHAC), 32);
784 return $hasPrefix !== false;
785 }
786
787 /**
788 * @param $binaryList The binary list to search, sorted as a binary string.
789 * @param $needle The binary needle to search for.
790 * @param int $size The byte size of each item in the list.
791 * @return bool|int false if not found, otherwise the index in the list
792 */
793 private function _binaryListContains($binaryList, $needle, $size /* bytes */) {
794 $p = substr($needle, 0, $size);
795
796 $count = ceil(wfWAFUtils::strlen($binaryList) / $size);
797 $low = 0;
798 $high = $count - 1;
799
800 while ($low <= $high) {
801 $mid = (int) (($high + $low) / 2);
802 $val = wfWAFUtils::substr($binaryList, $mid * $size, $size);
803 $cmp = strcmp($val, $p);
804 if ($cmp < 0) {
805 $low = $mid + 1;
806 }
807 else if ($cmp > 0) {
808 $high = $mid - 1;
809 }
810 else {
811 return $mid;
812 }
813 }
814
815 return false;
816 }
817
818 /**
819 * Queries the is_safe_file endpoint. If provided an array, it does a bulk check and returns an array containing the
820 * hashes that were marked as safe. If provided a string, it returns a boolean to indicate the safeness of the file.
821 *
822 * @param string|array $shac
823 * @return array|bool
824 */
825 private function isSafeFile($shac) {
826 if (is_array($shac)) {
827 $result = $this->engine->api->call('is_safe_file', array(), array('multipleSHAC' => json_encode($shac)));
828 if (isset($result['isSafe'])) {
829 return $result['isSafe'];
830 }
831 return array();
832 }
833
834 $result = $this->engine->api->call('is_safe_file', array(), array('shac' => strtoupper($shac)));
835 if(isset($result['isSafe']) && $result['isSafe'] == 1){
836 return true;
837 }
838 return false;
839 }
840 }
841