PluginProbe ʕ •ᴥ•ʔ
Wordfence Security – Firewall, Malware Scan, and Login Security / 7.2.4
Wordfence Security – Firewall, Malware Scan, and Login Security v7.2.4
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 rest-api 7 years ago .htaccess 7 years ago Diff.php 14 years ago GeoLite2-Country.mmdb 7 years ago IPTraf.php 8 years ago IPTrafList.php 7 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 flags.php 7 years ago live_activity.php 8 years ago menu_dashboard.php 7 years ago menu_dashboard_options.php 7 years ago menu_firewall.php 7 years ago menu_firewall_blocking.php 7 years ago menu_firewall_blocking_options.php 8 years ago menu_firewall_waf.php 7 years ago menu_firewall_waf_options.php 7 years ago menu_options.php 7 years ago menu_scanner.php 7 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 7 years ago menu_tools_diagnostic.php 7 years ago menu_tools_importExport.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 menu_wordfence_central.php 7 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 7 years ago wfBulkCountries.php 7 years ago wfCache.php 9 years ago wfCentralAPI.php 7 years ago wfConfig.php 7 years ago wfCrawl.php 8 years ago wfCredentialsController.php 7 years ago wfCrypt.php 8 years ago wfDB.php 7 years ago wfDashboard.php 7 years ago wfDateLocalization.php 8 years ago wfDiagnostic.php 7 years ago wfDict.php 8 years ago wfDirectoryIterator.php 7 years ago wfHelperBin.php 11 years ago wfHelperString.php 11 years ago wfIPWhitelist.php 7 years ago wfImportExportController.php 7 years ago wfIssues.php 7 years ago wfJWT.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 7 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 7 years ago wordfenceScanner.php 7 years ago wordfenceURLHoover.php 7 years ago
wordfenceHash.php
856 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 wfIssues::SEVERITY_MEDIUM,
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 base 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 wfIssues::SEVERITY_CRITICAL,
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 $resolvedFile = realpath($file);
313 if ($resolvedFile) {
314 $indexedFiles[] = substr($resolvedFile, $this->striplen);
315 }
316 }
317 else {
318 wordfence::status(4, 'info', "Skipping unneeded hash: {$file}");
319 }
320 $this->_serviceIndexQueue($indexedFiles);
321 } else if (is_dir($file)) {
322 $this->_dirIndex($file, $indexedFiles);
323 }
324 }
325
326 $this->foldersProcessed[$realPath] = 1;
327 unset($this->foldersEntered[$realPath]);
328 }
329 else {
330 if (is_file($path)) {
331 $relativeFile = substr($path, $this->striplen);
332 if ($this->stoppedOnFile && $relativeFile != $this->stoppedOnFile) {
333 return;
334 }
335
336 if (preg_match('/\.suspected$/i', $relativeFile)) { //Already iterating over all files in the search areas so generate this list here
337 wordfence::status(4, 'info', "Found .suspected file: {$relativeFile}");
338 $this->suspectedFiles[$relativeFile] = 1;
339 }
340
341 $this->_checkForTimeout($path, $indexedFiles);
342 if ($this->_shouldHashFile($path)) {
343 $indexedFiles[] = substr($path, $this->striplen);
344 }
345 else {
346 wordfence::status(4, 'info', "Skipping unneeded hash: {$path}");
347 }
348 $this->_serviceIndexQueue($indexedFiles);
349 }
350 }
351 }
352 private function _serviceIndexQueue(&$indexedFiles, $final = false) {
353 $payload = array();
354 if (count($indexedFiles) > 500) {
355 $payload = array_splice($indexedFiles, 0, 500);
356 }
357 else if ($final) {
358 $payload = $indexedFiles;
359 $indexedFiles = array();
360 }
361
362 if (count($payload) > 0) {
363 global $wpdb;
364 $table_wfKnownFileList = wfDB::networkTable('wfKnownFileList');
365 $query = substr("INSERT INTO {$table_wfKnownFileList} (path) VALUES " . str_repeat("('%s'), ", count($payload)), 0, -2);
366 $wpdb->query($wpdb->prepare($query, $payload));
367 $this->indexSize += count($payload);
368 wordfence::status(2, 'info', "{$this->indexSize} files indexed");
369 }
370 }
371 private function _nextFile($advanceCursor = true) {
372 static $files = array();
373 if (count($files) == 0) {
374 global $wpdb;
375 $table_wfKnownFileList = wfDB::networkTable('wfKnownFileList');
376 $files = $wpdb->get_col($wpdb->prepare("SELECT path FROM {$table_wfKnownFileList} WHERE id > %d ORDER BY id ASC LIMIT 500", $this->currentIndex));
377 }
378
379 $file = null;
380 if ($advanceCursor) {
381 $file = array_shift($files);
382 $this->currentIndex++;
383 }
384 else if (count($files) > 0) {
385 $file = $files[0];
386 }
387
388 if ($file === null) {
389 return null;
390 }
391 return ABSPATH . $file;
392 }
393 private function _checkForTimeout($path, $indexQueue = false) {
394 $file = substr($path, $this->striplen);
395 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.
396 if ($indexQueue !== false) {
397 $this->_serviceIndexQueue($indexQueue, true);
398 $this->stoppedOnFile = $file;
399 wordfence::status(4, 'info', "Forking during indexing: " . $path);
400 }
401 else {
402 wordfence::status(4, 'info', "Calling fork() from wordfenceHash with maxExecTime: " . $this->engine->maxExecTime);
403 }
404 $this->engine->fork();
405 //exits
406 }
407
408 if ($this->stoppedOnFile && $file != $this->stoppedOnFile && $indexQueue !== false) {
409 return;
410 }
411 else if ($this->stoppedOnFile && $file == $this->stoppedOnFile) {
412 $this->stoppedOnFile = false; //Continue indexing
413 }
414 }
415 private function _shouldProcessPath($path) {
416 $file = substr($path, $this->striplen);
417 $excludePatterns = wordfenceScanner::getExcludeFilePattern(wordfenceScanner::EXCLUSION_PATTERNS_USER);
418 if ($excludePatterns) {
419 foreach ($excludePatterns as $pattern) {
420 if (preg_match($pattern, $file)) {
421 return false;
422 }
423 }
424 }
425
426 $realPath = realpath($path);
427 if (isset($this->foldersProcessed[$realPath])) {
428 return false;
429 }
430
431 return true;
432 }
433 private function processFile($realFile) {
434 $file = substr($realFile, $this->striplen);
435
436 if (wfUtils::fileTooBig($realFile)) {
437 wordfence::status(4, 'info', "Skipping file larger than max size: $realFile");
438 return;
439 }
440
441 if (function_exists('memory_get_usage')) {
442 wordfence::status(4, 'info', "Scanning: $realFile (Mem:" . sprintf('%.1f', memory_get_usage(true) / (1024 * 1024)) . "M)");
443 }
444 else {
445 wordfence::status(4, 'info', "Scanning: $realFile");
446 }
447
448 wfUtils::beginProcessingFile($file);
449 $wfHash = self::hashFile($realFile);
450 $this->engine->scanController()->incrementSummaryItem(wfScanner::SUMMARY_SCANNED_FILES);
451 if ($wfHash) {
452 $md5 = strtoupper($wfHash[0]);
453 $shac = strtoupper($wfHash[1]);
454 $knownFile = 0;
455 if($this->malwareEnabled && $this->isMalwarePrefix($md5)){
456 $this->possibleMalware[] = array($file, $md5);
457 }
458
459 $knownFileExclude = wordfenceScanner::getExcludeFilePattern(wordfenceScanner::EXCLUSION_PATTERNS_KNOWN_FILES);
460 $allowKnownFileScan = true;
461 if ($knownFileExclude) {
462 foreach ($knownFileExclude as $pattern) {
463 if (preg_match($pattern, $realFile)) {
464 $allowKnownFileScan = false;
465 }
466 }
467 }
468
469 if ($allowKnownFileScan) {
470 if (isset($this->knownFiles['core'][$file])) {
471 if (strtoupper($this->knownFiles['core'][$file]) == $shac) {
472 $knownFile = 1;
473 }
474 else {
475 if ($this->coreEnabled) {
476 $localFile = ABSPATH . '/' . preg_replace('/^[\.\/]+/', '', $file);
477 $fileContents = @file_get_contents($localFile);
478 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))) {
479 $this->engine->addPendingIssue(
480 'knownfile',
481 wfIssues::SEVERITY_HIGH,
482 'coreModified' . $file,
483 'coreModified' . $file . $md5,
484 'WordPress core file modified: ' . $file,
485 "This WordPress core file has been modified and differs from the original file distributed with this version of WordPress.",
486 array(
487 'file' => $file,
488 'cType' => 'core',
489 'canDiff' => true,
490 'canFix' => true,
491 'canDelete' => false,
492 'haveIssues' => 'core'
493 )
494 );
495 }
496 }
497 }
498 }
499 else if (isset($this->knownFiles['plugins'][$file])) {
500 if (in_array($shac, $this->knownFiles['plugins'][$file])) {
501 $knownFile = 1;
502 }
503 else {
504 if ($this->pluginsEnabled) {
505 $options = $this->engine->scanController()->scanOptions();
506 $shouldGenerateIssue = true;
507 if (!$options['scansEnabled_highSense'] && preg_match('~/readme\.(?:txt|md)$~i', $file)) { //Don't generate issues for changed readme files unless high sensitivity is on
508 $shouldGenerateIssue = false;
509 }
510
511 if ($shouldGenerateIssue) {
512 $itemName = $this->knownFiles['plugins'][$file][0];
513 $itemVersion = $this->knownFiles['plugins'][$file][1];
514 $cKey = $this->knownFiles['plugins'][$file][2];
515 $this->engine->addPendingIssue(
516 'knownfile',
517 wfIssues::SEVERITY_MEDIUM,
518 'modifiedplugin' . $file,
519 'modifiedplugin' . $file . $md5,
520 'Modified plugin file: ' . $file,
521 "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]",
522 array(
523 'file' => $file,
524 'cType' => 'plugin',
525 'canDiff' => true,
526 'canFix' => true,
527 'canDelete' => false,
528 'cName' => $itemName,
529 'cVersion' => $itemVersion,
530 'cKey' => $cKey,
531 'haveIssues' => 'plugins'
532 )
533 );
534 }
535 }
536
537 }
538 }
539 else if (isset($this->knownFiles['themes'][$file])) {
540 if (in_array($shac, $this->knownFiles['themes'][$file])) {
541 $knownFile = 1;
542 }
543 else {
544 if ($this->themesEnabled) {
545 $options = $this->engine->scanController()->scanOptions();
546 $shouldGenerateIssue = true;
547 if (!$options['scansEnabled_highSense'] && preg_match('~/readme\.(?:txt|md)$~i', $file)) { //Don't generate issues for changed readme files unless high sensitivity is on
548 $shouldGenerateIssue = false;
549 }
550
551 if ($shouldGenerateIssue) {
552 $itemName = $this->knownFiles['themes'][$file][0];
553 $itemVersion = $this->knownFiles['themes'][$file][1];
554 $cKey = $this->knownFiles['themes'][$file][2];
555 $this->engine->addPendingIssue(
556 'knownfile',
557 wfIssues::SEVERITY_MEDIUM,
558 'modifiedtheme' . $file,
559 'modifiedtheme' . $file . $md5,
560 'Modified theme file: ' . $file,
561 "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.",
562 array(
563 'file' => $file,
564 'cType' => 'theme',
565 'canDiff' => true,
566 'canFix' => true,
567 'canDelete' => false,
568 'cName' => $itemName,
569 'cVersion' => $itemVersion,
570 'cKey' => $cKey,
571 'haveIssues' => 'themes'
572 )
573 );
574 }
575 }
576
577 }
578 }
579 else if ($this->coreUnknownEnabled && !$this->alertedOnUnknownWordPressVersion) { //Check for unknown files in system directories
580 $restrictedWordPressFolders = array(ABSPATH . 'wp-admin/', ABSPATH . WPINC . '/');
581 $added = false;
582 foreach ($restrictedWordPressFolders as $path) {
583 if (strpos($realFile, $path) === 0) {
584 if ($this->isPreviousCoreFile($shac)) {
585 $added = $this->engine->addIssue(
586 'knownfile',
587 wfIssues::SEVERITY_LOW,
588 'coreUnknown' . $file,
589 'coreUnknown' . $file . $md5,
590 sprintf(__('Old WordPress core file not removed during update: %s', 'wordfence'), $file),
591 __('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'),
592 array(
593 'file' => $file,
594 'cType' => 'core',
595 'canDiff' => false,
596 'canFix' => false,
597 'canDelete' => true,
598 )
599 );
600 }
601 else {
602 $added = $this->engine->addIssue(
603 'knownfile',
604 wfIssues::SEVERITY_HIGH,
605 'coreUnknown' . $file,
606 'coreUnknown' . $file . $md5,
607 'Unknown file in WordPress core: ' . $file,
608 "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.",
609 array(
610 'file' => $file,
611 'cType' => 'core',
612 'canDiff' => false,
613 'canFix' => false,
614 'canDelete' => true,
615 )
616 );
617 }
618 }
619 }
620
621 if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $this->haveIssues['coreUnknown'] = wfIssues::STATUS_PROBLEM; }
622 else if ($this->haveIssues['coreUnknown'] != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $this->haveIssues['coreUnknown'] = wfIssues::STATUS_IGNORED; }
623 }
624 }
625 // knownFile means that the file is both part of core or a known plugin or theme AND that we recognize the file's hash.
626 // we could split this into files whose path we recognize and file's whose path we recognize AND who have a valid sig.
627 // 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.
628 $fileModsTable = wfDB::networkTable('wfFileMods');
629 $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);
630
631 $this->totalFiles++;
632 $this->totalData += @filesize($realFile); //We already checked if file overflows int in the fileTooBig routine above
633 if($this->totalFiles % 100 === 0){
634 wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data so far");
635 }
636 } else {
637 //wordfence::status(2, 'error', "Could not gen hash for file (probably because we don't have permission to access the file): $realFile");
638 }
639 wfUtils::endProcessingFile();
640 }
641 private function _processPendingIssues() {
642 $fileModsTable = wfDB::networkTable('wfFileMods');
643
644 $count = $this->engine->getPendingIssueCount();
645 $offset = 0;
646 while ($offset < $count) {
647 $issues = $this->engine->getPendingIssues($offset);
648 if (count($issues) == 0) {
649 break;
650 }
651
652 //Do a bulk check of is_safe_file
653 $hashesToCheck = array();
654 foreach ($issues as &$i) {
655 $shac = $this->db->querySingle("SELECT HEX(SHAC) FROM {$fileModsTable} WHERE filename = '%s' AND isSafeFile = '?'", $i['data']['file']);
656 $shac = strtoupper($shac);
657 $i['shac'] = null;
658 if ($shac !== null) {
659 $shac = strtoupper($shac);
660 $i['shac'] = $shac;
661 $hashesToCheck[] = $shac;
662 }
663 }
664
665 $safeFiles = array();
666 if (count($hashesToCheck) > 0) {
667 $safeFiles = $this->isSafeFile($hashesToCheck);
668 }
669
670 //Migrate non-safe file issues to official issues
671 foreach ($issues as &$i) {
672 if (!in_array($i['shac'], $safeFiles)) {
673 $haveIssuesType = $i['data']['haveIssues'];
674 $added = $this->engine->addIssue(
675 $i['type'],
676 $i['severity'],
677 $i['ignoreP'],
678 $i['ignoreC'],
679 $i['shortMsg'],
680 $i['longMsg'],
681 $i['data'],
682 true //Prevent ignoreP and ignoreC from being hashed again
683 );
684 if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $this->haveIssues[$haveIssuesType] = wfIssues::STATUS_PROBLEM; }
685 else if ($this->haveIssues[$haveIssuesType] != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $this->haveIssues[$haveIssuesType] = wfIssues::STATUS_IGNORED; }
686 $this->db->queryWrite("UPDATE {$fileModsTable} SET isSafeFile = '0' WHERE SHAC = UNHEX('%s')", $i['shac']);
687 }
688 else {
689 $this->db->queryWrite("UPDATE {$fileModsTable} SET isSafeFile = '1' WHERE SHAC = UNHEX('%s')", $i['shac']);
690 }
691 }
692
693 $offset += count($issues);
694 $this->engine->checkForKill();
695 }
696 }
697 public static function hashFile($file) {
698 $fp = @fopen($file, "rb");
699 if (!$fp) {
700 return false;
701 }
702 $md5Context = hash_init('md5');
703 $sha256Context = hash_init('sha256');
704 while (!feof($fp)) {
705 $data = fread($fp, 65536);
706 if ($data === false) {
707 return false;
708 }
709 hash_update($md5Context, $data);
710 hash_update($sha256Context, str_replace(array("\n","\r","\t"," "),"", $data));
711 }
712 $md5 = hash_final($md5Context, false);
713 $shac = hash_final($sha256Context, false);
714 return array($md5, $shac);
715 }
716 private function _shouldHashFile($fullPath) {
717 $file = substr($fullPath, $this->striplen);
718
719 //Core File, return true
720 if ((isset($this->knownFiles['core']) && isset($this->knownFiles['core'][$file])) ||
721 (isset($this->knownFiles['plugins']) && isset($this->knownFiles['plugins'][$file])) ||
722 (isset($this->knownFiles['themes']) && isset($this->knownFiles['themes'][$file]))) {
723 return true;
724 }
725
726 //Excluded file, return false
727 $excludePatterns = wordfenceScanner::getExcludeFilePattern(wordfenceScanner::EXCLUSION_PATTERNS_USER | wordfenceScanner::EXCLUSION_PATTERNS_MALWARE);
728 if ($excludePatterns) {
729 foreach ($excludePatterns as $pattern) {
730 if (preg_match($pattern, $file)) {
731 return false;
732 }
733 }
734 }
735
736 //Unknown file in a core location
737 if ($this->coreUnknownEnabled && !$this->alertedOnUnknownWordPressVersion) {
738 $restrictedWordPressFolders = array(ABSPATH . 'wp-admin/', ABSPATH . WPINC . '/');
739 foreach ($restrictedWordPressFolders as $path) {
740 if (strpos($fullPath, $path) === 0) {
741 return true;
742 }
743 }
744 }
745
746 //Determine treatment
747 $fileExt = '';
748 if (preg_match('/\.([a-zA-Z\d\-]{1,7})$/', $file, $matches)) {
749 $fileExt = strtolower($matches[1]);
750 }
751 $isPHP = false;
752 if (preg_match('/\.(?:php(?:\d+)?|phtml)(\.|$)/i', $file)) {
753 $isPHP = true;
754 }
755 $isHTML = false;
756 if (preg_match('/\.(?:html?)(\.|$)/i', $file)) {
757 $isHTML = true;
758 }
759 $isJS = false;
760 if (preg_match('/\.(?:js|svg)(\.|$)/i', $file)) {
761 $isJS = true;
762 }
763
764 $options = $this->engine->scanController()->scanOptions();
765
766 //If scan images is disabled, only allow .js through
767 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)) {
768 if (!$options['scansEnabled_scanImages'] && !$isJS) {
769 return false;
770 }
771 }
772
773 //If high sensitivity is disabled, don't allow .sql
774 if (strtolower($fileExt) == 'sql') {
775 if (!$options['scansEnabled_highSense']) {
776 return false;
777 }
778 }
779
780 //Treating as binary, return true
781 $treatAsBinary = ($isPHP || $isHTML || $options['scansEnabled_scanImages']);
782 if ($treatAsBinary) {
783 return true;
784 }
785
786 //Will be malware scanned, return true
787 if ($isJS) {
788 return true;
789 }
790
791 return false;
792 }
793 private function isMalwarePrefix($hexMD5){
794 $hasPrefix = $this->_binaryListContains($this->malwareData, wfUtils::hex2bin($hexMD5), 4);
795 return $hasPrefix !== false;
796 }
797 private function isPreviousCoreFile($hexContentsSHAC) {
798 $hasPrefix = $this->_binaryListContains($this->coreHashesData, wfUtils::hex2bin($hexContentsSHAC), 32);
799 return $hasPrefix !== false;
800 }
801
802 /**
803 * @param $binaryList The binary list to search, sorted as a binary string.
804 * @param $needle The binary needle to search for.
805 * @param int $size The byte size of each item in the list.
806 * @return bool|int false if not found, otherwise the index in the list
807 */
808 private function _binaryListContains($binaryList, $needle, $size /* bytes */) {
809 $p = substr($needle, 0, $size);
810
811 $count = ceil(wfWAFUtils::strlen($binaryList) / $size);
812 $low = 0;
813 $high = $count - 1;
814
815 while ($low <= $high) {
816 $mid = (int) (($high + $low) / 2);
817 $val = wfWAFUtils::substr($binaryList, $mid * $size, $size);
818 $cmp = strcmp($val, $p);
819 if ($cmp < 0) {
820 $low = $mid + 1;
821 }
822 else if ($cmp > 0) {
823 $high = $mid - 1;
824 }
825 else {
826 return $mid;
827 }
828 }
829
830 return false;
831 }
832
833 /**
834 * Queries the is_safe_file endpoint. If provided an array, it does a bulk check and returns an array containing the
835 * hashes that were marked as safe. If provided a string, it returns a boolean to indicate the safeness of the file.
836 *
837 * @param string|array $shac
838 * @return array|bool
839 */
840 private function isSafeFile($shac) {
841 if (is_array($shac)) {
842 $result = $this->engine->api->call('is_safe_file', array(), array('multipleSHAC' => json_encode($shac)));
843 if (isset($result['isSafe'])) {
844 return $result['isSafe'];
845 }
846 return array();
847 }
848
849 $result = $this->engine->api->call('is_safe_file', array(), array('shac' => strtoupper($shac)));
850 if(isset($result['isSafe']) && $result['isSafe'] == 1){
851 return true;
852 }
853 return false;
854 }
855 }
856