Wordfence Security – Firewall & Malware Scan - Version 3.3.2

Version Description

  • A complete rearchitecture of Wordfence scanning to massively improve performance.
  • Our free customers are now 100% back in business. Apologies for the delay, but this was worth the wait.
  • Wordfence is now 4X faster for both free and paid customers.
  • Significantly reduced CPU and memory overhead.
  • Significantly reduced network througput when communicating with Wordfence scanning servers.
  • Big performance improvement on our own scanning servers which allows us to continue to provide Wordfence free for the forseeable future.
  • Upgraded scanning API to version 2.4
  • Upgraded Geo IP database to October version.
  • Moved core, theme, plugin and malware scanning into hashing recursive routine for big performance gain.
  • Removed need for fileQ in hashing routine for reduction in memory usage and reduction in DB write size.
  • Removed send-packet architecture and now processing files locally by fetching comparison data from scanning server instead.
  • Removed wfModTracker - old module that is no longer used.
  • Malware is now scanned by fetching hash prefixes from WF server instead of sending hashes of every file to our server. Much more efficient.
  • Made status messages in summary console a little more user friendly.
Download this release

Release Info

Developer mmaunder
Plugin Icon 128x128 Wordfence Security – Firewall & Malware Scan
Version 3.3.2
Comparing to
See all releases

Code changes from version 3.2.7 to 3.3.2

js/admin.js CHANGED
@@ -150,7 +150,7 @@ window['wordfenceAdmin'] = {
150
  pointerWidth: 400,
151
  position: {
152
  edge: edge,
153
- align: align,
154
  }
155
  };
156
  this.currentPointer = jQuery('#' + elemID).pointer(options).pointer('open');
@@ -269,10 +269,18 @@ window['wordfenceAdmin'] = {
269
  var msg = item.msg.replace('SUM_ENDBAD:', '');
270
  jQuery('div.wfSummaryMsg:contains("' + msg + '")').next().addClass('wfSummaryBad').html('Problems found.');
271
  summaryUpdated = true;
 
 
 
 
272
  } else if(item.msg.indexOf('SUM_ENDOK') != -1){
273
  var msg = item.msg.replace('SUM_ENDOK:', '');
274
  jQuery('div.wfSummaryMsg:contains("' + msg + '")').next().addClass('wfSummaryOK').html('Secure.');
275
  summaryUpdated = true;
 
 
 
 
276
  } else if(item.msg.indexOf('SUM_ENDERR') != -1){
277
  var msg = item.msg.replace('SUM_ENDERR:', '');
278
  jQuery('div.wfSummaryMsg:contains("' + msg + '")').next().addClass('wfSummaryErr').html('An error occurred.');
150
  pointerWidth: 400,
151
  position: {
152
  edge: edge,
153
+ align: align
154
  }
155
  };
156
  this.currentPointer = jQuery('#' + elemID).pointer(options).pointer('open');
269
  var msg = item.msg.replace('SUM_ENDBAD:', '');
270
  jQuery('div.wfSummaryMsg:contains("' + msg + '")').next().addClass('wfSummaryBad').html('Problems found.');
271
  summaryUpdated = true;
