Diff
14 years ago
whois
12 years ago
.htaccess
14 years ago
Diff.php
14 years ago
GeoIP.dat
11 years ago
IPTraf.php
11 years ago
conntest.php
11 years ago
dashboard.php
11 years ago
diffResult.php
11 years ago
email_genericAlert.php
11 years ago
email_newIssues.php
11 years ago
email_unlockRequest.php
11 years ago
menuHeader.php
11 years ago
menu_activity.php
11 years ago
menu_blockedIPs.php
11 years ago
menu_countryBlocking.php
11 years ago
menu_options.php
11 years ago
menu_rangeBlocking.php
11 years ago
menu_scan.php
11 years ago
menu_scanSchedule.php
11 years ago
menu_sitePerf.php
11 years ago
menu_sitePerfStats.php
11 years ago
menu_twoFactor.php
11 years ago
menu_whois.php
11 years ago
pageTitle.php
13 years ago
schedWeekEntry.php
12 years ago
sysinfo.php
11 years ago
unknownFiles.php
11 years ago
viewFullActivityLog.php
11 years ago
wf503.php
12 years ago
wfAPI.php
11 years ago
wfAction.php
14 years ago
wfArray.php
13 years ago
wfBrowscap.php
11 years ago
wfBrowscapCache.php
11 years ago
wfBulkCountries.php
13 years ago
wfCache.php
11 years ago
wfConfig.php
11 years ago
wfCountryMap.php
13 years ago
wfCrawl.php
11 years ago
wfDB.php
11 years ago
wfDict.php
14 years ago
wfGeoIP.php
13 years ago
wfIssues.php
11 years ago
wfLockedOut.php
13 years ago
wfLog.php
11 years ago
wfRate.php
14 years ago
wfScan.php
11 years ago
wfScanEngine.php
11 years ago
wfSchema.php
11 years ago
wfUnlockMsg.php
11 years ago
wfUtils.php
11 years ago
wfViewResult.php
11 years ago
wordfenceClass.php
11 years ago
wordfenceConstants.php
11 years ago
wordfenceHash.php
11 years ago
wordfenceScanner.php
11 years ago
wordfenceURLHoover.php
11 years ago
wordfenceHash.php
371 lines
| 1 | <?php |
| 2 | require_once('wordfenceClass.php'); |
| 3 | class wordfenceHash { |
| 4 | private $engine = false; |
| 5 | private $db = false; |
| 6 | private $startTime = false; |
| 7 | |
| 8 | //Begin serialized vars |
| 9 | public $striplen = 0; |
| 10 | public $totalFiles = 0; |
| 11 | public $totalDirs = 0; |
| 12 | public $totalData = 0; //To do a sanity check, don't use 'du' because it gets sparse files wrong and reports blocks used on disk. Use : find . -type f -ls | awk '{total += $7} END {print total}' |
| 13 | public $linesOfPHP = 0; |
| 14 | public $linesOfJCH = 0; //lines of HTML, CSS and javascript |
| 15 | public $stoppedOnFile = false; |
| 16 | private $coreEnabled = false; |
| 17 | private $pluginsEnabled = false; |
| 18 | private $themesEnabled = false; |
| 19 | private $malwareEnabled = false; |
| 20 | private $knownFiles = false; |
| 21 | private $malwareData = ""; |
| 22 | private $haveIssues = array(); |
| 23 | private $status = array(); |
| 24 | private $possibleMalware = array(); |
| 25 | private $path = false; |
| 26 | private $only = false; |
| 27 | private $totalForks = 0; |
| 28 | |
| 29 | public function __construct($striplen, $path, $only, $themes, $plugins, $engine){ |
| 30 | $this->striplen = $striplen; |
| 31 | $this->path = $path; |
| 32 | $this->only = $only; |
| 33 | |
| 34 | $this->startTime = microtime(true); |
| 35 | |
| 36 | if(wfConfig::get('scansEnabled_core')){ |
| 37 | $this->coreEnabled = true; |
| 38 | } |
| 39 | if(wfConfig::get('scansEnabled_plugins')){ |
| 40 | $this->pluginsEnabled = true; |
| 41 | } |
| 42 | if(wfConfig::get('scansEnabled_themes')){ |
| 43 | $this->themesEnabled = true; |
| 44 | } |
| 45 | if(wfConfig::get('scansEnabled_malware')){ |
| 46 | $this->malwareEnabled = true; |
| 47 | } |
| 48 | $this->db = new wfDB(); |
| 49 | |
| 50 | //Doing a delete for now. Later we can optimize this to only scan modified files. |
| 51 | //$this->db->queryWrite("update " . $this->db->prefix() . "wfFileMods set oldMD5 = newMD5"); |
| 52 | $this->db->queryWrite("delete from " . $this->db->prefix() . "wfFileMods"); |
| 53 | $fetchCoreHashesStatus = wordfence::statusStart("Fetching core, theme and plugin file signatures from Wordfence"); |
| 54 | $dataArr = $engine->api->binCall('get_known_files', json_encode(array( |
| 55 | 'plugins' => $plugins, |
| 56 | 'themes' => $themes |
| 57 | )) ); |
| 58 | if($dataArr['code'] != 200){ |
| 59 | wordfence::statusEndErr(); |
| 60 | throw new Exception("Got error response from Wordfence servers: " . $dataArr['code']); |
| 61 | } |
| 62 | $this->knownFiles = @json_decode($dataArr['data'], true); |
| 63 | if(! is_array($this->knownFiles)){ |
| 64 | wordfence::statusEndErr(); |
| 65 | throw new Exception("Invalid response from Wordfence servers."); |
| 66 | } |
| 67 | wordfence::statusEnd($fetchCoreHashesStatus, false, true); |
| 68 | if($this->malwareEnabled){ |
| 69 | $malwarePrefixStatus = wordfence::statusStart("Fetching list of known malware files from Wordfence"); |
| 70 | $malwareData = $engine->api->getStaticURL('/malwarePrefixes.bin'); |
| 71 | if(! $malwareData){ |
| 72 | wordfence::statusEndErr(); |
| 73 | throw new Exception("Could not fetch malware signatures from Wordfence servers."); |
| 74 | } |
| 75 | if(strlen($malwareData) % 4 != 0){ |
| 76 | wordfence::statusEndErr(); |
| 77 | throw new Exception("Malware data received from Wordfence servers was not valid."); |
| 78 | } |
| 79 | $this->malwareData = array(); |
| 80 | for($i = 0; $i < strlen($malwareData); $i += 4){ |
| 81 | $this->malwareData[substr($malwareData, $i, 4)] = '1'; |
| 82 | } |
| 83 | wordfence::statusEnd($malwarePrefixStatus, false, true); |
| 84 | } |
| 85 | |
| 86 | if($this->path[strlen($this->path) - 1] != '/'){ |
| 87 | $this->path .= '/'; |
| 88 | } |
| 89 | if(! is_readable($path)){ |
| 90 | throw new Exception("Could not read directory " . $this->path . " to do scan."); |
| 91 | exit(); |
| 92 | } |
| 93 | $this->haveIssues = array( |
| 94 | 'core' => false, |
| 95 | 'themes' => false, |
| 96 | 'plugins' => false, |
| 97 | 'malware' => false |
| 98 | ); |
| 99 | if($this->coreEnabled){ $this->status['core'] = wordfence::statusStart("Comparing core WordPress files against originals in repository"); } else { wordfence::statusDisabled("Skipping core scan"); } |
| 100 | if($this->themesEnabled){ $this->status['themes'] = wordfence::statusStart("Comparing open source themes against WordPress.org originals"); } else { wordfence::statusDisabled("Skipping theme scan"); } |
| 101 | if($this->pluginsEnabled){ $this->status['plugins'] = wordfence::statusStart("Comparing plugins against WordPress.org originals"); } else { wordfence::statusDisabled("Skipping plugin scan"); } |
| 102 | if($this->malwareEnabled){ $this->status['malware'] = wordfence::statusStart("Scanning for known malware files"); } else { wordfence::statusDisabled("Skipping malware scan"); } |
| 103 | } |
| 104 | public function __sleep(){ |
| 105 | return array('striplen', 'totalFiles', 'totalDirs', 'totalData', 'linesOfPHP', 'linesOfJCH', 'stoppedOnFile', 'coreEnabled', 'pluginsEnabled', 'themesEnabled', 'malwareEnabled', 'knownFiles', 'malwareData', 'haveIssues', 'status', 'possibleMalware', 'path', 'only', 'totalForks'); |
| 106 | } |
| 107 | public function __wakeup(){ |
| 108 | $this->db = new wfDB(); |
| 109 | $this->startTime = microtime(true); |
| 110 | $this->totalForks++; |
| 111 | } |
| 112 | public function run($engine){ //base path and 'only' is a list of files and dirs in the bast that are the only ones that should be processed. Everything else in base is ignored. If only is empty then everything is processed. |
| 113 | if($this->totalForks > 1000){ |
| 114 | throw new Exception("Wordfence file scanner detected a possible infinite loop. Exiting on file: " . $this->stoppedOnFile); |
| 115 | } |
| 116 | $this->engine = $engine; |
| 117 | $files = scandir($this->path); |
| 118 | foreach($files as $file){ |
| 119 | if($file == '.' || $file == '..'){ continue; } |
| 120 | if(sizeof($this->only) > 0 && (! in_array($file, $this->only))){ |
| 121 | continue; |
| 122 | } |
| 123 | $file = $this->path . $file; |
| 124 | wordfence::status(4, 'info', "Hashing item in base dir: $file"); |
| 125 | $this->_dirHash($file); |
| 126 | } |
| 127 | wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data."); |
| 128 | if($this->coreEnabled){ wordfence::statusEnd($this->status['core'], $this->haveIssues['core']); } |
| 129 | if($this->themesEnabled){ wordfence::statusEnd($this->status['themes'], $this->haveIssues['themes']); } |
| 130 | if($this->pluginsEnabled){ wordfence::statusEnd($this->status['plugins'], $this->haveIssues['plugins']); } |
| 131 | if(sizeof($this->possibleMalware) > 0){ |
| 132 | $malwareResp = $engine->api->binCall('check_possible_malware', json_encode($this->possibleMalware)); |
| 133 | if($malwareResp['code'] != 200){ |
| 134 | wordfence::statusEndErr(); |
| 135 | throw new Exception("Invalid response from Wordfence API during check_possible_malware"); |
| 136 | } |
| 137 | $malwareList = json_decode($malwareResp['data'], true); |
| 138 | if(is_array($malwareList) && sizeof($malwareList) > 0){ |
| 139 | for($i = 0; $i < sizeof($malwareList); $i++){ |
| 140 | $file = $malwareList[$i][0]; |
| 141 | $md5 = $malwareList[$i][1]; |
| 142 | $name = $malwareList[$i][2]; |
| 143 | $this->haveIssues['malware'] = true; |
| 144 | $this->engine->addIssue( |
| 145 | 'file', |
| 146 | 1, |
| 147 | $this->path . $file, |
| 148 | $md5, |
| 149 | 'This file is suspected malware: ' . $file, |
| 150 | "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.", |
| 151 | array( |
| 152 | 'file' => $file, |
| 153 | 'cType' => 'unknown', |
| 154 | 'canDiff' => false, |
| 155 | 'canFix' => false, |
| 156 | 'canDelete' => true |
| 157 | ) |
| 158 | ); |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | if($this->malwareEnabled){ wordfence::statusEnd($this->status['malware'], $this->haveIssues['malware']); } |
| 163 | } |
| 164 | private function _dirHash($path){ |
| 165 | if(substr($path, -3, 3) == '/..' || substr($path, -2, 2) == '/.'){ |
| 166 | return; |
| 167 | } |
| 168 | if(! is_readable($path)){ return; } //Applies to files and dirs |
| 169 | if(is_dir($path)){ |
| 170 | $this->totalDirs++; |
| 171 | if($path[strlen($path) - 1] != '/'){ |
| 172 | $path .= '/'; |
| 173 | } |
| 174 | $cont = scandir($path); |
| 175 | for($i = 0; $i < sizeof($cont); $i++){ |
| 176 | if($cont[$i] == '.' || $cont[$i] == '..'){ continue; } |
| 177 | $file = $path . $cont[$i]; |
| 178 | if(is_file($file)){ |
| 179 | $this->processFile($file); |
| 180 | } else if(is_dir($file)) { |
| 181 | $this->_dirHash($file); |
| 182 | } |
| 183 | } |
| 184 | } else { |
| 185 | if(is_file($path)){ |
| 186 | $this->processFile($path); |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | private function processFile($realFile){ |
| 191 | $file = substr($realFile, $this->striplen); |
| 192 | if( (! $this->stoppedOnFile) && microtime(true) - $this->startTime > $this->engine->maxExecTime){ //max X seconds but don't allow fork if we're looking for the file we stopped on. Search mode is VERY fast. |
| 193 | $this->stoppedOnFile = $file; |
| 194 | wordfence::status(4, 'info', "Calling fork() from wordfenceHash::processFile with maxExecTime: " . $this->engine->maxExecTime); |
| 195 | $this->engine->fork(); |
| 196 | //exits |
| 197 | } |
| 198 | |
| 199 | //Put this after the fork, that way we will at least scan one more file after we fork if it takes us more than 10 seconds to search for the stoppedOnFile |
| 200 | if($this->stoppedOnFile && $file != $this->stoppedOnFile){ |
| 201 | return; |
| 202 | } else if($this->stoppedOnFile && $file == $this->stoppedOnFile){ |
| 203 | $this->stoppedOnFile = false; //Continue scanning |
| 204 | } |
| 205 | |
| 206 | if(wfUtils::fileTooBig($realFile)){ |
| 207 | wordfence::status(4, 'info', "Skipping file larger than max size: $realFile"); |
| 208 | return; |
| 209 | } |
| 210 | if(function_exists('memory_get_usage')){ |
| 211 | wordfence::status(4, 'info', "Scanning: $realFile (Mem:" . sprintf('%.1f', memory_get_usage(true) / (1024 * 1024)) . "M)"); |
| 212 | } else { |
| 213 | wordfence::status(4, 'info', "Scanning: $realFile"); |
| 214 | } |
| 215 | $wfHash = self::wfHash($realFile); |
| 216 | if($wfHash){ |
| 217 | $md5 = strtoupper($wfHash[0]); |
| 218 | $shac = strtoupper($wfHash[1]); |
| 219 | $knownFile = 0; |
| 220 | if($this->malwareEnabled && $this->isMalwarePrefix($md5)){ |
| 221 | $this->possibleMalware[] = array($file, $md5); |
| 222 | } |
| 223 | if(isset($this->knownFiles['core'][$file])){ |
| 224 | if(strtoupper($this->knownFiles['core'][$file]) == $shac){ |
| 225 | $knownFile = 1; |
| 226 | } else { |
| 227 | if($this->coreEnabled){ |
| 228 | $localFile = ABSPATH . '/' . preg_replace('/^[\.\/]+/', '', $file); |
| 229 | $fileContents = @file_get_contents($localFile); |
| 230 | if($fileContents && (! preg_match('/<\?' . 'php[\r\n\s\t]*\/\/[\r\n\s\t]*Silence is golden\.[\r\n\s\t]*(?:\?>)?[\r\n\s\t]*$/s', $fileContents))){ //<?php |
| 231 | if(! $this->isSafeFile($shac)){ |
| 232 | |
| 233 | $this->haveIssues['core'] = true; |
| 234 | $this->engine->addIssue( |
| 235 | 'file', |
| 236 | 1, |
| 237 | 'coreModified' . $file . $md5, |
| 238 | 'coreModified' . $file, |
| 239 | 'WordPress core file modified: ' . $file, |
| 240 | "This WordPress core file has been modified and differs from the original file distributed with this version of WordPress.", |
| 241 | array( |
| 242 | 'file' => $file, |
| 243 | 'cType' => 'core', |
| 244 | 'canDiff' => true, |
| 245 | 'canFix' => true, |
| 246 | 'canDelete' => false |
| 247 | ) |
| 248 | ); |
| 249 | } |
| 250 | } |
| 251 | } |
| 252 | } |
| 253 | } else if(isset($this->knownFiles['plugins'][$file])){ |
| 254 | if(in_array($shac, $this->knownFiles['plugins'][$file])){ |
| 255 | $knownFile = 1; |
| 256 | } else { |
| 257 | if($this->pluginsEnabled){ |
| 258 | if(! $this->isSafeFile($shac)){ |
| 259 | $itemName = $this->knownFiles['plugins'][$file][0]; |
| 260 | $itemVersion = $this->knownFiles['plugins'][$file][1]; |
| 261 | $cKey = $this->knownFiles['plugins'][$file][2]; |
| 262 | $this->haveIssues['plugins'] = true; |
| 263 | $this->engine->addIssue( |
| 264 | 'file', |
| 265 | 2, |
| 266 | 'modifiedplugin' . $file . $md5, |
| 267 | 'modifiedplugin' . $file, |
| 268 | 'Modified plugin file: ' . $file, |
| 269 | "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]", |
| 270 | array( |
| 271 | 'file' => $file, |
| 272 | 'cType' => 'plugin', |
| 273 | 'canDiff' => true, |
| 274 | 'canFix' => true, |
| 275 | 'canDelete' => false, |
| 276 | 'cName' => $itemName, |
| 277 | 'cVersion' => $itemVersion, |
| 278 | 'cKey' => $cKey |
| 279 | ) |
| 280 | ); |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | } |
| 285 | } else if(isset($this->knownFiles['themes'][$file])){ |
| 286 | if(in_array($shac, $this->knownFiles['themes'][$file])){ |
| 287 | $knownFile = 1; |
| 288 | } else { |
| 289 | if($this->themesEnabled){ |
| 290 | if(! $this->isSafeFile($shac)){ |
| 291 | $itemName = $this->knownFiles['themes'][$file][0]; |
| 292 | $itemVersion = $this->knownFiles['themes'][$file][1]; |
| 293 | $cKey = $this->knownFiles['themes'][$file][2]; |
| 294 | $this->haveIssues['themes'] = true; |
| 295 | $this->engine->addIssue( |
| 296 | 'file', |
| 297 | 2, |
| 298 | 'modifiedtheme' . $file . $md5, |
| 299 | 'modifiedtheme' . $file, |
| 300 | 'Modified theme file: ' . $file, |
| 301 | "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.", |
| 302 | array( |
| 303 | 'file' => $file, |
| 304 | 'cType' => 'theme', |
| 305 | 'canDiff' => true, |
| 306 | 'canFix' => true, |
| 307 | 'canDelete' => false, |
| 308 | 'cName' => $itemName, |
| 309 | 'cVersion' => $itemVersion, |
| 310 | 'cKey' => $cKey |
| 311 | ) |
| 312 | ); |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | } |
| 317 | } |
| 318 | // knownFile means that the file is both part of core or a known plugin or theme AND that we recognize the file's hash. |
| 319 | // 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. |
| 320 | // 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. |
| 321 | $this->db->queryWrite("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); |
| 322 | |
| 323 | //Now that we know we can open the file, lets update stats |
| 324 | if(preg_match('/\.(?:js|html|htm|css)$/i', $realFile)){ |
| 325 | $this->linesOfJCH += sizeof(file($realFile)); |
| 326 | } else if(preg_match('/\.php$/i', $realFile)){ |
| 327 | $this->linesOfPHP += sizeof(file($realFile)); |
| 328 | } |
| 329 | $this->totalFiles++; |
| 330 | $this->totalData += filesize($realFile); //We already checked if file overflows int in the fileTooBig routine above |
| 331 | if($this->totalFiles % 100 === 0){ |
| 332 | wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data so far"); |
| 333 | } |
| 334 | } else { |
| 335 | //wordfence::status(2, 'error', "Could not gen hash for file (probably because we don't have permission to access the file): $realFile"); |
| 336 | } |
| 337 | } |
| 338 | public static function wfHash($file){ |
| 339 | wfUtils::errorsOff(); |
| 340 | $md5 = @md5_file($file, false); |
| 341 | wfUtils::errorsOn(); |
| 342 | |
| 343 | if(! $md5){ return false; } |
| 344 | $fp = @fopen($file, "rb"); |
| 345 | if(! $fp){ |
| 346 | return false; |
| 347 | } |
| 348 | $ctx = hash_init('sha256'); |
| 349 | while (!feof($fp)) { |
| 350 | hash_update($ctx, str_replace( array("\n","\r","\t"," ") ,"",fread($fp, 65536))); |
| 351 | } |
| 352 | $shac = hash_final($ctx, false); |
| 353 | return array($md5, $shac); |
| 354 | } |
| 355 | private function isMalwarePrefix($hexMD5){ |
| 356 | $binPrefix = pack("H*", substr($hexMD5, 0, 8)); |
| 357 | if(isset($this->malwareData[$binPrefix])){ |
| 358 | return true; |
| 359 | } |
| 360 | return false; |
| 361 | } |
| 362 | private function isSafeFile($shac){ |
| 363 | $result = $this->engine->api->call('is_safe_file', array(), array('shac' => strtoupper($shac))); |
| 364 | if(isset($result['isSafe']) && $result['isSafe'] == 1){ |
| 365 | return true; |
| 366 | } |
| 367 | return false; |
| 368 | } |
| 369 | } |
| 370 | ?> |
| 371 |