PluginProbe ʕ •ᴥ•ʔ
Wordfence Security – Firewall, Malware Scan, and Login Security / 7.3.2
Wordfence Security – Firewall, Malware Scan, and Login Security v7.3.2
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 / wordfenceScanner.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 WFLSPHP52Compatability.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 7 years ago menu_tools_whois.php 8 years ago menu_wordfence_central.php 7 years ago noc1.key 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 7 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 wfModuleController.php 7 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 7 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
wordfenceScanner.php
808 lines
1 <?php
2 require_once('wordfenceConstants.php');
3 require_once('wordfenceClass.php');
4 require_once('wordfenceURLHoover.php');
5 class wordfenceScanner {
6 /*
7 * Mask to return all patterns in the exclusion list.
8 * @var int
9 */
10 const EXCLUSION_PATTERNS_ALL = PHP_INT_MAX;
11 /*
12 * Mask for patterns that the user has added.
13 */
14 const EXCLUSION_PATTERNS_USER = 0x1;
15 /*
16 * Mask for patterns that should be excluded from the known files scan.
17 */
18 const EXCLUSION_PATTERNS_KNOWN_FILES = 0x2;
19 /*
20 * Mask for patterns that should be excluded from the malware scan.
21 */
22 const EXCLUSION_PATTERNS_MALWARE = 0x4;
23
24 //serialized:
25 protected $path = '';
26 protected $results = array();
27 public $errorMsg = false;
28 protected $apiKey = false;
29 protected $wordpressVersion = '';
30 protected $totalFilesScanned = 0;
31 protected $startTime = false;
32 protected $lastStatusTime = false;
33 protected $patterns = "";
34 protected $api = false;
35 protected static $excludePatterns = array();
36 protected static $builtinExclusions = array(
37 array('pattern' => 'wp\-includes\/version\.php', 'include' => self::EXCLUSION_PATTERNS_KNOWN_FILES), //Excluded from the known files scan because non-en_US installations will have extra content that fails the check, still in malware scan
38 array('pattern' => '(?:wp\-includes|wp\-admin)\/(?:[^\/]+\/+)*(?:\.htaccess|\.htpasswd|php_errorlog|error_log|[^\/]+?\.log|\._|\.DS_Store|\.listing|dwsync\.xml)', 'include' => self::EXCLUSION_PATTERNS_KNOWN_FILES),
39 );
40 /** @var wfScanEngine */
41 protected $scanEngine;
42
43 public function __sleep(){
44 return array('path', 'results', 'errorMsg', 'apiKey', 'wordpressVersion', 'urlHoover', 'totalFilesScanned',
45 'startTime', 'lastStatusTime', 'patterns', 'scanEngine');
46 }
47 public function __wakeup(){
48 }
49 public function __construct($apiKey, $wordpressVersion, $path, $scanEngine) {
50 $this->apiKey = $apiKey;
51 $this->wordpressVersion = $wordpressVersion;
52 $this->api = new wfAPI($this->apiKey, $this->wordpressVersion);
53 if($path[strlen($path) - 1] != '/'){
54 $path .= '/';
55 }
56 $this->path = $path;
57 $this->scanEngine = $scanEngine;
58
59
60 $this->results = array();
61 $this->errorMsg = false;
62 //First extract hosts or IPs and their URLs into $this->hostsFound and URL's into $this->urlsFound
63 $options = $this->scanEngine->scanController()->scanOptions();
64 if ($options['scansEnabled_fileContentsGSB']) {
65 $this->urlHoover = new wordfenceURLHoover($this->apiKey, $this->wordpressVersion);
66 }
67 else {
68 $this->urlHoover = false;
69 }
70
71 if ($options['scansEnabled_fileContents']) {
72 $this->setupSigs();
73 }
74 else {
75 $this->patterns = array();
76 }
77 }
78
79 /**
80 * Get scan regexes from noc1 and add any user defined regexes, including descriptions, ID's and time added.
81 * @todo add caching to this.
82 * @throws Exception
83 */
84 protected function setupSigs() {
85 $sigData = $this->api->call('get_patterns', array(), array());
86 if(! (is_array($sigData) && isset($sigData['rules'])) ){
87 throw new Exception(__('Wordfence could not get the attack signature patterns from the scanning server.', 'wordfence'));
88 }
89
90 if (wfWAF::getInstance() && method_exists(wfWAF::getInstance(), 'setMalwareSignatures')) {
91 try { wfWAF::getInstance()->setMalwareSignatures(array()); } catch (Exception $e) { /* Ignore */ }
92 if (method_exists(wfWAF::getInstance(), 'setMalwareSignatureCommonStrings')) {
93 try {
94 wfWAF::getInstance()->setMalwareSignatureCommonStrings(array(), array());
95 }
96 catch (Exception $e) { /* Ignore */ }
97 }
98 }
99
100 if (is_array($sigData['rules'])) {
101 $wafPatterns = array();
102 $wafCommonStringIndexes = array();
103 foreach ($sigData['rules'] as $key => $signatureRow) {
104 list(, , $pattern) = $signatureRow;
105 if (empty($pattern)) {
106 throw new Exception(__('Wordfence received malformed attack signature patterns from the scanning server.', 'wordfence'));
107 }
108
109 $logOnly = (isset($signatureRow[5]) && !empty($signatureRow[5])) ? $signatureRow[5] : false;
110 $commonStringIndexes = (isset($signatureRow[8]) && is_array($signatureRow[8])) ? $signatureRow[8] : array();
111 if (@preg_match('/' . $pattern . '/iS', null) === false) {
112 wordfence::status(1, 'error', __('A regex Wordfence received from its servers is invalid. The pattern is: ', 'wordfence') . esc_html($pattern));
113 unset($sigData['rules'][$key]);
114 }
115 else if (!$logOnly) {
116 $wafPatterns[] = $pattern;
117 $wafCommonStringIndexes[] = $commonStringIndexes;
118 }
119 }
120
121 if (wfWAF::getInstance() && method_exists(wfWAF::getInstance(), 'setMalwareSignatures')) {
122 try { wfWAF::getInstance()->setMalwareSignatures($wafPatterns); } catch (Exception $e) { /* Ignore */ }
123 if (method_exists(wfWAF::getInstance(), 'setMalwareSignatureCommonStrings') && isset($sigData['commonStrings']) && is_array($sigData['commonStrings'])) {
124 try {
125 wfWAF::getInstance()->setMalwareSignatureCommonStrings($sigData['commonStrings'], $wafCommonStringIndexes);
126 }
127 catch (Exception $e) { /* Ignore */ }
128 }
129 }
130 }
131
132 $userSignatures = wfScanner::shared()->userScanSignatures();
133 foreach ($userSignatures as $s) {
134 $sigData['rules'][] = $s;
135 }
136
137 $this->patterns = $sigData;
138 if (isset($this->patterns['signatureUpdateTime'])) {
139 wfConfig::set('signatureUpdateTime', $this->patterns['signatureUpdateTime']);
140 }
141 }
142
143 /**
144 * Return regular expression to exclude files or false if
145 * there is no pattern
146 *
147 * @param $whichPatterns int Bitmask indicating which patterns to include.
148 * @return array|boolean
149 */
150 public static function getExcludeFilePattern($whichPatterns = self::EXCLUSION_PATTERNS_USER) {
151 if (isset(self::$excludePatterns[$whichPatterns])) {
152 return self::$excludePatterns[$whichPatterns];
153 }
154
155 $exParts = array();
156 if (($whichPatterns & self::EXCLUSION_PATTERNS_USER) > 0)
157 {
158 $exParts = wfScanner::shared()->userExclusions();
159 }
160
161 $exParts = array_filter($exParts);
162 foreach ($exParts as &$exPart) {
163 $exPart = preg_quote(trim($exPart), '/');
164 $exPart = preg_replace('/\\\\\*/', '.*', $exPart);
165 }
166
167 foreach (self::$builtinExclusions as $pattern) {
168 if (($pattern['include'] & $whichPatterns) > 0) {
169 $exParts[] = $pattern['pattern'];
170 }
171 }
172
173 $exParts = array_filter($exParts);
174 if (!empty($exParts)) {
175 $chunks = array_chunk($exParts, 100);
176 self::$excludePatterns[$whichPatterns] = array();
177 foreach ($chunks as $parts) {
178 self::$excludePatterns[$whichPatterns][] = '/(?:' . implode('|', $parts) . ')$/i';
179 }
180 }
181 else {
182 self::$excludePatterns[$whichPatterns] = false;
183 }
184
185 return self::$excludePatterns[$whichPatterns];
186 }
187
188 /**
189 * @param wfScanEngine $forkObj
190 * @return array
191 */
192 public function scan($forkObj){
193 $this->scanEngine = $forkObj;
194 $loader = $this->scanEngine->getKnownFilesLoader();
195 if(! $this->startTime){
196 $this->startTime = microtime(true);
197 }
198 if(! $this->lastStatusTime){
199 $this->lastStatusTime = microtime(true);
200 }
201
202 //The site's own URL is checked in an earlier scan stage so we exclude it here.
203 $options = $this->scanEngine->scanController()->scanOptions();
204 $hooverExclusions = array();
205 if ($options['scansEnabled_fileContentsGSB']) {
206 $hooverExclusions = wordfenceURLHoover::standardExcludedHosts();
207 }
208
209 $backtrackLimit = ini_get('pcre.backtrack_limit');
210 if (is_numeric($backtrackLimit)) {
211 $backtrackLimit = (int) $backtrackLimit;
212 if ($backtrackLimit > 10000000) {
213 ini_set('pcre.backtrack_limit', 1000000);
214 wordfence::status(4, 'info', sprintf(__('Backtrack limit is %d, reducing to 1000000', 'wordfence'), $backtrackLimit));
215 }
216 }
217 else {
218 $backtrackLimit = false;
219 }
220
221 $lastCount = 'whatever';
222 $excludePatterns = self::getExcludeFilePattern(self::EXCLUSION_PATTERNS_USER | self::EXCLUSION_PATTERNS_MALWARE);
223 while (true) {
224 $thisCount = wordfenceMalwareScanFile::countRemaining();
225 if ($thisCount == $lastCount) {
226 //count should always be decreasing. If not, we're in an infinite loop so lets catch it early
227 wordfence::status(4, 'info', __('Detected loop in malware scan, aborting.', 'wordfence'));
228 break;
229 }
230 $lastCount = $thisCount;
231
232 $files = wordfenceMalwareScanFile::files();
233 if (count($files) < 1) {
234 wordfence::status(4, 'info', __('No files remaining for malware scan.', 'wordfence'));
235 break;
236 }
237
238 foreach ($files as $record) {
239 $file = $record->filename;
240 if ($excludePatterns) {
241 foreach ($excludePatterns as $pattern) {
242 if (preg_match($pattern, $file)) {
243 $record->markComplete();
244 continue 2;
245 }
246 }
247 }
248 if (!file_exists($this->path . $file)) {
249 $record->markComplete();
250 continue;
251 }
252 $fileSum = $record->newMD5;
253
254 $fileExt = '';
255 if(preg_match('/\.([a-zA-Z\d\-]{1,7})$/', $file, $matches)){
256 $fileExt = strtolower($matches[1]);
257 }
258 $isPHP = false;
259 if(preg_match('/\.(?:php(?:\d+)?|phtml)(\.|$)/i', $file)) {
260 $isPHP = true;
261 }
262 $isHTML = false;
263 if(preg_match('/\.(?:html?)(\.|$)/i', $file)) {
264 $isHTML = true;
265 }
266 $isJS = false;
267 if(preg_match('/\.(?:js|svg)(\.|$)/i', $file)) {
268 $isJS = true;
269 }
270 $dontScanForURLs = false;
271 if (!$options['scansEnabled_highSense'] && (preg_match('/^(?:\.htaccess|wp\-config\.php)$/', $file) || $file === ini_get('user_ini.filename'))) {
272 $dontScanForURLs = true;
273 }
274
275 $isScanImagesFile = false;
276 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)) {
277 if ($options['scansEnabled_scanImages']) {
278 $isScanImagesFile = true;
279 }
280 else if (!$isJS) {
281 $record->markComplete();
282 continue;
283 }
284 }
285 $isHighSensitivityFile = false;
286 if (strtolower($fileExt) == 'sql') {
287 if ($options['scansEnabled_highSense']) {
288 $isHighSensitivityFile = true;
289 }
290 else {
291 $record->markComplete();
292 continue;
293 }
294 }
295 if(wfUtils::fileTooBig($this->path . $file)){ //We can't use filesize on 32 bit systems for files > 2 gigs
296 //We should not need this check because files > 2 gigs are not hashed and therefore won't be received back as unknowns from the API server
297 //But we do it anyway to be safe.
298 wordfence::status(2, 'error', sprintf(__('Encountered file that is too large: %s - Skipping.', 'wordfence'), $file));
299 $record->markComplete();
300 continue;
301 }
302 wfUtils::beginProcessingFile($file);
303
304 $fsize = @filesize($this->path . $file); //Checked if too big above
305 $fsize = wfUtils::formatBytes($fsize);
306 if (function_exists('memory_get_usage')) {
307 wordfence::status(4, 'info', sprintf(__('Scanning contents: %s (Size: %s Mem: %s)', 'wordfence'), $file, $fsize, wfUtils::formatBytes(memory_get_usage(true))));
308 } else {
309 wordfence::status(4, 'info', sprintf(__('Scanning contents: %s (Size: %s)', 'wordfence'), $file, $fsize));
310 }
311
312 $stime = microtime(true);
313 $fh = @fopen($this->path . $file, 'r');
314 if (!$fh) {
315 $record->markComplete();
316 continue;
317 }
318 $totalRead = $record->stoppedOnPosition;
319 if ($totalRead > 0) {
320 if (@fseek($fh, $totalRead, SEEK_SET) !== 0) {
321 $totalRead = 0;
322 }
323 }
324
325 $dataForFile = $this->dataForFile($file);
326
327 while (!feof($fh)) {
328 $data = fread($fh, 1 * 1024 * 1024); //read 1 megs max per chunk
329 $readSize = wfUtils::strlen($data);
330 $currentPosition = $totalRead;
331 $totalRead += $readSize;
332 if ($readSize < 1) {
333 break;
334 }
335
336 $extraMsg = '';
337 if ($isScanImagesFile) {
338 $extraMsg = ' ' . __('This file was detected because you have enabled "Scan images, binary, and other files as if they were executable", which treats non-PHP files as if they were PHP code. This option is more aggressive than the usual scans, and may cause false positives.', 'wordfence');
339 }
340 else if ($isHighSensitivityFile) {
341 $extraMsg = ' ' . __('This file was detected because you have enabled HIGH SENSITIVITY scanning. This option is more aggressive than the usual scans, and may cause false positives.', 'wordfence');
342 }
343
344 $treatAsBinary = ($isPHP || $isHTML || $options['scansEnabled_scanImages']);
345 if ($options['scansEnabled_fileContents']) {
346 if ($treatAsBinary && wfUtils::strpos($data, '$allowed'.'Sites') !== false && wfUtils::strpos($data, "define ('VER"."SION', '1.") !== false && wfUtils::strpos($data, "TimThum"."b script created by") !== false) {
347 $this->addResult(array(
348 'type' => 'file',
349 'severity' => wfIssues::SEVERITY_CRITICAL,
350 'ignoreP' => $this->path . $file,
351 'ignoreC' => $fileSum,
352 'shortMsg' => __('File is an old version of TimThumb which is vulnerable.', 'wordfence'),
353 'longMsg' => __('This file appears to be an old version of the TimThumb script which makes your system vulnerable to attackers. Please upgrade the theme or plugin that uses this or remove it.', 'wordfence') . $extraMsg,
354 'data' => array_merge(array(
355 'file' => $file,
356 'shac' => $record->SHAC,
357 'highSense' => $options['scansEnabled_highSense'],
358 'betaSigs' => wfConfig::get('betaThreatDefenseFeed'),
359 ), $dataForFile),
360 ));
361 break;
362 }
363 else {
364 $allCommonStrings = $this->patterns['commonStrings'];
365 $commonStringsFound = array_fill(0, count($allCommonStrings), null); //Lazily looked up below
366
367 $regexMatched = false;
368 foreach ($this->patterns['rules'] as $rule) {
369 $stoppedOnSignature = $record->stoppedOnSignature;
370 if (!empty($stoppedOnSignature)) { //Advance until we find the rule we stopped on last time
371 //wordfence::status(4, 'info', "Searching for malware scan resume point (". $stoppedOnSignature . ") at rule " . $rule[0]);
372 if ($stoppedOnSignature == $rule[0]) {
373 $record->updateStoppedOn('', $currentPosition);
374 wordfence::status(4, 'info', sprintf(__('Resuming malware scan at rule %s.', 'wordfence'), $rule[0]));
375 }
376 continue;
377 }
378
379 $type = (isset($rule[4]) && !empty($rule[4])) ? $rule[4] : 'server';
380 $logOnly = (isset($rule[5]) && !empty($rule[5])) ? $rule[5] : false;
381 $commonStringIndexes = (isset($rule[8]) && is_array($rule[8])) ? $rule[8] : array();
382 $customMessage = isset($rule[9]) ? $rule[9] : __('This file appears to be installed or modified by a hacker to perform malicious activity. If you know about this file you can choose to ignore it to exclude it from future scans.', 'wordfence');
383 if ($type == 'server' && !$treatAsBinary) { continue; }
384 else if (($type == 'both' || $type == 'browser') && $isJS) { $extraMsg = ''; }
385 else if (($type == 'both' || $type == 'browser') && !$treatAsBinary) { continue; }
386
387 foreach ($commonStringIndexes as $i) {
388 if ($commonStringsFound[$i] === null) {
389 $s = $allCommonStrings[$i];
390 $commonStringsFound[$i] = (preg_match('/' . $s . '/i', $data) == 1);
391 }
392
393 if (!$commonStringsFound[$i]) {
394 //wordfence::status(4, 'info', "Skipping malware signature ({$rule[0]}) due to short circuit.");
395 continue 2;
396 }
397 }
398
399 /*if (count($commonStringIndexes) > 0) {
400 wordfence::status(4, 'info', "Processing malware signature ({$rule[0]}) because short circuit matched.");
401 }*/
402
403 if (preg_match('/(' . $rule[2] . ')/iS', $data, $matches, PREG_OFFSET_CAPTURE)) {
404 $matchString = $matches[1][0];
405 $matchOffset = $matches[1][1];
406 $beforeString = wfWAFUtils::substr($data, max(0, $matchOffset - 100), $matchOffset - max(0, $matchOffset - 100));
407 $afterString = wfWAFUtils::substr($data, $matchOffset + strlen($matchString), 100);
408 if (!$logOnly) {
409 $this->addResult(array(
410 'type' => 'file',
411 'severity' => wfIssues::SEVERITY_CRITICAL,
412 'ignoreP' => $this->path . $file,
413 'ignoreC' => $fileSum,
414 'shortMsg' => __('File appears to be malicious: ', 'wordfence') . esc_html($file),
415 'longMsg' => $customMessage . ' ' . __('The matched text in this file is:', 'wordfence') . ' ' . '<strong style="color: #F00;" class="wf-split-word">' . wfUtils::potentialBinaryStringToHTML((wfUtils::strlen($matchString) > 200 ? wfUtils::substr($matchString, 0, 200) . '...' : $matchString)) . '</strong>' . '<br><br>' . sprintf(__('The issue type is: <strong>%s</strong>', 'wordfence'), esc_html($rule[7])) . '<br>' . sprintf(__('Description: <strong>%s</strong>', 'wordfence'), esc_html($rule[3])) . $extraMsg,
416 'data' => array_merge(array(
417 'file' => $file,
418 'shac' => $record->SHAC,
419 'highSense' => $options['scansEnabled_highSense'],
420 'betaSigs' => wfConfig::get('betaThreatDefenseFeed'),
421 ), $dataForFile),
422 ));
423 }
424 $regexMatched = true;
425 $this->scanEngine->recordMetric('malwareSignature', $rule[0], array('file' => $file, 'match' => $matchString, 'before' => $beforeString, 'after' => $afterString, 'md5' => $record->newMD5, 'shac' => $record->SHAC), false);
426 break;
427 }
428
429 if ($forkObj->shouldFork()) {
430 $record->updateStoppedOn($rule[0], $currentPosition);
431 fclose($fh);
432
433 wordfence::status(4, 'info', sprintf(__('Forking during malware scan (%s) to ensure continuity.', 'wordfence'), $rule[0]));
434 $forkObj->fork(); //exits
435 }
436 }
437 if ($regexMatched) { break; }
438 }
439 if ($treatAsBinary && $options['scansEnabled_highSense']) {
440 $badStringFound = false;
441 if (strpos($data, $this->patterns['badstrings'][0]) !== false) {
442 for ($i = 1; $i < sizeof($this->patterns['badstrings']); $i++) {
443 if (wfUtils::strpos($data, $this->patterns['badstrings'][$i]) !== false) {
444 $badStringFound = $this->patterns['badstrings'][$i];
445 break;
446 }
447 }
448 }
449 if ($badStringFound) {
450 $this->addResult(array(
451 'type' => 'file',
452 'severity' => wfIssues::SEVERITY_CRITICAL,
453 'ignoreP' => $this->path . $file,
454 'ignoreC' => $fileSum,
455 'shortMsg' => __('This file may contain malicious executable code: ', 'wordfence') . esc_html($file),
456 'longMsg' => sprintf(__('This file is a PHP executable file and contains the word "eval" (without quotes) and the word "<span class="wf-split-word">%s</span>" (without quotes). The eval() function along with an encoding function like the one mentioned are commonly used by hackers to hide their code. If you know about this file you can choose to ignore it to exclude it from future scans. This file was detected because you have enabled HIGH SENSITIVITY scanning. This option is more aggressive than the usual scans, and may cause false positives.', 'wordfence'), esc_html($badStringFound)),
457 'data' => array_merge(array(
458 'file' => $file,
459 'shac' => $record->SHAC,
460 'highSense' => $options['scansEnabled_highSense'],
461 'betaSigs' => wfConfig::get('betaThreatDefenseFeed'),
462 ), $dataForFile),
463 ));
464 break;
465 }
466 }
467 }
468
469 if (!$dontScanForURLs && $options['scansEnabled_fileContentsGSB']) {
470 $found = $this->urlHoover->hoover($file, $data, $hooverExclusions);
471 $this->scanEngine->scanController()->incrementSummaryItem(wfScanner::SUMMARY_SCANNED_URLS, $found);
472 }
473
474 if ($totalRead > 2 * 1024 * 1024) {
475 break;
476 }
477 }
478 fclose($fh);
479 $this->totalFilesScanned++;
480 if(microtime(true) - $this->lastStatusTime > 1){
481 $this->lastStatusTime = microtime(true);
482 $this->writeScanningStatus();
483 }
484
485 $record->markComplete();
486 $forkObj->forkIfNeeded();
487 }
488 }
489 $this->writeScanningStatus();
490 if ($options['scansEnabled_fileContentsGSB']) {
491 wordfence::status(2, 'info', __('Asking Wordfence to check URLs against malware list.', 'wordfence'));
492 $hooverResults = $this->urlHoover->getBaddies();
493 if($this->urlHoover->errorMsg){
494 $this->errorMsg = $this->urlHoover->errorMsg;
495 if ($backtrackLimit !== false) { ini_set('pcre.backtrack_limit', $backtrackLimit); }
496 return false;
497 }
498 $this->urlHoover->cleanup();
499
500 foreach($hooverResults as $file => $hresults){
501 $record = wordfenceMalwareScanFile::fileForPath($file);
502 $dataForFile = $this->dataForFile($file, $this->path . $file);
503
504 foreach($hresults as $result){
505 if(preg_match('/wfBrowscapCache\.php$/', $file)){
506 continue;
507 }
508
509 if (empty($result['URL'])) {
510 continue;
511 }
512
513 if ($result['badList'] == 'goog-malware-shavar') {
514 $this->addResult(array(
515 'type' => 'file',
516 'severity' => wfIssues::SEVERITY_CRITICAL,
517 'ignoreP' => $this->path . $file,
518 'ignoreC' => md5_file($this->path . $file),
519 'shortMsg' => __('File contains suspected malware URL: ', 'wordfence') . esc_html($file),
520 'longMsg' => sprintf(__('This file contains a suspected malware URL listed on Google\'s list of malware sites. Wordfence decodes %s when scanning files so the URL may not be visible if you view this file. The URL is: %s - More info available at <a href="http://safebrowsing.clients.google.com/safebrowsing/diagnostic?site=%s&client=googlechrome&hl=en-US" target="_blank" rel="noopener noreferrer">Google Safe Browsing diagnostic page</a>.', 'wordfence'), esc_html($this->patterns['word3']), esc_html($result['URL']), urlencode($result['URL'])),
521 'data' => array_merge(array(
522 'file' => $file,
523 'shac' => $record->SHAC,
524 'badURL' => $result['URL'],
525 'gsb' => 'goog-malware-shavar',
526 'highSense' => $options['scansEnabled_highSense'],
527 'betaSigs' => wfConfig::get('betaThreatDefenseFeed'),
528 ), $dataForFile),
529 ));
530 }
531 else if ($result['badList'] == 'googpub-phish-shavar') {
532 $this->addResult(array(
533 'type' => 'file',
534 'severity' => wfIssues::SEVERITY_CRITICAL,
535 'ignoreP' => $this->path . $file,
536 'ignoreC' => md5_file($this->path . $file),
537 'shortMsg' => __('File contains suspected phishing URL: ', 'wordfence') . esc_html($file),
538 'longMsg' => __('This file contains a URL that is a suspected phishing site that is currently listed on Google\'s list of known phishing sites. The URL is: ', 'wordfence') . esc_html($result['URL']),
539 'data' => array_merge(array(
540 'file' => $file,
541 'shac' => $record->SHAC,
542 'badURL' => $result['URL'],
543 'gsb' => 'googpub-phish-shavar',
544 'highSense' => $options['scansEnabled_highSense'],
545 'betaSigs' => wfConfig::get('betaThreatDefenseFeed'),
546 ), $dataForFile),
547 ));
548 }
549 else if ($result['badList'] == 'wordfence-dbl') {
550 $this->addResult(array(
551 'type' => 'file',
552 'severity' => wfIssues::SEVERITY_CRITICAL,
553 'ignoreP' => $this->path . $file,
554 'ignoreC' => md5_file($this->path . $file),
555 'shortMsg' => __('File contains suspected malware URL: ', 'wordfence') . esc_html($file),
556 'longMsg' => __('This file contains a URL that is currently listed on Wordfence\'s domain blacklist. The URL is: ', 'wordfence') . esc_html($result['URL']),
557 'data' => array_merge(array(
558 'file' => $file,
559 'shac' => $record->SHAC,
560 'badURL' => $result['URL'],
561 'gsb' => 'wordfence-dbl',
562 'highSense' => $options['scansEnabled_highSense'],
563 'betaSigs' => wfConfig::get('betaThreatDefenseFeed'),
564 ), $dataForFile),
565 ));
566 }
567 }
568 }
569 }
570 wfUtils::endProcessingFile();
571
572 wordfence::status(4, 'info', __('Finalizing malware scan results', 'wordfence'));
573 $hashesToCheck = array();
574 foreach ($this->results as $r) {
575 $hashesToCheck[] = $r['data']['shac'];
576 }
577
578 if (count($hashesToCheck) > 0) {
579 $safeFiles = $this->isSafeFile($hashesToCheck);
580 foreach ($this->results as $index => $value) {
581 if (in_array($value['data']['shac'], $safeFiles)) {
582 unset($this->results[$index]);
583 }
584 }
585 }
586
587 if ($backtrackLimit !== false) { ini_set('pcre.backtrack_limit', $backtrackLimit); }
588 return $this->results;
589 }
590
591 protected function writeScanningStatus() {
592 wordfence::status(2, 'info', sprintf(__('Scanned contents of %d additional files at %.2f per second', 'wordfence'), $this->totalFilesScanned, ($this->totalFilesScanned / (microtime(true) - $this->startTime))));
593 }
594
595 protected function addResult($result) {
596 for ($i = 0; $i < sizeof($this->results); $i++) {
597 if ($this->results[$i]['type'] == 'file' && $this->results[$i]['data']['file'] == $result['data']['file']) {
598 if ($this->results[$i]['severity'] > $result['severity']) {
599 $this->results[$i] = $result; //Overwrite with more severe results
600 }
601 return;
602 }
603 }
604 //We don't have a results for this file so append
605 $this->results[] = $result;
606 }
607
608 /**
609 * Queries the is_safe_file endpoint. If provided an array, it does a bulk check and returns an array containing the
610 * hashes that were marked as safe. If provided a string, it returns a boolean to indicate the safeness of the file.
611 *
612 * @param string|array $shac
613 * @return array|bool
614 */
615 private function isSafeFile($shac) {
616 if(! $this->api){
617 $this->api = new wfAPI($this->apiKey, $this->wordpressVersion);
618 }
619
620 if (is_array($shac)) {
621 $result = $this->api->call('is_safe_file', array(), array('multipleSHAC' => json_encode($shac)));
622 if (isset($result['isSafe'])) {
623 return $result['isSafe'];
624 }
625 return array();
626 }
627
628 $result = $this->api->call('is_safe_file', array(), array('shac' => strtoupper($shac)));
629 if(isset($result['isSafe']) && $result['isSafe'] == 1){
630 return true;
631 }
632 return false;
633 }
634
635 /**
636 * @param string $file
637 * @return array
638 */
639 private function dataForFile($file, $fullPath = null) {
640 $loader = $this->scanEngine->getKnownFilesLoader();
641 $data = array();
642 if ($isKnownFile = $loader->isKnownFile($file)) {
643 if ($loader->isKnownCoreFile($file)) {
644 $data['cType'] = 'core';
645
646 } else if ($loader->isKnownPluginFile($file)) {
647 $data['cType'] = 'plugin';
648 list($itemName, $itemVersion, $cKey) = $loader->getKnownPluginData($file);
649 $data = array_merge($data, array(
650 'cName' => $itemName,
651 'cVersion' => $itemVersion,
652 'cKey' => $cKey
653 ));
654
655 } else if ($loader->isKnownThemeFile($file)) {
656 $data['cType'] = 'theme';
657 list($itemName, $itemVersion, $cKey) = $loader->getKnownThemeData($file);
658 $data = array_merge($data, array(
659 'cName' => $itemName,
660 'cVersion' => $itemVersion,
661 'cKey' => $cKey
662 ));
663 }
664 }
665
666 $suppressDelete = false;
667 $canRegenerate = false;
668 if ($fullPath !== null) {
669 $bootstrapPath = wordfence::getWAFBootstrapPath();
670 $htaccessPath = get_home_path() . '.htaccess';
671 $userIni = ini_get('user_ini.filename');
672 $userIniPath = false;
673 if ($userIni) {
674 $userIniPath = get_home_path() . $userIni;
675 }
676
677 if ($fullPath == $htaccessPath) {
678 $suppressDelete = true;
679 }
680 else if ($userIniPath !== false && $fullPath == $userIniPath) {
681 $suppressDelete = true;
682 }
683 else if ($fullPath == $bootstrapPath) {
684 $suppressDelete = true;
685 $canRegenerate = true;
686 }
687 }
688
689 $localFile = realpath($this->path . $file);
690 $isWPConfig = $localFile === ABSPATH . 'wp-config.php';
691
692 $data['canDiff'] = $isKnownFile;
693 $data['canFix'] = $isKnownFile && !$isWPConfig;
694 $data['canDelete'] = !$isKnownFile && !$canRegenerate && !$suppressDelete && !$isWPConfig;
695 $data['canRegenerate'] = $canRegenerate && !$isWPConfig;
696 $data['wpconfig'] = $isWPConfig;
697
698 return $data;
699 }
700 }
701
702 /**
703 * Convenience class for interfacing with the wfFileMods table.
704 *
705 * @property string $filename
706 * @property string $filenameMD5
707 * @property string $newMD5
708 * @property string $SHAC
709 * @property string $stoppedOnSignature
710 * @property string $stoppedOnPosition
711 * @property string $isSafeFile
712 */
713 class wordfenceMalwareScanFile {
714 protected $_filename;
715 protected $_filenameMD5;
716 protected $_newMD5;
717 protected $_shac;
718 protected $_stoppedOnSignature;
719 protected $_stoppedOnPosition;
720 protected $_isSafeFile;
721
722 protected static function getDB() {
723 static $db = null;
724 if ($db === null) {
725 $db = new wfDB();
726 }
727 return $db;
728 }
729
730 public static function countRemaining() {
731 $db = self::getDB();
732 return $db->querySingle("SELECT COUNT(*) FROM " . wfDB::networkTable('wfFileMods') . " WHERE oldMD5 != newMD5 AND knownFile = 0");
733 }
734
735 public static function files($limit = 500) {
736 $db = self::getDB();
737 $result = $db->querySelect("SELECT filename, filenameMD5, HEX(newMD5) AS newMD5, HEX(SHAC) AS SHAC, stoppedOnSignature, stoppedOnPosition, isSafeFile FROM " . wfDB::networkTable('wfFileMods') . " WHERE oldMD5 != newMD5 AND knownFile = 0 limit %d", $limit);
738 $files = array();
739 foreach ($result as $row) {
740 $files[] = new wordfenceMalwareScanFile($row['filename'], $row['filenameMD5'], $row['newMD5'], $row['SHAC'], $row['stoppedOnSignature'], $row['stoppedOnPosition'], $row['isSafeFile']);
741 }
742 return $files;
743 }
744
745 public static function fileForPath($file) {
746 $db = self::getDB();
747 $row = $db->querySingleRec("SELECT filename, filenameMD5, HEX(newMD5) AS newMD5, HEX(SHAC) AS SHAC, stoppedOnSignature, stoppedOnPosition, isSafeFile FROM " . wfDB::networkTable('wfFileMods') . " WHERE filename = '%s'", $file);
748 return new wordfenceMalwareScanFile($row['filename'], $row['filenameMD5'], $row['newMD5'], $row['SHAC'], $row['stoppedOnSignature'], $row['stoppedOnPosition'], $row['isSafeFile']);
749 }
750
751 public function __construct($filename, $filenameMD5, $newMD5, $shac, $stoppedOnSignature, $stoppedOnPosition, $isSafeFile) {
752 $this->_filename = $filename;
753 $this->_filenameMD5 = $filenameMD5;
754 $this->_newMD5 = $newMD5;
755 $this->_shac = strtoupper($shac);
756 $this->_stoppedOnSignature = $stoppedOnSignature;
757 $this->_stoppedOnPosition = $stoppedOnPosition;
758 $this->_isSafeFile = $isSafeFile;
759 }
760
761 public function __get($key) {
762 switch ($key) {
763 case 'filename':
764 return $this->_filename;
765 case 'filenameMD5':
766 return $this->_filenameMD5;
767 case 'newMD5':
768 return $this->_newMD5;
769 case 'SHAC':
770 return $this->_shac;
771 case 'stoppedOnSignature':
772 return $this->_stoppedOnSignature;
773 case 'stoppedOnPosition':
774 return $this->_stoppedOnPosition;
775 case 'isSafeFile':
776 return $this->_isSafeFile;
777 }
778 }
779
780 public function __toString() {
781 return "Record [filename: {$this->filename}, filenameMD5: {$this->filenameMD5}, newMD5: {$this->newMD5}, stoppedOnSignature: {$this->stoppedOnSignature}, stoppedOnPosition: {$this->stoppedOnPosition}]";
782 }
783
784 public function markComplete() {
785 $db = self::getDB();
786 $db->queryWrite("UPDATE " . wfDB::networkTable('wfFileMods') . " SET oldMD5 = newMD5 WHERE filenameMD5 = '%s'", $this->filenameMD5); //A way to mark as scanned so that if we come back from a sleep we don't rescan this one.
787 }
788
789 public function updateStoppedOn($signature, $position) {
790 $this->_stoppedOnSignature = $signature;
791 $this->_stoppedOnPosition = $position;
792 $db = self::getDB();
793 $db->queryWrite("UPDATE " . wfDB::networkTable('wfFileMods') . " SET stoppedOnSignature = '%s', stoppedOnPosition = %d WHERE filenameMD5 = '%s'", $this->stoppedOnSignature, $this->stoppedOnPosition, $this->filenameMD5);
794 }
795
796 public function markSafe() {
797 $db = self::getDB();
798 $db->queryWrite("UPDATE " . wfDB::networkTable('wfFileMods') . " SET isSafeFile = '1' WHERE filenameMD5 = '%s'", $this->filenameMD5);
799 $this->isSafeFile = '1';
800 }
801
802 public function markUnsafe() {
803 $db = self::getDB();
804 $db->queryWrite("UPDATE " . wfDB::networkTable('wfFileMods') . " SET isSafeFile = '0' WHERE filenameMD5 = '%s'", $this->filenameMD5);
805 $this->isSafeFile = '0';
806 }
807 }
808