272
+ } else if(item.msg.indexOf('SUM_ENDFAILED') != -1){
273
+ var msg = item.msg.replace('SUM_ENDFAILED:', '');
274
+ jQuery('div.wfSummaryMsg:contains("' + msg + '")').next().addClass('wfSummaryBad').html('Failed.');
275
+ summaryUpdated = true;
276
  } else if(item.msg.indexOf('SUM_ENDOK') != -1){
277
  var msg = item.msg.replace('SUM_ENDOK:', '');
278
  jQuery('div.wfSummaryMsg:contains("' + msg + '")').next().addClass('wfSummaryOK').html('Secure.');
279
  summaryUpdated = true;
280
+ } else if(item.msg.indexOf('SUM_ENDSUCCESS') != -1){
281
+ var msg = item.msg.replace('SUM_ENDSUCCESS:', '');
282
+ jQuery('div.wfSummaryMsg:contains("' + msg + '")').next().addClass('wfSummaryOK').html('Success.');
283
+ summaryUpdated = true;
284
  } else if(item.msg.indexOf('SUM_ENDERR') != -1){
285
  var msg = item.msg.replace('SUM_ENDERR:', '');
286
  jQuery('div.wfSummaryMsg:contains("' + msg + '")').next().addClass('wfSummaryErr').html('An error occurred.');
lib/GeoIP.dat CHANGED
Binary file
lib/wfAPI.php CHANGED
@@ -13,6 +13,9 @@ class wfAPI {
13
  $this->APIKey = $apiKey;
14
  $this->wordpressVersion = $wordpressVersion;
15
  }
 
 
 
16
  public function call($action, $getParams = array(), $postParams = array()){
17
  $json = $this->getURL($this->getAPIURL() . '/v' . WORDFENCE_API_VERSION . '/?' . $this->makeAPIQueryString() . '&' . http_build_query(
18
  array_merge(
@@ -45,7 +48,7 @@ class wfAPI {
45
  $this->curlDataWritten = 0;
46
  $this->curlContent = "";
47
  $curl = curl_init($url);
48
- curl_setopt ($curl, CURLOPT_TIMEOUT, 300);
49
  curl_setopt ($curl, CURLOPT_USERAGENT, "Wordfence.com UA " . (defined('WORDFENCE_VERSION') ? WORDFENCE_VERSION : '[Unknown version]') );
50
  curl_setopt ($curl, CURLOPT_RETURNTRANSFER, TRUE);
51
  curl_setopt ($curl, CURLOPT_HEADER, 0);
@@ -106,7 +109,7 @@ class wfAPI {
106
  $url = $this->getAPIURL() . '/v' . WORDFENCE_API_VERSION . '/?' . $this->makeAPIQueryString() . '&action=' . $func;
107
  if(function_exists('curl_init')){
108
  $curl = curl_init($url);
109
- curl_setopt ($curl, CURLOPT_TIMEOUT, 300);
110
  //curl_setopt($curl, CURLOPT_VERBOSE, true);
111
  curl_setopt ($curl, CURLOPT_USERAGENT, "Wordfence");
112
  curl_setopt ($curl, CURLOPT_RETURNTRANSFER, TRUE);
13
  $this->APIKey = $apiKey;
14
  $this->wordpressVersion = $wordpressVersion;
15
  }
16
+ public function getStaticURL($url){ // In the form '/something.bin' without quotes
17
+ return $this->getURL($this->getAPIURL() . $url);
18
+ }
19
  public function call($action, $getParams = array(), $postParams = array()){
20
  $json = $this->getURL($this->getAPIURL() . '/v' . WORDFENCE_API_VERSION . '/?' . $this->makeAPIQueryString() . '&' . http_build_query(
21
  array_merge(
48
  $this->curlDataWritten = 0;
49
  $this->curlContent = "";
50
  $curl = curl_init($url);
51
+ curl_setopt ($curl, CURLOPT_TIMEOUT, 900);
52
  curl_setopt ($curl, CURLOPT_USERAGENT, "Wordfence.com UA " . (defined('WORDFENCE_VERSION') ? WORDFENCE_VERSION : '[Unknown version]') );
53
  curl_setopt ($curl, CURLOPT_RETURNTRANSFER, TRUE);
54
  curl_setopt ($curl, CURLOPT_HEADER, 0);
109
  $url = $this->getAPIURL() . '/v' . WORDFENCE_API_VERSION . '/?' . $this->makeAPIQueryString() . '&action=' . $func;
110
  if(function_exists('curl_init')){
111
  $curl = curl_init($url);
112
+ curl_setopt ($curl, CURLOPT_TIMEOUT, 900);
113
  //curl_setopt($curl, CURLOPT_VERBOSE, true);
114
  curl_setopt ($curl, CURLOPT_USERAGENT, "Wordfence");
115
  curl_setopt ($curl, CURLOPT_RETURNTRANSFER, TRUE);
lib/wfModTracker.php DELETED
@@ -1,125 +0,0 @@
1
- <?php
2
- require_once('wordfenceClass.php');
3
- class wfModTracker {
4
- private $themeSum = false;
5
- private $pluginSum = false;
6
- private $coreSum = false;
7
- private $db = false;
8
- private $changesTable = false;
9
- private $anyFilesChangedCached = false;
10
- public function __construct(){
11
- global $wpdb;
12
- $this->changesTable = $wpdb->base_prefix . 'wfFileChanges';
13
- $this->status(2, 'info', "Getting file change DB handle");
14
- $this->db = new wfDB();
15
- $this->status(2, 'info', "Starting theme change check");
16
- $this->themeSum = $this->makeSum(get_theme_root());
17
- $this->status(2, 'info', "Starting plugin change scan");
18
- $this->pluginSum = $this->makeSum(WP_PLUGIN_DIR);
19
- $this->status(2, 'info', "Starting core file change scan");
20
- $this->coreSum = $this->makeCoreSum();
21
- $this->allFilesSum = array();
22
- $this->status(2, 'info', "Getting changes in all other files");
23
- $this->getAllFilesSum(ABSPATH);
24
- $this->status(2, 'info', "Done compiling file changes");
25
- }
26
- public static function resetChanges(){
27
- wfConfig::set('wfmdt_coreSum', '');
28
- wfConfig::set('wfmdt_themeSum', '');
29
- wfConfig::set('wfmdt_pluginSum', '');
30
- $db = new wfDB();
31
- global $wpdb;
32
- $db->query("delete from " . $wpdb->base_prefix . 'wfFileChanges');
33
- }
34
- public function filesModifiedInCore(){ if(wfConfig::get('wfmdt_coreSum') != $this->coreSum){ return true; } else { return false; } }
35
- public function filesModifiedInThemes(){ if(wfConfig::get('wfmdt_themeSum') != $this->themeSum){ return true; } else { return false; } }
36
- public function filesModifiedInPlugins(){ if(wfConfig::get('wfmdt_pluginSum') != $this->pluginSum){ return true; } else { return false; } }
37
- public function getChangedFiles($stripPath, $filterOutFiles){
38
- $changed = array();
39
- foreach($this->allFilesSum as $file => $md5){
40
- if(in_array($file, $filterOutFiles)){ continue; }
41
- $dbSig = $this->db->querySingle("select md5 from " . $this->changesTable . " where filenameHash='%s'", hash('sha256', $file));
42
- if($dbSig != $md5){
43
- $changed[] = substr($file, strlen($stripPath) - 1);
44
- }
45
- }
46
- return $changed;
47
- }
48
- public function anyFilesChanged(){
49
- if(! $this->anyFilesChangedCached){
50
- $changed = false;
51
- $q = $this->db->query("select file, md5 from " . $this->changesTable);
52
- $knownDBFiles = array();
53
- while($row = mysql_fetch_assoc($q)){
54
- $knownDBFiles[$row['file']] = true;
55
- if( (! isset($this->allFilesSum[$row['file']])) || $this->allFilesSum[$row['file']] != $row['md5']){
56
- $changed = true;
57
- //Can't break because we need to populate all of knownDBFiles
58
- }
59
- }
60
- foreach($this->allFilesSum as $file => $md5){
61
- if(! isset($knownDBFiles[$file])){
62
- //We have a new file the DB doesn't know about
63
- $changed = true;
64
- break;
65
- }
66
- }
67
- $this->anyFilesChangedCached = $changed ? 'true' : 'false';
68
- }
69
- return $this->anyFilesChangedCached == 'true' ? true : false;
70
- }
71
- public function logCurrentState(){
72
- wfConfig::set('wfmdt_coreSum', $this->coreSum);
73
- wfConfig::set('wfmdt_themeSum', $this->themeSum);
74
- wfConfig::set('wfmdt_pluginSum', $this->pluginSum);
75
- foreach($this->allFilesSum as $file => $md5){
76
- $this->db->query("insert into " . $this->changesTable . " (file, md5, filenameHash) values ('%s', '%s', '%s') ON DUPLICATE KEY UPDATE md5='%s'", $file, $md5, hash('sha256', $file), $md5);
77
- }
78
- $q = $this->db->query("select file from " . $this->changesTable);
79
- while($row = mysql_fetch_assoc($q)){
80
- if(! isset($this->allFilesSum[$row['file']])){
81
- $this->db->query("delete from " . $this->changesTable . " where filenameHash='%s'", hash('sha256', $row['file']));
82
- }
83
- }
84
- }
85
- private function getAllFilesSum($path){
86
- $path = rtrim($path, '/');
87
- $files = scandir($path);
88
- foreach($files as $file){
89
- if($file == '.' || $file == '..'){ continue; }
90
- $file = $path . '/' . $file;
91
- if(is_file($file)){
92
- $md5 = @md5_file($file);
93
- if($md5){ $this->allFilesSum[$file] = $md5; }
94
- } else if(is_dir($file)){
95
- $this->getAllFilesSum($file, $this->allFilesSum);
96
- }
97
- }
98
- }
99
- private function makeCoreSum(){
100
- return md5(
101
- $this->makeSum(ABSPATH, true) . //norecurse
102
- $this->makeSum(ABSPATH . '/wp-admin/') .
103
- $this->makeSum(ABSPATH . '/wp-includes/')
104
- );
105
- }
106
- public function makeSum($dir, $norecurse = false, $str = ''){
107
- $dir = rtrim($dir, '/');
108
- $files = scandir($dir);
109
- foreach($files as $file){
110
- if($file == '.' || $file == '..'){ continue; }
111
- $file = $dir . '/' . $file;
112
- if(is_file($file)){
113
- $md5 = @md5_file($file);
114
- if($md5){ $str .= $md5; }
115
- } else if((! $norecurse) && is_dir($file)){
116
- $str .= md5($this->makeSum($file, $norecurse, $str));
117
- }
118
- }
119
- return md5($str);
120
- }
121
- private function status($level, $type, $msg){
122
- wordfence::status($level, $type, $msg);
123
- }
124
- }
125
- ?>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/wfScanEngine.php CHANGED
@@ -8,7 +8,7 @@ require_once('wfDB.php');
8
  require_once('wfUtils.php');
9
  class wfScanEngine {
10
  private static $cronTestFailedURLs = array();
11
- private $api = false;
12
  private $dictWords = array();
13
  private $forkRequested = false;
14
 
@@ -22,12 +22,7 @@ class wfScanEngine {
22
  private $startTime = 0;
23
  private $scanStep = 0;
24
  private $maxExecTime = 10; //If more than $maxExecTime has elapsed since last check, fork a new scan process and continue
25
- private $malwareScanEnabled = false;
26
- private $pluginScanEnabled = false;
27
- private $coreScanEnabled = false;
28
  private $publicScanEnabled = false;
29
- private $themeScanEnabled = false;
30
- private $unknownFiles = "";
31
  private $fileContentsResults = false;
32
  private $scanner = false;
33
  private $scanQueue = array();
@@ -42,7 +37,7 @@ class wfScanEngine {
42
  private $userPasswdQueue = "";
43
  private $passwdHasIssues = false;
44
  public function __sleep(){ //Same order here as above for properties that are included in serialization
45
- return array('hasher', 'hashes', 'jobList', 'i', 'wp_version', 'apiKey', 'startTime', 'scanStep', 'maxExecTime', 'publicScanEnabled', 'malwareScanEnabled', 'pluginScanEnabled', 'coreScanEnabled', 'themeScanEnabled', 'unknownFiles', 'fileContentsResults', 'scanner', 'scanQueue', 'hoover', 'scanData', 'statusIDX', 'userPasswdQueue', 'passwdHasIssues');
46
  }
47
  public function __construct(){
48
  $this->startTime = time();
@@ -55,7 +50,7 @@ class wfScanEngine {
55
  include('wfDict.php'); //$dictWords
56
  $this->dictWords = $dictWords;
57
  $this->jobList[] = 'publicSite';
58
- foreach(array('init', 'main', 'finish') as $op){ $this->jobList[] = 'knownFiles_' . $op; };
59
  foreach(array('fileContents', 'posts', 'comments', 'passwds', 'dns', 'diskSpace', 'oldVersions') as $scanType){
60
  if(wfConfig::get('scansEnabled_' . $scanType)){
61
  if(method_exists($this, 'scan_' . $scanType . '_init')){
@@ -153,47 +148,9 @@ class wfScanEngine {
153
  sleep(2); //enough time to read the message before it scrolls off.
154
  }
155
  }
156
- private function scan_knownFiles_init(){
157
  $this->status(1, 'info', "Contacting Wordfence to initiate scan");
158
  $this->api->call('log_scan', array(), array());
159
- if(wfConfig::get('scansEnabled_core')){
160
- $this->coreScanEnabled = true;
161
- $this->statusIDX['core'] = wordfence::statusStart("Comparing core WordPress files against originals in repository");
162
- } else {
163
- wordfence::statusDisabled("Skipping core scan");
164
- }
165
-
166
- //These are both now available to free customers
167
- if(wfConfig::get('scansEnabled_plugins')){
168
- $this->pluginScanEnabled = true;
169
- $this->statusIDX['plugin'] = wordfence::statusStart("Comparing open source plugins against WordPress.org originals");
170
- } else {
171
- wordfence::statusDisabled("Skipping comparing plugin files against originals in repository");
172
- }
173
-
174
- if(wfConfig::get('scansEnabled_themes')){
175
- $this->themeScanEnabled = true;
176
- $this->statusIDX['theme'] = wordfence::statusStart("Comparing open source themes against WordPress.org originals");
177
- } else {
178
- wordfence::statusDisabled("Skipping comparing theme files against originals in repository");
179
- }
180
- //End new section available to free customers
181
-
182
- if(wfConfig::get('scansEnabled_malware')){
183
- $this->statusIDX['unknown'] = wordfence::statusStart("Scanning for known malware files");
184
- $this->malwareScanEnabled = true;
185
- } else {
186
- wordfence::statusDisabled("Skipping malware scan");
187
- $this->status(2, 'info', "Skipping malware scan because it's disabled.");
188
- }
189
- if((! $this->i->summaryUpdateRequired()) && (! ($this->coreScanEnabled || $this->pluginScanEnabled || $this->themeScanEnabled || $this->malwareScanEnabled))){
190
- $this->status(2, 'info', "Finishing this stage because we don't have to do a summary update and we don't need to do a core, plugin, theme or malware scan.");
191
- //Remove main and finish routines because they rely on $this->hasher being created
192
- array_shift($this->jobList);
193
- array_shift($this->jobList);
194
- return array();
195
- }
196
- //CORE SCAN
197
  $this->hasher = new wordfenceHash(strlen(ABSPATH));
198
  $baseWPStuff = array( '.htaccess', 'index.php', 'license.txt', 'readme.html', 'wp-activate.php', 'wp-admin', 'wp-app.php', 'wp-blog-header.php', 'wp-comments-post.php', 'wp-config-sample.php', 'wp-content', 'wp-cron.php', 'wp-includes', 'wp-links-opml.php', 'wp-load.php', 'wp-login.php', 'wp-mail.php', 'wp-pass.php', 'wp-register.php', 'wp-settings.php', 'wp-signup.php', 'wp-trackback.php', 'xmlrpc.php');
199
  $baseContents = scandir(ABSPATH);
@@ -212,107 +169,61 @@ class wfScanEngine {
212
  $includeInScan[] = $file;
213
  }
214
  }
215
- $this->status(2, 'info', "Hashing your WordPress files for comparison against originals.");
216
- $this->hasher->buildFileQueue(ABSPATH, $includeInScan);
217
- }
218
- private function scan_knownFiles_main(){
219
- $this->hasher->genHashes($this);
220
- }
221
- private function scan_knownFiles_finish(){
222
- $this->status(2, 'info', "Done hash. Updating summary items.");
223
- $this->i->updateSummaryItem('totalData', wfUtils::formatBytes($this->hasher->totalData));
224
- $this->i->updateSummaryItem('totalFiles', $this->hasher->totalFiles);
225
- $this->i->updateSummaryItem('totalDirs', $this->hasher->totalDirs);
226
- $this->i->updateSummaryItem('linesOfPHP', $this->hasher->linesOfPHP);
227
- $this->i->updateSummaryItem('linesOfJCH', $this->hasher->linesOfJCH);
228
 
229
  if(! function_exists( 'get_plugins')){
230
  require_once ABSPATH . '/wp-admin/includes/plugin.php';
231
  }
232
  $this->status(2, 'info', "Getting plugin list from WordPress");
233
- $plugins = get_plugins();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  $this->status(2, 'info', "Found " . sizeof($plugins) . " plugins");
235
  $this->i->updateSummaryItem('totalPlugins', sizeof($plugins));
 
236
  if(! function_exists( 'get_themes')){
237
  require_once ABSPATH . '/wp-includes/theme.php';
238
  }
239
  $this->status(2, 'info', "Getting theme list from WordPress");
240
- $themes = get_themes();
241
- $this->status(2, 'info', "Found " . sizeof($themes) . " themes");
242
- $this->i->updateSummaryItem('totalThemes', sizeof($themes));
243
- //Return now because we needed to do a summary update but don't have any other work to do.
244
- if(! ($this->coreScanEnabled || $this->pluginScanEnabled || $this->themeScanEnabled || $this->malwareScanEnabled)){
245
- $this->status(2, 'info', "Finishing up because we have done our required summary update and don't need to do a core, plugin, theme or malware scan.");
246
- return array();
247
- }
248
- $this->status(2, 'info', "Reading theme information from each theme's style.css file");
249
- foreach($themes as $themeName => $themeData){
250
- $cssFile = $themeData['Stylesheet Dir'] . '/style.css';
251
- $cssData = @file_get_contents($cssFile);
252
- if($cssData){
253
- if(preg_match('/Theme URI:\s*([^\r\n]+)/', $cssData, $matches)){ $themes[$themeName]['Theme URI'] = $matches[1]; }
254
- if(preg_match('/License:\s*([^\r\n]+)/', $cssData, $matches)){ $themes[$themeName]['License'] = $matches[1]; }
255
- if(preg_match('/License URI:\s*([^\r\n]+)/', $cssData, $matches)){ $themes[$themeName]['License URI'] = $matches[1]; }
256
- }
257
- }
258
- $this->status(2, 'info', "Sending request to Wordfence servers to do main scan.");
259
-
260
- $scanData = array(
261
- 'pluginScanEnabled' => $this->pluginScanEnabled,
262
- 'themeScanEnabled' => $this->themeScanEnabled,
263
- 'coreScanEnabled' => $this->coreScanEnabled,
264
- 'malwareScanEnabled' => $this->malwareScanEnabled,
265
- 'plugins' => $plugins,
266
- 'themes' => $themes,
267
- 'hashStorageID' => $this->hasher->getHashStorageID()
268
- );
269
- $content = json_encode($scanData);
270
- try {
271
- $dataArr = $this->api->binCall('main_scan', $content);
272
- } catch(Exception $e){
273
- wordfence::statusEndErr();
274
- throw $e;
275
- }
276
- if(! is_array($dataArr)){
277
- wordfence::statusEndErr();
278
- throw new Exception("We received an empty response from the Wordfence server when scanning core, plugin and theme files.");
279
- }
280
- //Data is an encoded string of <4 bytes of total length including these 4 bytes><2 bytes of filename length><filename>
281
- $totalUStrLen = unpack('N', substr($dataArr['data'], 0, 4));
282
- $totalUStrLen = $totalUStrLen[1];
283
- $this->unknownFiles = substr($dataArr['data'], 4, ($totalUStrLen - 4)); //subtruct the first 4 bytes which is an INT that is the total length of unknown string including the 4 bytes
284
- wfConfig::set('lastUnknownFileList', $this->unknownFiles);
285
- $resultArr = json_decode(substr($dataArr['data'], $totalUStrLen), true);
286
- if(! (is_array($resultArr) && isset($resultArr['results'])) ){
287
- wordfence::statusEndErr();
288
- throw new Exception("We received an incorrect response from the Wordfence server when scanning core, plugin and theme files.");
289
- }
290
-
291
- $this->status(2, 'info', "Processing scan results");
292
- $haveIssues = array(
293
- 'core' => false,
294
- 'plugin' => false,
295
- 'theme' => false,
296
- 'unknown' => false
297
- );
298
- foreach($resultArr['results'] as $issue){
299
- $this->status(2, 'info', "Adding issue: " . $issue['shortMsg']);
300
- if($this->addIssue($issue['type'], $issue['severity'], $issue['ignoreP'], $issue['ignoreC'], $issue['shortMsg'], $issue['longMsg'], $issue['data'])){
301
- $haveIssues[$issue['data']['cType']] = true;
302
- }
303
- }
304
- foreach($haveIssues as $type => $have){
305
- if($this->statusIDX[$type] !== false){
306
- wordfence::statusEnd($this->statusIDX[$type], $have);
307
  }
308
  }
 
 
309
 
 
 
 
 
 
 
310
  }
311
  private function scan_fileContents_init(){
312
  $this->statusIDX['infect'] = wordfence::statusStart('Scanning file contents for infections and vulnerabilities');
313
  $this->statusIDX['GSB'] = wordfence::statusStart('Scanning files for URLs in Google\'s Safe Browsing List');
314
- $this->scanner = new wordfenceScanner($this->apiKey, $this->wp_version, $this->unknownFiles, ABSPATH);
315
- $this->unknownFiles = false;
316
  $this->status(2, 'info', "Starting scan of file contents");
317
  }
318
  private function scan_fileContents_main(){
@@ -918,7 +829,7 @@ class wfScanEngine {
918
  public function status($level, $type, $msg){
919
  wordfence::status($level, $type, $msg);
920
  }
921
- private function addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData){
922
  return $this->i->addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData);
923
  }
924
  public static function requestKill(){
8
  require_once('wfUtils.php');
9
  class wfScanEngine {
10
  private static $cronTestFailedURLs = array();
11
+ public $api = false;
12
  private $dictWords = array();
13
  private $forkRequested = false;
14
 
22
  private $startTime = 0;
23
  private $scanStep = 0;
24
  private $maxExecTime = 10; //If more than $maxExecTime has elapsed since last check, fork a new scan process and continue
 
 
 
25
  private $publicScanEnabled = false;
 
 
26
  private $fileContentsResults = false;
27
  private $scanner = false;
28
  private $scanQueue = array();
37
  private $userPasswdQueue = "";
38
  private $passwdHasIssues = false;
39
  public function __sleep(){ //Same order here as above for properties that are included in serialization
40
+ return array('hasher', 'hashes', 'jobList', 'i', 'wp_version', 'apiKey', 'startTime', 'scanStep', 'maxExecTime', 'publicScanEnabled', 'fileContentsResults', 'scanner', 'scanQueue', 'hoover', 'scanData', 'statusIDX', 'userPasswdQueue', 'passwdHasIssues');
41
  }
42
  public function __construct(){
43
  $this->startTime = time();
50
  include('wfDict.php'); //$dictWords
51
  $this->dictWords = $dictWords;
52
  $this->jobList[] = 'publicSite';
53
+ $this->jobList[] = 'knownFiles';
54
  foreach(array('fileContents', 'posts', 'comments', 'passwds', 'dns', 'diskSpace', 'oldVersions') as $scanType){
55
  if(wfConfig::get('scansEnabled_' . $scanType)){
56
  if(method_exists($this, 'scan_' . $scanType . '_init')){
148
  sleep(2); //enough time to read the message before it scrolls off.
149
  }
150
  }
151
+ private function scan_knownFiles(){
152
  $this->status(1, 'info', "Contacting Wordfence to initiate scan");
153
  $this->api->call('log_scan', array(), array());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  $this->hasher = new wordfenceHash(strlen(ABSPATH));
155
  $baseWPStuff = array( '.htaccess', 'index.php', 'license.txt', 'readme.html', 'wp-activate.php', 'wp-admin', 'wp-app.php', 'wp-blog-header.php', 'wp-comments-post.php', 'wp-config-sample.php', 'wp-content', 'wp-cron.php', 'wp-includes', 'wp-links-opml.php', 'wp-load.php', 'wp-login.php', 'wp-mail.php', 'wp-pass.php', 'wp-register.php', 'wp-settings.php', 'wp-signup.php', 'wp-trackback.php', 'xmlrpc.php');
156
  $baseContents = scandir(ABSPATH);
169
  $includeInScan[] = $file;
170
  }
171
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  if(! function_exists( 'get_plugins')){
174
  require_once ABSPATH . '/wp-admin/includes/plugin.php';
175
  }
176
  $this->status(2, 'info', "Getting plugin list from WordPress");
177
+ $pluginData = get_plugins();
178
+ $plugins = array();
179
+ foreach($pluginData as $key => $data){
180
+ if(preg_match('/^([^\/]+)\//', $key, $matches)){
181
+ $pluginDir = $matches[1];
182
+ $pluginFullDir = "wp-content/plugins/" . $pluginDir;
183
+ $plugins[$key] = array(
184
+ 'Name' => $data['Name'],
185
+ 'Version' => $data['Version'],
186
+ 'ShortDir' => $pluginDir,
187
+ 'FullDir' => $pluginFullDir
188
+ );
189
+ }
190
+ }
191
+
192
  $this->status(2, 'info', "Found " . sizeof($plugins) . " plugins");
193
  $this->i->updateSummaryItem('totalPlugins', sizeof($plugins));
194
+
195
  if(! function_exists( 'get_themes')){
196
  require_once ABSPATH . '/wp-includes/theme.php';
197
  }
198
  $this->status(2, 'info', "Getting theme list from WordPress");
199
+ $themeData = get_themes();
200
+ $themes = array();
201
+ foreach($themeData as $themeName => $themeData){
202
+ if(preg_match('/\/([^\/]+)$/', $themeData['Stylesheet Dir'], $matches)){
203
+ $shortDir = $matches[1]; //e.g. evo4cms
204
+ $fullDir = substr($themeData['Stylesheet Dir'], strlen(ABSPATH)); //e.g. wp-content/themes/evo4cms
205
+ $themes[$themeName] = array(
206
+ 'Name' => $themeData['Name'],
207
+ 'Version' => $themeData['Version'],
208
+ 'ShortDir' => $shortDir,
209
+ 'FullDir' => $fullDir
210
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  }
212
  }
213
+ $this->status(2, 'info', "Found " . sizeof($themes) . " themes");
214
+ $this->i->updateSummaryItem('totalThemes', sizeof($themes));
215
 
216
+ $this->hasher->run(ABSPATH, $includeInScan, $themes, $plugins, $this); //Include this so we can call addIssue and ->api->
217
+ $this->i->updateSummaryItem('totalData', wfUtils::formatBytes($this->hasher->totalData));
218
+ $this->i->updateSummaryItem('totalFiles', $this->hasher->totalFiles);
219
+ $this->i->updateSummaryItem('totalDirs', $this->hasher->totalDirs);
220
+ $this->i->updateSummaryItem('linesOfPHP', $this->hasher->linesOfPHP);
221
+ $this->i->updateSummaryItem('linesOfJCH', $this->hasher->linesOfJCH);
222
  }
223
  private function scan_fileContents_init(){
224
  $this->statusIDX['infect'] = wordfence::statusStart('Scanning file contents for infections and vulnerabilities');
225
  $this->statusIDX['GSB'] = wordfence::statusStart('Scanning files for URLs in Google\'s Safe Browsing List');
226
+ $this->scanner = new wordfenceScanner($this->apiKey, $this->wp_version, ABSPATH);
 
227
  $this->status(2, 'info', "Starting scan of file contents");
228
  }
229
  private function scan_fileContents_main(){
829
  public function status($level, $type, $msg){
830
  wordfence::status($level, $type, $msg);
831
  }
832
+ public function addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData){
833
  return $this->i->addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData);
834
  }
835
  public static function requestKill(){
lib/wfSchema.php CHANGED
@@ -143,9 +143,12 @@ class wfSchema {
143
  hostKey binary(4),
144
  KEY k2(hostKey)
145
  ) default charset=utf8",
146
- 'wfFileQueue' => "(
147
- id int UNSIGNED NOT NULL auto_increment PRIMARY KEY,
148
- filename text
 
 
 
149
  ) default charset=utf8"
150
  );
151
  private $db = false;
143
  hostKey binary(4),
144
  KEY k2(hostKey)
145
  ) default charset=utf8",
146
+ 'wfFileMods' => "(
147
+ filenameMD5 binary(16) NOT NULL PRIMARY KEY,
148
+ filename varchar(1000) NOT NULL,
149
+ knownFile tinyint UNSIGNED NOT NULL,
150
+ oldMD5 binary(16) NOT NULL,
151
+ newMD5 binary(16) NOT NULL
152
  ) default charset=utf8"
153
  );
154
  private $db = false;
lib/wordfenceClass.php CHANGED
@@ -229,6 +229,8 @@ class wordfence {
229
  $db->queryIgnoreError("alter table $prefix"."wfBlocks modify column blockedTime bigint signed NOT NULL");
230
  //3.2.1 to 3.2.2
231
  $db->queryIgnoreError("alter table $prefix"."wfLockedOut modify column blockedTime bigint signed NOT NULL");
 
 
232
 
233
  //Must be the final line
234
  }
@@ -1289,7 +1291,7 @@ class wordfence {
1289
  add_action('wp_ajax_wordfence_' . $func, 'wordfence::ajaxReceiver');
1290
  }
1291
 
1292
- if(preg_match('/^Wordfence/', @$_GET['page'])){
1293
  wp_enqueue_style('wp-pointer');
1294
  wp_enqueue_script('wp-pointer');
1295
  wp_enqueue_style('wordfence-main-style', wfUtils::getBaseURL() . 'css/main.css', '', WORDFENCE_VERSION);
@@ -1481,12 +1483,20 @@ class wordfence {
1481
  self::status(10, 'info', 'SUM_START:' . $msg);
1482
  return sizeof($statusStartMsgs) - 1;
1483
  }
1484
- public static function statusEnd($idx, $haveIssues){
1485
  $statusStartMsgs = wfConfig::get_ser('wfStatusStartMsgs', array());
1486
  if($haveIssues){
1487
- self::status(10, 'info', 'SUM_ENDBAD:' . $statusStartMsgs[$idx]);
 
 
 
 
1488
  } else {
1489
- self::status(10, 'info', 'SUM_ENDOK:' . $statusStartMsgs[$idx]);
 
 
 
 
1490
  }
1491
  $statusStartMsgs[$idx] = '';
1492
  wfConfig::set_ser('wfStatusStartMsgs', $statusStartMsgs);
229
  $db->queryIgnoreError("alter table $prefix"."wfBlocks modify column blockedTime bigint signed NOT NULL");
230
  //3.2.1 to 3.2.2
231
  $db->queryIgnoreError("alter table $prefix"."wfLockedOut modify column blockedTime bigint signed NOT NULL");
232
+ $db->queryIgnoreError("drop table if exists $prefix"."wfFileQueue");
233
+ $db->queryIgnoreError("drop table if exists $prefix"."wfFileChanges");
234
 
235
  //Must be the final line
236
  }
1291
  add_action('wp_ajax_wordfence_' . $func, 'wordfence::ajaxReceiver');
1292
  }
1293
 
1294
+ if(isset($_GET['page']) && preg_match('/^Wordfence/', @$_GET['page']) ){
1295
  wp_enqueue_style('wp-pointer');
1296
  wp_enqueue_script('wp-pointer');
1297
  wp_enqueue_style('wordfence-main-style', wfUtils::getBaseURL() . 'css/main.css', '', WORDFENCE_VERSION);
1483
  self::status(10, 'info', 'SUM_START:' . $msg);
1484
  return sizeof($statusStartMsgs) - 1;
1485
  }
1486
+ public static function statusEnd($idx, $haveIssues, $successFailed = false){
1487
  $statusStartMsgs = wfConfig::get_ser('wfStatusStartMsgs', array());
1488
  if($haveIssues){
1489
+ if($successFailed){
1490
+ self::status(10, 'info', 'SUM_ENDFAILED:' . $statusStartMsgs[$idx]);
1491
+ } else {
1492
+ self::status(10, 'info', 'SUM_ENDBAD:' . $statusStartMsgs[$idx]);
1493
+ }
1494
  } else {
1495
+ if($successFailed){
1496
+ self::status(10, 'info', 'SUM_ENDSUCCESS:' . $statusStartMsgs[$idx]);
1497
+ } else {
1498
+ self::status(10, 'info', 'SUM_ENDOK:' . $statusStartMsgs[$idx]);
1499
+ }
1500
  }
1501
  $statusStartMsgs[$idx] = '';
1502
  wfConfig::set_ser('wfStatusStartMsgs', $statusStartMsgs);
lib/wordfenceConstants.php CHANGED
@@ -1,5 +1,5 @@
1
  <?php
2
- define('WORDFENCE_API_VERSION', '2.3');
3
  define('WORDFENCE_API_URL_SEC', 'https://noc1.wordfence.com/');
4
  define('WORDFENCE_API_URL_NONSEC', 'http://noc1.wordfence.com/');
5
  define('WORDFENCE_MAX_SCAN_TIME', 600);
1
  <?php
2
+ define('WORDFENCE_API_VERSION', '2.4');
3
  define('WORDFENCE_API_URL_SEC', 'https://noc1.wordfence.com/');
4
  define('WORDFENCE_API_URL_NONSEC', 'http://noc1.wordfence.com/');
5
  define('WORDFENCE_MAX_SCAN_TIME', 600);
lib/wordfenceHash.php CHANGED
@@ -1,13 +1,6 @@
1
  <?php
2
  require_once('wordfenceClass.php');
3
  class wordfenceHash {
4
- private $apiKey = false;
5
- private $wp_version = false;
6
- private $api = false;
7
- private $db = false;
8
- private $table = false;
9
- private $fileQ = array();
10
-
11
  //Begin serialized vars
12
  private $whitespace = array("\n","\r","\t"," ");
13
  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}'
@@ -16,33 +9,72 @@ class wordfenceHash {
16
  public $linesOfPHP = 0;
17
  public $linesOfJCH = 0; //lines of HTML, CSS and javascript
18
  public $striplen = 0;
19
- private $hashPacket = "";
20
- public $hashStorageID = false;
21
- private $hashingStartTime = false;
22
- private $lastStatusTime = false;
23
- public function __sleep(){ //same order as above
24
- if(sizeof($this->fileQ) > 0){
25
- throw new Exception("Sanity fail. fileQ is not empty. Has: " . sizeof($this->fileQ));
26
- }
27
- return array('whitespace', 'totalData', 'totalFiles', 'totalDirs', 'linesOfPHP', 'linesOfJCH', 'striplen', 'hashPacket', 'hashStorageID', 'hashingStartTime', 'lastStatusTime');
28
- }
29
  public function __construct($striplen){
30
  $this->striplen = $striplen;
31
- $this->db = new wfDB();
32
- $this->table = $this->db->prefix() . 'wfFileQueue';
33
- $this->apiKey = wfConfig::get('apiKey');
34
- $this->wp_version = wfUtils::getWPVersion();
35
- $this->api = new wfAPI($this->apiKey, $this->wp_version);
36
  }
37
- public function __wakeup(){
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  $this->db = new wfDB();
39
- $this->table = $this->db->prefix() . 'wfFileQueue';
40
- $this->apiKey = wfConfig::get('apiKey');
41
- $this->wp_version = wfUtils::getWPVersion();
42
- $this->api = new wfAPI($this->apiKey, $this->wp_version);
43
- }
44
- public function buildFileQueue($path, $only = array()){ //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.
45
- $this->db->truncate($this->table);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  if($path[strlen($path) - 1] != '/'){
47
  $path .= '/';
48
  }
@@ -51,6 +83,16 @@ class wordfenceHash {
51
  exit();
52
  }
53
  $files = scandir($path);
 
 
 
 
 
 
 
 
 
 
54
  foreach($files as $file){
55
  if(sizeof($only) > 0 && (! in_array($file, $only))){
56
  continue;
@@ -58,44 +100,43 @@ class wordfenceHash {
58
  $file = $path . $file;
59
  wordfence::status(4, 'info', "Hashing item in base dir: $file");
60
  $this->_dirHash($file);
61
- }
62
- $this->writeFileQueue(); //Final write to DB
63
-
64
- }
65
- public function genHashes($forkObj){
66
- if(! $this->hashingStartTime){
67
- $this->hashingStartTime = microtime(true);
68
- }
69
- if(! $this->lastStatusTime){
70
- $this->lastStatusTime = microtime(true);
71
- }
72
- $haveMoreInDB = true;
73
- while($haveMoreInDB){
74
- $haveMoreInDB = false;
75
- //This limit used to be 1000, but we changed it to 5 because forkIfNeeded needs to run frequently, but
76
- // we still want to minimize the number of queries we do.
77
- // So now we select, process and delete 5 from teh queue and then check forkIfNeeded()
78
- // So this assumes that processing 5 files won't take longer than wfScanEngine::$maxExecTime (which was 10 at the time of writing, which is 2 secs per file)
79
- $res = $this->db->query("select id, filename from " . $this->table . " limit 5");
80
- $ids = array();
81
- while($rec = mysql_fetch_row($res)){
82
- $this->processFile($rec[1]);
83
- array_push($ids, $rec[0]);
84
- $haveMoreInDB = true;
85
  }
86
- if(sizeof($ids) > 0){
87
- $this->db->query("delete from " . $this->table . " where id IN (" . implode(',', $ids) . ")");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
89
- $forkObj->forkIfNeeded();
90
  }
91
- //Will only reach here if we empty file queue. fork may cause exit
92
- $this->sendHashPacket();
93
- $this->db->truncate($this->table); //Also resets id autoincrement to 1
94
- $this->writeHashingStatus();
95
- }
96
- private function writeHashingStatus(){
97
- $this->lastStatusTime = microtime(true);
98
- wordfence::status(2, 'info', "Scanned " . $this->totalFiles . " files at a rate of " . sprintf('%.2f', ($this->totalFiles / (microtime(true) - $this->hashingStartTime))) . " files per second.");
99
  }
100
  private function _dirHash($path){
101
  if(substr($path, -3, 3) == '/..' || substr($path, -2, 2) == '/.'){
@@ -112,96 +153,140 @@ class wordfenceHash {
112
  if($cont[$i] == '.' || $cont[$i] == '..'){ continue; }
113
  $file = $path . $cont[$i];
114
  if(is_file($file)){
115
- $this->qFile($file);
116
  } else if(is_dir($file)) {
117
  $this->_dirHash($file);
118
  }
119
  }
120
  } else {
121
  if(is_file($path)){
122
- $this->qFile($path);
123
  }
124
  }
125
  }
126
- private function qFile($file){
127
- $this->fileQ[] = $file;
128
- if(sizeof($this->fileQ) > 1000){
129
- $this->writeFileQueue();
130
- }
131
- }
132
- private function writeFileQueue(){
133
- $sql = "insert into " . $this->table . " (filename) values ";
134
- $added = false;
135
- foreach($this->fileQ as $val){
136
- $added = true;
137
- $sql .= "('" . mysql_real_escape_string($val) . "'),";
138
- }
139
- if($added){
140
- $sql = rtrim($sql, ',');
141
- $this->db->query($sql);
142
- }
143
- $this->fileQ = array();
144
- }
145
- private function processFile($file){
146
- if(wfUtils::fileTooBig($file)){
147
- wordfence::status(4, 'info', "Skipping file larger than max size: $file");
148
  return;
149
  }
150
  if(function_exists('memory_get_usage')){
151
- wordfence::status(4, 'info', "Scanning: $file (Mem:" . sprintf('%.1f', memory_get_usage(true) / (1024 * 1024)) . "M)");
152
  } else {
153
- wordfence::status(4, 'info', "Scanning: $file");
154
  }
155
- $wfHash = $this->wfHash($file);
156
  if($wfHash){
157
- $packetFile = substr($file, $this->striplen);
158
- $this->hashPacket .= $wfHash[0] . $wfHash[1] . pack('n', strlen($packetFile)) . $packetFile;
159
- if(strlen($this->hashPacket) > 500000){ //roughly 2 megs in string mem space
160
- $this->writeHashingStatus();
161
- $this->sendHashPacket();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  }
 
 
 
 
163
 
164
  //Now that we know we can open the file, lets update stats
165
- if(preg_match('/\.(?:js|html|htm|css)$/i', $file)){
166
- $this->linesOfJCH += sizeof(file($file));
167
- } else if(preg_match('/\.php$/i', $file)){
168
- $this->linesOfPHP += sizeof(file($file));
169
  }
170
  $this->totalFiles++;
171
- $this->totalData += filesize($file); //We already checked if file overflows int in the fileTooBig routine above
172
- if(microtime(true) - $this->lastStatusTime > 1){
173
- $this->writeHashingStatus();
174
  }
175
  } else {
176
- wordfence::status(2, 'error', "Could not gen hash for file (probably because we don't have permission to access the file): $file");
177
  }
178
  }
179
- private function sendHashPacket(){
180
- wordfence::status(4, 'info', "Sending packet of hash data to Wordfence scanning servers");
181
- if(strlen($this->hashPacket) < 1){
182
- return;
183
- }
184
- if($this->hashStorageID){
185
- $dataArr = $this->api->binCall('add_hash_chunk', "WFID:" . pack('N', $this->hashStorageID) . $this->hashPacket);
186
- $this->hashPacket = "";
187
- if(is_array($dataArr) && isset($dataArr['data']) && $dataArr['data'] == $this->hashStorageID){
188
- //keep going
189
- } else {
190
- throw new Exception("Could not store an additional chunk of hash data on Wordfence servers with ID: " . $this->hashStorageID);
191
- }
192
- } else {
193
- $dataArr = $this->api->binCall('add_hash_chunk', "WFST:" . $this->hashPacket);
194
- $this->hashPacket = "";
195
- if(is_array($dataArr) && isset($dataArr['data']) && preg_match('/^\d+$/', $dataArr['data'])){
196
- $this->hashStorageID = $dataArr['data'];
197
- } else {
198
- throw new Exception("Could not store hash data on Wordfence servers. Got response: " . var_export($dataArr, true));
199
- }
200
- }
201
- }
202
- public function getHashStorageID(){
203
- return $this->hashStorageID;
204
- }
205
  public function wfHash($file){
206
  wfUtils::errorsOff();
207
  $md5 = @md5_file($file, false);
@@ -219,5 +304,12 @@ class wordfenceHash {
219
  $shac = hash_final($ctx, false);
220
  return array($md5, $shac);
221
  }
 
 
 
 
 
 
 
222
  }
223
  ?>
1
  <?php
2
  require_once('wordfenceClass.php');
3
  class wordfenceHash {
 
 
 
 
 
 
 
4
  //Begin serialized vars
5
  private $whitespace = array("\n","\r","\t"," ");
6
  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}'
9
  public $linesOfPHP = 0;
10
  public $linesOfJCH = 0; //lines of HTML, CSS and javascript
11
  public $striplen = 0;
12
+ private $engine = false;
13
+ private $db = false;
14
+ private $coreEnabled = false;
15
+ private $themesEnabled = false;
16
+ private $pluginsEnabled = false;
17
+ private $malwareEnabled = false;
18
+ private $malwareData = "";
19
+ private $possibleMalware = array();
20
+ private $status = array();
21
+ private $haveIssues = array();
22
  public function __construct($striplen){
23
  $this->striplen = $striplen;
 
 
 
 
 
24
  }
25
+ public function run($path, $only, $themes, $plugins, $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.
26
+ $this->engine = $engine;
27
+ if(wfConfig::get('scansEnabled_core')){
28
+ $this->coreEnabled = true;
29
+ }
30
+ if(wfConfig::get('scansEnabled_plugins')){
31
+ $this->pluginsEnabled = true;
32
+ }
33
+ if(wfConfig::get('scansEnabled_themes')){
34
+ $this->themesEnabled = true;
35
+ }
36
+ if(wfConfig::get('scansEnabled_malware')){
37
+ $this->malwareEnabled = true;
38
+ }
39
  $this->db = new wfDB();
40
+
41
+ //Doing a delete for now. Later we can optimize this to only scan modified files.
42
+ //$this->db->query("update " . $this->db->prefix() . "wfFileMods set oldMD5 = newMD5");
43
+ $this->db->query("delete from " . $this->db->prefix() . "wfFileMods");
44
+ $fetchCoreHashesStatus = wordfence::statusStart("Fetching core, theme and plugin file signatures from Wordfence");
45
+ $dataArr = $engine->api->binCall('get_known_files', json_encode(array(
46
+ 'plugins' => $plugins,
47
+ 'themes' => $themes
48
+ )) );
49
+ if($dataArr['code'] != 200){
50
+ wordfence::statusEndErr();
51
+ throw new Exception("Got error response from Wordfence servers: " . $dataArr['code']);
52
+ }
53
+ $this->knownFiles = @json_decode($dataArr['data'], true);
54
+ if(! is_array($this->knownFiles)){
55
+ wordfence::statusEndErr();
56
+ throw new Exception("Invaid response from Wordfence servers.");
57
+ }
58
+ wordfence::statusEnd($fetchCoreHashesStatus, false, true);
59
+
60
+ if($this->malwareEnabled){
61
+ $malwarePrefixStatus = wordfence::statusStart("Fetching list of known malware files from Wordfence");
62
+ $malwareData = $engine->api->getStaticURL('/malwarePrefixes.bin');
63
+ if(! $malwareData){
64
+ wordfence::statusEndErr();
65
+ throw new Exception("Could not fetch malware signatures from Wordfence servers.");
66
+ }
67
+ if(strlen($malwareData) % 4 != 0){
68
+ wordfence::statusEndErr();
69
+ throw new Exception("Malware data received from Wordfence servers was not valid.");
70
+ }
71
+ $this->malwareData = array();
72
+ for($i = 0; $i < strlen($malwareData); $i += 4){
73
+ $this->malwareData[substr($malwareData, $i, 4)] = '1';
74
+ }
75
+ wordfence::statusEnd($malwarePrefixStatus, false, true);
76
+ }
77
+
78
  if($path[strlen($path) - 1] != '/'){
79
  $path .= '/';
80
  }
83
  exit();
84
  }
85
  $files = scandir($path);
86
+ $this->haveIssues = array(
87
+ 'core' => false,
88
+ 'themes' => false,
89
+ 'plugins' => false,
90
+ 'malware' => false
91
+ );
92
+ if($this->coreEnabled){ $this->status['core'] = wordfence::statusStart("Comparing core WordPress files against originals in repository"); } else { wordfence::statusDisabled("Skipping core scan"); }
93
+ if($this->themesEnabled){ $this->status['themes'] = wordfence::statusStart("Comparing open source themes against WordPress.org originals"); } else { wordfence::statusDisabled("Skipping theme scan"); }
94
+ if($this->pluginsEnabled){ $this->status['plugins'] = wordfence::statusStart("Comparing plugins against WordPress.org originals"); } else { wordfence::statusDisabled("Skipping plugin scan"); }
95
+ if($this->malwareEnabled){ $this->status['malware'] = wordfence::statusStart("Scanning for known malware files"); } else { wordfence::statusDisabled("Skipping malware scan"); }
96
  foreach($files as $file){
97
  if(sizeof($only) > 0 && (! in_array($file, $only))){
98
  continue;
100
  $file = $path . $file;
101
  wordfence::status(4, 'info', "Hashing item in base dir: $file");
102
  $this->_dirHash($file);
103
+ }
104
+ wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data.");
105
+ if($this->coreEnabled){ wordfence::statusEnd($this->status['core'], $this->haveIssues['core']); }
106
+ if($this->themesEnabled){ wordfence::statusEnd($this->status['themes'], $this->haveIssues['themes']); }
107
+ if($this->pluginsEnabled){ wordfence::statusEnd($this->status['plugins'], $this->haveIssues['plugins']); }
108
+ if(sizeof($this->possibleMalware) > 0){
109
+ $malwareResp = $engine->api->binCall('check_possible_malware', json_encode($this->possibleMalware));
110
+ if($malwareResp['code'] != 200){
111
+ wordfence::statusEndErr();
112
+ throw new Exception("Invalid response from Wordfence API during check_possible_malware");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
+ $malwareList = json_decode($malwareResp['data'], true);
115
+ if(is_array($malwareList) && sizeof($malwareList) > 0){
116
+ for($i = 0; $i < sizeof($malwareList); $i++){
117
+ $file = $malwareList[$i][0];
118
+ $md5 = $malwareList[$i][1];
119
+ $name = $malwareList[$i][2];
120
+ $this->haveIssues['malware'] = true;
121
+ $this->engine->addIssue(
122
+ 'file',
123
+ 1,
124
+ $path . $file,
125
+ $md5,
126
+ 'This file is suspected malware: ' . $file,
127
+ "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.",
128
+ array(
129
+ 'file' => $file,
130
+ 'cType' => 'unknown',
131
+ 'canDiff' => false,
132
+ 'canFix' => false,
133
+ 'canDelete' => true
134
+ )
135
+ );
136
+ }
137
  }
 
138
  }
139
+ if($this->malwareEnabled){ wordfence::statusEnd($this->status['malware'], $this->haveIssues['malware']); }
 
 
 
 
 
 
 
140
  }
141
  private function _dirHash($path){
142
  if(substr($path, -3, 3) == '/..' || substr($path, -2, 2) == '/.'){
153
  if($cont[$i] == '.' || $cont[$i] == '..'){ continue; }
154
  $file = $path . $cont[$i];
155
  if(is_file($file)){
156
+ $this->processFile($file);
157
  } else if(is_dir($file)) {
158
  $this->_dirHash($file);
159
  }
160
  }
161
  } else {
162
  if(is_file($path)){
163
+ $this->processFile($path);
164
  }
165
  }
166
  }
167
+ private function processFile($realFile){
168
+ $file = substr($realFile, $this->striplen);
169
+ if(wfUtils::fileTooBig($realFile)){
170
+ wordfence::status(4, 'info', "Skipping file larger than max size: $realFile");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  return;
172
  }
173
  if(function_exists('memory_get_usage')){
174
+ wordfence::status(4, 'info', "Scanning: $realFile (Mem:" . sprintf('%.1f', memory_get_usage(true) / (1024 * 1024)) . "M)");
175
  } else {
176
+ wordfence::status(4, 'info', "Scanning: $realFile");
177
  }
178
+ $wfHash = $this->wfHash($realFile);
179
  if($wfHash){
180
+ $md5 = strtoupper($wfHash[0]);
181
+ $shac = strtoupper($wfHash[1]);
182
+ $knownFile = 0;
183
+ if($this->malwareEnabled && $this->isMalwarePrefix($md5)){
184
+ $this->possibleMalware[] = array($file, $md5);
185
+ }
186
+ if(isset($this->knownFiles['core'][$file])){
187
+ if(strtoupper($this->knownFiles['core'][$file]) == $shac){
188
+ $knownFile = 1;
189
+ } else {
190
+ if($this->coreEnabled){
191
+ $this->haveIssues['core'] = true;
192
+ $this->engine->addIssue(
193
+ 'file',
194
+ 1,
195
+ 'coreModified' . $file . $md5,
196
+ 'coreModified' . $file,
197
+ 'WordPress core file modified: ' . $file,
198
+ "This WordPress core file has been modified and differs from the original file distributed with this version of WordPress.",
199
+ array(
200
+ 'file' => $file,
201
+ 'cType' => 'core',
202
+ 'canDiff' => true,
203
+ 'canFix' => true,
204
+ 'canDelete' => false
205
+ )
206
+ );
207
+ }
208
+ }
209
+ } else if(isset($this->knownFiles['plugins'][$file])){
210
+ if(in_array($shac, $this->knownFiles['plugins'][$file])){
211
+ $knownFile = 1;
212
+ } else {
213
+ if($this->pluginsEnabled){
214
+ $itemName = $this->knownFiles['plugins'][$file][0];
215
+ $itemVersion = $this->knownFiles['plugins'][$file][1];
216
+ $cKey = $this->knownFiles['plugins'][$file][2];
217
+ $this->haveIssues['plugins'] = true;
218
+ $this->engine->addIssue(
219
+ 'file',
220
+ 2,
221
+ 'modifiedplugin' . $file . $md5,
222
+ 'modifiedplugin' . $file,
223
+ 'Modified plugin file: ' . $file,
224
+ "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]",
225
+ array(
226
+ 'file' => $file,
227
+ 'cType' => 'plugin',
228
+ 'canDiff' => true,
229
+ 'canFix' => true,
230
+ 'canDelete' => false,
231
+ 'cName' => $itemName,
232
+ 'cVersion' => $itemVersion,
233
+ 'cKey' => $cKey
234
+ )
235
+ );
236
+ }
237
+
238
+ }
239
+ } else if(isset($this->knownFiles['themes'][$file])){
240
+ if(in_array($shac, $this->knownFiles['themes'][$file])){
241
+ $knownFile = 1;
242
+ } else {
243
+ if($this->themesEnabled){
244
+ $itemName = $this->knownFiles['themes'][$file][0];
245
+ $itemVersion = $this->knownFiles['themes'][$file][1];
246
+ $cKey = $this->knownFiles['themes'][$file][2];
247
+ $this->haveIssues['themes'] = true;
248
+ $this->engine->addIssue(
249
+ 'file',
250
+ 2,
251
+ 'modifiedtheme' . $file . $md5,
252
+ 'modifiedtheme' . $file,
253
+ 'Modified theme file: ' . $file,
254
+ "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.",
255
+ array(
256
+ 'file' => $file,
257
+ 'cType' => 'theme',
258
+ 'canDiff' => true,
259
+ 'canFix' => true,
260
+ 'canDelete' => false,
261
+ 'cName' => $itemName,
262
+ 'cVersion' => $itemVersion,
263
+ 'cKey' => $cKey
264
+ )
265
+ );
266
+ }
267
+
268
+ }
269
  }
270
+ // knownFile means that the file is both part of core or a known plugin or theme AND that we recognize the file's hash.
271
+ // we could split this into files who's path we recognize and file's who's path we recognize AND who have a valid sig.
272
+ // But because we want to scan files who's sig we don't recognize, regardless of known path or not, we only need one "knownFile" field.
273
+ $this->db->query("insert into " . $this->db->prefix() . "wfFileMods (filename, filenameMD5, knownFile, oldMD5, newMD5) values ('%s', unhex(md5('%s')), %d, '', unhex('%s')) ON DUPLICATE KEY UPDATE newMD5=unhex('%s'), knownFile=%d", $file, $file, $knownFile, $md5, $md5, $knownFile);
274
 
275
  //Now that we know we can open the file, lets update stats
276
+ if(preg_match('/\.(?:js|html|htm|css)$/i', $realFile)){
277
+ $this->linesOfJCH += sizeof(file($realFile));
278
+ } else if(preg_match('/\.php$/i', $realFile)){
279
+ $this->linesOfPHP += sizeof(file($realFile));
280
  }
281
  $this->totalFiles++;
282
+ $this->totalData += filesize($realFile); //We already checked if file overflows int in the fileTooBig routine above
283
+ if($this->totalFiles % 100 === 0){
284
+ wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data so far");
285
  }
286
  } else {
287
+ //wordfence::status(2, 'error', "Could not gen hash for file (probably because we don't have permission to access the file): $realFile");
288
  }
289
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  public function wfHash($file){
291
  wfUtils::errorsOff();
292
  $md5 = @md5_file($file, false);
304
  $shac = hash_final($ctx, false);
305
  return array($md5, $shac);
306
  }
307
+ private function isMalwarePrefix($hexMD5){
308
+ $binPrefix = pack("H*", substr($hexMD5, 0, 8));
309
+ if(isset($this->malwareData[$binPrefix])){
310
+ return true;
311
+ }
312
+ return false;
313
+ }
314
  }
315
  ?>
lib/wordfenceScanner.php CHANGED
@@ -5,7 +5,6 @@ require_once('wordfenceURLHoover.php');
5
  class wordfenceScanner {
6
  //serialized:
7
  protected $path = '';
8
- protected $fileList = array();
9
  protected $results = array();
10
  public $errorMsg = false;
11
  private $apiKey = false;
@@ -15,15 +14,14 @@ class wordfenceScanner {
15
  private $lastStatusTime = false;
16
  private $patterns = "";
17
  public function __sleep(){
18
- return array('path', 'fileList', 'results', 'errorMsg', 'apiKey', 'wordpressVersion', 'urlHoover', 'totalFilesScanned', 'startTime', 'lastStatusTime', 'patterns');
19
  }
20
  public function __wakeup(){
21
  }
22
- public function __construct($apiKey, $wordpressVersion, $fileList, $path){
23
  $this->apiKey = $apiKey;
24
  $this->wordpressVersion = $wordpressVersion;
25
  $this->api = new wfAPI($this->apiKey, $this->wordpressVersion);
26
- $this->fileList = $fileList; //A long string of <2 byte network order short showing filename length><filename>
27
  if($path[strlen($path) - 1] != '/'){
28
  $path .= '/';
29
  }
@@ -51,16 +49,12 @@ class wordfenceScanner {
51
  if(! $this->lastStatusTime){
52
  $this->lastStatusTime = microtime(true);
53
  }
54
- while(strlen($this->fileList) > 0){
55
- $filenameLen = unpack('n', substr($this->fileList, 0, 2));
56
- $filenameLen = $filenameLen[1];
57
- if($filenameLen > 1000 || $filenameLen < 1){
58
- wordfence::status(1, 'error', "wordfenceScanner got bad data from the Wordfence API with a filename length of: " . $filenameLen);
59
- exit();
60
- }
61
-
62
- $file = substr($this->fileList, 2, $filenameLen);
63
- $this->fileList = substr($this->fileList, 2 + $filenameLen);
64
 
65
  if(! file_exists($this->path . $file)){
66
  continue;
@@ -96,11 +90,6 @@ class wordfenceScanner {
96
  }
97
 
98
  $stime = microtime(true);
99
- $fileSum = @md5_file($this->path . $file);
100
- if(! $fileSum){
101
- //usually permission denied
102
- continue;
103
- }
104
  $fh = @fopen($this->path . $file, 'r');
105
  if(! $fh){
106
  continue;
@@ -250,7 +239,7 @@ class wordfenceScanner {
250
  return $this->results;
251
  }
252
  private function writeScanningStatus(){
253
- wordfence::status(2, 'info', "Scanned contents of " . $this->totalFilesScanned . " files at a rate of " . sprintf('%.2f', ($this->totalFilesScanned / (microtime(true) - $this->startTime))) . " files per second");
254
  }
255
  private function addEncIssue($ignoreP, $ignoreC, $encoding, $file){
256
  $this->addResult(array(
5
  class wordfenceScanner {
6
  //serialized:
7
  protected $path = '';
 
8
  protected $results = array();
9
  public $errorMsg = false;
10
  private $apiKey = false;
14
  private $lastStatusTime = false;
15
  private $patterns = "";
16
  public function __sleep(){
17
+ return array('path', 'results', 'errorMsg', 'apiKey', 'wordpressVersion', 'urlHoover', 'totalFilesScanned', 'startTime', 'lastStatusTime', 'patterns');
18
  }
19
  public function __wakeup(){
20
  }
21
+ public function __construct($apiKey, $wordpressVersion, $path){
22
  $this->apiKey = $apiKey;
23
  $this->wordpressVersion = $wordpressVersion;
24
  $this->api = new wfAPI($this->apiKey, $this->wordpressVersion);
 
25
  if($path[strlen($path) - 1] != '/'){
26
  $path .= '/';
27
  }
49
  if(! $this->lastStatusTime){
50
  $this->lastStatusTime = microtime(true);
51
  }
52
+ $db = new wfDB();
53
+ $res1 = $db->query("select filename, filenameMD5, hex(newMD5) as newMD5 from " . $db->prefix() . "wfFileMods where oldMD5 != newMD5 and knownFile=0");
54
+ while($rec1 = mysql_fetch_assoc($res1)){
55
+ $db->query("update " . $db->prefix() . "wfFileMods set oldMD5 = newMD5 where filenameMD5='%s'", $rec1['filenameMD5']); //A way to mark as scanned so that if we come back from a sleep we don't rescan this one.
56
+ $file = $rec1['filename'];
57
+ $fileSum = $rec1['newMD5'];
 
 
 
 
58
 
59
  if(! file_exists($this->path . $file)){
60
  continue;
90
  }
91
 
92
  $stime = microtime(true);
 
 
 
 
 
93
  $fh = @fopen($this->path . $file, 'r');
94
  if(! $fh){
95
  continue;
239
  return $this->results;
240
  }
241
  private function writeScanningStatus(){
242
+ wordfence::status(2, 'info', "Scanned contents of " . $this->totalFilesScanned . " unrecognized files at " . sprintf('%.2f', ($this->totalFilesScanned / (microtime(true) - $this->startTime))) . " per second");
243
  }
244
  private function addEncIssue($ignoreP, $ignoreC, $encoding, $file){
245
  $this->addResult(array(
readme.txt CHANGED
@@ -2,8 +2,8 @@
2
  Contributors: mmaunder
3
  Tags: wordpress, security, wordpress security, security plugin, secure, anti-virus, malware, firewall, antivirus, virus, google safe browsing, phishing, scrapers, hacking, wordfence, securty, secrity, secure
4
  Requires at least: 3.3.1
5
- Tested up to: 3.4.1
6
- Stable tag: 3.2.7
7
 
8
  Wordfence Security is a free enterprise class security plugin that includes a firewall, virus scanning, real-time traffic with geolocation and more.
9
 
@@ -153,6 +153,22 @@ or a theme, because often these have been updated to fix a security hole.
153
  5. If you're technically minded, this is the under-the-hood view of Wordfence options where you can fine-tune your security settings.
154
 
155
  == Changelog ==
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  = 3.2.7 =
157
  * Fixed dates and times in activity log alert emails and other emails to be in site's local timezone.
158
  * Added advanced country blocking options which allow bypass if a special URL is hit.
2
  Contributors: mmaunder
3
  Tags: wordpress, security, wordpress security, security plugin, secure, anti-virus, malware, firewall, antivirus, virus, google safe browsing, phishing, scrapers, hacking, wordfence, securty, secrity, secure
4
  Requires at least: 3.3.1
5
+ Tested up to: 3.4.2
6
+ Stable tag: 3.3.2
7
 
8
  Wordfence Security is a free enterprise class security plugin that includes a firewall, virus scanning, real-time traffic with geolocation and more.
9
 
153
  5. If you're technically minded, this is the under-the-hood view of Wordfence options where you can fine-tune your security settings.
154
 
155
  == Changelog ==
156
+ = 3.3.2 =
157
+ * A complete rearchitecture of Wordfence scanning to massively improve performance.
158
+ * Our free customers are now 100% back in business. Apologies for the delay, but this was worth the wait.
159
+ * Wordfence is now 4X faster for both free and paid customers.
160
+ * Significantly reduced CPU and memory overhead.
161
+ * Significantly reduced network througput when communicating with Wordfence scanning servers.
162
+ * Big performance improvement on our own scanning servers which allows us to continue to provide Wordfence free for the forseeable future.
163
+ * Upgraded scanning API to version 2.4
164
+ * Upgraded Geo IP database to October version.
165
+ * Moved core, theme, plugin and malware scanning into hashing recursive routine for big performance gain.
166
+ * Removed need for fileQ in hashing routine for reduction in memory usage and reduction in DB write size.
167
+ * Removed send-packet architecture and now processing files locally by fetching comparison data from scanning server instead.
168
+ * Removed wfModTracker - old module that is no longer used.
169
+ * Malware is now scanned by fetching hash prefixes from WF server instead of sending hashes of every file to our server. Much more efficient.
170
+ * Made status messages in summary console a little more user friendly.
171
+
172
  = 3.2.7 =
173
  * Fixed dates and times in activity log alert emails and other emails to be in site's local timezone.
174
  * Added advanced country blocking options which allow bypass if a special URL is hit.
wordfence.php CHANGED
@@ -4,10 +4,10 @@ Plugin Name: Wordfence Security
4
  Plugin URI: http://wordfence.com/
5
  Description: Wordfence Security - Anti-virus and Firewall security plugin for WordPress
6
  Author: Mark Maunder
7
- Version: 3.2.7
8
  Author URI: http://wordfence.com/
9
  */
10
- define('WORDFENCE_VERSION', '3.2.7');
11
  if(! defined('WORDFENCE_VERSIONONLY_MODE')){
12
  if((int) @ini_get('memory_limit') < 64){
13
  @ini_set('memory_limit', '64M'); //Some hosts have ini set at as little as 32 megs. 64 is the min sane amount of memory.
4
  Plugin URI: http://wordfence.com/
5
  Description: Wordfence Security - Anti-virus and Firewall security plugin for WordPress
6
  Author: Mark Maunder
7
+ Version: 3.3.2
8
  Author URI: http://wordfence.com/
9
  */
10
+ define('WORDFENCE_VERSION', '3.3.2');
11
  if(! defined('WORDFENCE_VERSIONONLY_MODE')){
12
  if((int) @ini_get('memory_limit') < 64){
13
  @ini_set('memory_limit', '64M'); //Some hosts have ini set at as little as 32 megs. 64 is the min sane amount of memory.