<?php /* * @package bfNetwork * @copyright Copyright (C) 2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025 Blue Flame Digital Solutions Ltd. All rights reserved. * @license GNU General Public License version 3 or later * * @see https://mySites.guru/ * @see https://www.phil-taylor.com/ * * @author Phil Taylor / Blue Flame Digital Solutions Limited. * * bfNetwork is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * bfNetwork is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this package. If not, see http://www.gnu.org/licenses/ * * If you have any questions regarding this code, please contact [email protected] */ use Joomla\CMS\Factory; use Joomla\CMS\Version; final class bfAudit { public $_timer; public $_steps; private $db; private $_encryptedAndSuspectIds = []; private $_needsCompatIds = []; private $_encryptedIds = []; private $_suspectIds = []; private $_uploaderIds = []; private $_hackedIds = []; private $_mailerIds = []; private $_notencryptedAndSuspectIds = []; private $alreadyAddedRootDirs = false; private $foundDirs; private $foundFiles; private $suspectfiles; private $noMoreFoldersToScan = false; private $noMoreFilesToScan = false; private $deepscancomplete = false; private $tickOver = 0; private $startTime; private $endTime; private $version; private $platform; private $scancomplete; private $foundRecentlyModifiedFilesTotal; private $hashfailedcount; private $step; private $connectorversion; private $files_777; private $hacked; private $zerobytes; private $folders_777; private $hidden_folders; private $hidden_files; private $renamedtohidefiles; private $nestedinstalls; private $error_logs_seen; private $encrypted_files; private $large_files; private $has_robots_modified; private $user_hasdefaultuserids; private $archive_files; private $htaccess_files; private $phpiniseen; private $uploader; private $mailer; private $max_allowed_packet; private $phpinwrongplace; private $notcorefiles; private $missingcorefiles; private $modifiedfilessincelastaudit; private $tmp_install_folders; private $sqlfilesseen; private $admintoolbreaches; private $dotunderscorefilesseen; private $skipped; private $extensionsjson; private $needscompat; /** * Set up the audit, reading from cached state if needed Also handles the uploading of the scanner config. * * @param stdClass $request The decrypted request */ public function __construct($request) { $this->_cleanUpStuff(); bfLog::log(_BF_SPEED); if (_BF_API_DEBUG === true) { error_reporting(\E_ALL); ini_set('display_errors', 1); } // Check that the permissions are set correctly before proceeding $this->_checkOurPerms(); // Connect to the database $this->initDb(); // get Joomla version $this->version = (new Version())->getShortVersion(); /* * Should we abandon/clear the current audit and restart * * If this is the first time we are running then also reset */ if ((property_exists($request, 'forceRestart') && @$request->forceRestart) || (file_exists( './FIRSTRUN' ) && true === _BF_CONFIG_RESET_STATE_ON_UPGRADE)) { $this->resetState(); } // remove the trigger for the first run if (file_exists('./FIRSTRUN')) { @unlink('./FIRSTRUN'); } // If there is a non encrypted md5's file then import it to the db if (property_exists($request, 'NOTENCRYPTED') && array_key_exists('md5s', $request->NOTENCRYPTED)) { // clean up first $this->db->setQuery('TRUNCATE bf_core_hashes'); $this->db->execute(); $url = base64_decode($request->NOTENCRYPTED['md5s']); $options = [ 'http' => [ 'method' => 'GET', 'header' => "Accept-language: en\r\n" . 'User-Agent: ' . $_SERVER['HTTP_HOST'] . "\r\n", ], ]; $context = stream_context_create($options); // get the data from the request // @ error supressor to hid errors when https:// wrapper is disabled in the server configuration by allow_url_fopen=0 in php.ini $data = @file_get_contents($url, false, $context); // F.M.L - I hate crap servers! if (! $data) { $ch = curl_init(); // Set up bare minimum CURL Options needed for mysites.guru curl_setopt($ch, \CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, \CURLOPT_HEADER, false); curl_setopt($ch, \CURLOPT_URL, $url); curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, \CURLOPT_USERAGENT, $_SERVER['HTTP_HOST']); // Attempt to download using CURL and CURLOPT_SSL_VERIFYPEER set to TRUE $data = curl_exec($ch); // Did we succeed in getting something????? if (! $data) { /* * ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** * * Ok try without validation of the SSL (gulp) but this is needed on some servers without a pem file * and we need to be compatible as possible - even on crappy webhosts when they need us most ;-( */ curl_setopt($ch, \CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, \CURLOPT_SSL_VERIFYHOST, false); // Second Attempt to download using CURL and CURLOPT_SSL_VERIFYPEER set to FALSE (gulp) $data = curl_exec($ch); } curl_close($ch); } if ($data) { if (! function_exists('gzinflate')) { bfEncrypt::reply( bfReply::ERROR, 'Your server doesnt meet the minimum requirements of Joomla - it has no gzinflate function in PHP!' ); } if (! gzinflate($data)) { bfEncrypt::reply( bfReply::ERROR, 'We could not download and inflate a required file from the CDN (Something wrong with the downloaded data or gzinflate of that data) - seek assistance from [email protected]' ); } if (! preg_match('/\<\!DOCTYPE\shtml\>/', gzinflate($data))) { $dataLines = explode("\n", gzinflate($data)); // Import the md5s to the database - easier to query a db than a // single file $sql = 'INSERT INTO bf_core_hashes (filewithpath, hash) VALUES '; $values = []; foreach ($dataLines as $line) { $parts = explode("\t", $line); // Do it this way for speed, 1 query instead of 4000+ queries! if (array_key_exists(1, $parts)) { $values[] = sprintf('("/%s", "%s")', @$parts[0], @$parts[1]); } } // import now! $this->db->setQuery($sql . implode(' , ', $values)); $this->db->execute(); // memory cleanup unset($parts); unset($dataLines); unset($data); } } } // get the base bfnetwork folder $base = __DIR__; // Save our patterns to a file if (property_exists($request, 'NOTENCRYPTED') && array_key_exists('pattern', $request->NOTENCRYPTED)) { bfLog::log('Saving audit pattern config'); $url = base64_decode($request->NOTENCRYPTED['pattern']); $options = [ 'http' => [ 'method' => 'GET', 'header' => "Accept-language: en\r\n" . 'User-Agent: ' . $_SERVER['HTTP_HOST'] . "\r\n", ], ]; $context = stream_context_create($options); // get the data from the request // @ error supressor to hid errors when https:// wrapper is disabled in the server configuration by allow_url_fopen=0 in php.ini $patterns = @file_get_contents($url, false, $context); // F.M.L - I hate crap servers! if (! $patterns) { $ch = curl_init(); // Set up bare minimum CURL Options needed for mysites.guru curl_setopt($ch, \CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, \CURLOPT_HEADER, false); curl_setopt($ch, \CURLOPT_URL, $url); curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, \CURLOPT_USERAGENT, $_SERVER['HTTP_HOST']); // Attempt to download using CURL and CURLOPT_SSL_VERIFYPEER set to TRUE $patterns = curl_exec($ch); // Did we succeed in getting something????? if (! $patterns) { /* * ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** * * Ok try without validation of the SSL (gulp) but this is needed on some servers without a pem file * and we need to be compatible as possible - even on crappy webhosts when they need us most ;-( */ curl_setopt($ch, \CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, \CURLOPT_SSL_VERIFYHOST, false); // Second Attempt to download using CURL and CURLOPT_SSL_VERIFYPEER set to FALSE (gulp) $patterns = curl_exec($ch); } curl_close($ch); } if (! $patterns) { bfEncrypt::reply( bfReply::ERROR, 'We could not download a required file from the CDN (Nothing downloaded!) - seek assistance from [email protected]' ); } if (false === file_put_contents($base . '/tmp/tmp.pattern', $patterns)) { bfEncrypt::reply(bfReply::ERROR, 'Could not save audit patterns to ' . $base . '/tmp/tmp.pattern'); } // finally as a last ditch attempt - ensure its worth running an audit! if (! filesize($base . '/tmp/tmp.pattern')) { bfEncrypt::reply(bfReply::ERROR, 'We have no audit config to run with - this is fatal - seek assistance!'); } } if (property_exists($request, 'NOTENCRYPTED') && array_key_exists('config', $request->NOTENCRYPTED)) { // just in case if (! is_writable($base . '/bfConfig.php')) { // @ error supressor to hid errors when crappy servers dont allow chmod from php @chmod($base . '/bfConfig.php', 0777); } // write config to file file_put_contents($base . '/bfConfig.php', gzinflate(base64_decode($request->NOTENCRYPTED['config']))); // reset permissions to be more secure // @ error supressor to hid errors when crappy servers dont allow chmod from php @chmod($base . '/bfConfig.php', 0644); } // reset permissions - just to be sure! // @ error supressor to hid errors when crappy servers dont allow chmod from php @chmod($base . '/tmp/', 0755); // remove all the request unset($request); bfLog::log('Waking the lab rats from their sleep...'); // Get the current status from the database $this->wakeUp(); // init the timer bfLog::log('Priming the lab rats with a timer...'); $this->_timer = bfTimer::getInstance(); // init the steps bfLog::log('Teaching the lab rats to dance...'); $this->_steps = new STEP($this->step); // belt and braces - check we have a step if (! $this->step) { $this->step = STEP::TESTCONNECTION; } } /** * Remove all the fluff that we need to, Including old crap that we used to have installed and we dont need any * longer. */ private function _cleanUpStuff() { } /** * Checks and sets permissions on files/folders as tight as we can be * depending on the environment this script is running in * - I dont want 0777 but sometimes its required on some stupid environments * :-(. */ private function _checkOurPerms() { // attempt to ensure our tmp folder is writable if (! is_writable(__DIR__ . '/tmp')) { @chmod(__DIR__ . '/tmp', 0755); } // Argh! if (! is_writable(__DIR__ . '/tmp')) { @chmod(__DIR__ . '/tmp', 0777); } // Give Up! if (! is_writable(__DIR__ . '/tmp')) { bfEncrypt::reply(bfReply::ERROR, 'Our ' . __DIR__ . '/tmp folder on your site is not writable!'); } // attempt to ensure our folder is writable if (! is_writable(__DIR__)) { @chmod(__DIR__, 0755); } // Argh! if (! is_writable(__DIR__)) { @chmod(__DIR__, 0777); } // Give Up! if (! is_writable(__DIR__)) { bfEncrypt::reply(bfReply::ERROR, __DIR__ . '/ folder not writeable'); } } /** * Init the Joomla db connection. * * @todo Rip this out and use our own database connection */ private function initDb() { bfLog::log('init database connection...'); // require all we need to access Joomla API if (! defined('BF_JOOMLA_INIT_DONE')) { require_once 'bfInitJoomla.php'; } $this->db = Factory::getContainer()->get('DatabaseDriver'); } /** * Reset the state of our audit, cleaning files and database. */ private function resetState() { bfLog::log('Creating our database tables'); $this->db->setQuery('SHOW TABLES LIKE "bf_files_last"'); if ($this->db->loadResult()) { $this->db->setQuery('DROP TABLE IF EXISTS `bf_files_last`'); $this->db->execute(); } $this->db->setQuery('SHOW TABLES LIKE "bf_files"'); if ($this->db->loadResult()) { $this->db->setQuery('RENAME TABLE `bf_files` TO `bf_files_last`'); $this->db->execute(); $this->db->setQuery('ANALYZE TABLE `bf_files_last`'); $this->db->execute(); } // Drop and recreate our database tables $sql = file_get_contents('./db/blank.sql'); $sqls = explode(';', $sql); foreach ($sqls as $sql) { if ('' != trim($sql)) { $this->db->setQuery($sql); $this->db->execute(); } } // remove any tmp files we might have created @unlink(__DIR__ . '/tmp/tmp.md5s'); @unlink(__DIR__ . '/tmp/tmp.pattern'); @unlink(__DIR__ . '/tmp/tmp.pattern.unenc'); @unlink(__DIR__ . '/tmp/tmp.false'); @unlink(__DIR__ . '/tmp/tmp.log'); @unlink(__DIR__ . '/tmp/tmp.ob'); @unlink(__DIR__ . '/tmp/large.sql'); @unlink(__DIR__ . '/tmp/large1.sql'); @unlink(__DIR__ . '/tmp/large2.sql'); @unlink(__DIR__ . '/tmp/large3.sql'); @unlink(__DIR__ . '/tmp/large4.sql'); @unlink(__DIR__ . '/tmp/large5.sql'); @unlink(__DIR__ . '/tmp/large6.sql'); @unlink(__DIR__ . '/tmp/speedup.sql'); @unlink(__DIR__ . '/tmp/STATE'); @unlink(__DIR__ . '/tmp/STATE.php'); @unlink(__DIR__ . '/tmp/Folders'); @unlink(__DIR__ . '/tmp/Files'); bfLog::truncate(); $this->logStartAudit(); } private function logfinishAudit() { bfActivitylog::getInstance()->log( 'bfNetwork', '', 'Audit Finished', 'onAuditFinished', 'bfNetwork', null, null, '', bfEvents::onAuditFinished, 'onAuditFinished', bfEvents::onAuditFinished ); } private function logStartAudit() { bfActivitylog::getInstance()->log( 'bfNetwork', '', 'Audit Started', 'onAuditStarted', 'bfNetwork', null, null, '', bfEvents::onAuditStarted, 'onAuditStarted', bfEvents::onAuditStarted ); } /** * Wake up the audit from the state files. */ public function wakeUp() { if (! file_exists('tmp/STATE.php')) { return false; } // load state $result = unserialize(str_replace(['<?php die();?>', '<? die();?>'], '', file_get_contents('tmp/STATE.php'))); // Doh! if (! $result) { return; } // populate state into worker foreach ($result as $k => $v) { $this->$k = $v; } } /** * Tick over. */ public function tick() { if (1 != $this->scancomplete) { // init the start of the timer to prevent max time overruns if (! $this->startTime) { $this->startTime = time(); } // increment the ticker, just shows how many ticks we have had ++$this->tickOver; // Run the correct stepAction method $function = $this->_steps->getStepFunction($this->step); bfLog::log('Running method ' . $function); $this->$function(); // sleep and die bfLog::log('Sleeping in tick'); $this->saveState(false, __LINE__); } else { // Scan is already complete! bfLog::log('Sleeping as scan already complete'); $this->saveState(true, __LINE__); } } /** * ZZZzzz...... Sleep state to the database to provide session persistance We need a few seconds to run this :-(. * * @param bool $alreadyComplete */ public function saveState($alreadyComplete = false, $line = 0) { // bfLog::log('Sleeping audit status to persistent db store'); // When did we complete this step/audit $this->endTime = time(); // make sure we cache the connectorversion $this->connectorversion = file_get_contents('./VERSION'); // Inject the state to the database $obj = new stdClass(); foreach ($this as $k => $v) { // Dont save private/system objects if ('db' == $k || '_steps' == $k || '_timer' == $k || '_' == substr($k, 0, 1)) { continue; } // convert objects and arrays to strings if (is_object($v) || is_array($v)) { $v = json_encode($v); } // inject to the object we will return $obj->$k = $v; } // Save state file_put_contents('tmp/STATE.php', '<?php die();?>' . serialize($obj)); // save the step we are on $obj->step = (string) $this->_steps; // report back to service with json object; $obj->maxPHPMemoryUsed = round((memory_get_peak_usage(true) / 1048576), 2); $obj->queuecount = $this->_getQueueCount('files'); // legacy $obj->filestoscan = $obj->queuecount; // Log tail for debugging online // $obj->logtail = bfLog::getTail(); // close db unset($this->db); unset($this->_timer); unset($this->_steps); // go to sleep, but first tell the service we are dreaming... bfEncrypt::reply(bfReply::SUCCESS, $obj); } /** * See whats left in the queue. * * @return int The number of rows */ private function _getQueueCount($tbl) { $this->db->setQuery('SELECT count(*) FROM bf_' . $tbl . ' WHERE queued = 1'); return $this->db->loadResult(); } /** * This simply adds the JPATH_BASE / folders to the scan queue. */ public function scanningrootdirsAction() { // Add the root folder to the scan quque $this->addDirToScanQueue($this->getFolders(JPATH_BASE)); // mark scan $this->alreadyAddedRootDirs = true; // move to the next scan step $this->nextStepPlease(); } /** * Add a folder to the scan queue. * * @param array $arr Array of folders to add to the queue * @param int $queued * * @return array */ private function addDirToScanQueue($arr, $queued = 1) { $insertFolderToDb = []; if (! count($arr)) { return []; } // Update stats $this->foundDirs = $this->foundDirs + count($arr); bfLog::log('Adding ' . count($arr) . ' folders To the audit queue'); // skip if no folders in the array if (false === $arr) { return; } $parts = []; foreach ($arr as $folder) { // clean up $folder = $this->wp_normalize_path($folder); // Dont allow duplicates - ffs $this->db->setQuery(sprintf('SELECT count(*) from bf_folders where folderwithpath = "%s"', $folder)); if ($this->db->loadResult()) { bfLog::log(sprintf('WARNING: Skipping adding %s to db as its already there', $folder)); continue; } // Dont allow blank or invalid folders if (! is_dir($this->wp_normalize_path(JPATH_BASE . \DIRECTORY_SEPARATOR . $folder)) || ! is_dir($this->wp_normalize_path(JPATH_BASE . \DIRECTORY_SEPARATOR . $folder . \DIRECTORY_SEPARATOR)) || is_link($this->wp_normalize_path(JPATH_BASE . \DIRECTORY_SEPARATOR . $folder . \DIRECTORY_SEPARATOR)) || is_link($this->wp_normalize_path(JPATH_BASE . \DIRECTORY_SEPARATOR . $folder)) || ! $folder ) { continue; } $perms = $this->_getFolderPerms($folder); $insertFolderToDb[] = " ( '" . $this->db->escape($folder) . "', '" . $perms . "', " . $queued . ')'; } if ($insertFolderToDb && is_array($insertFolderToDb) && count($insertFolderToDb)) { $sqlprefix = 'INSERT INTO bf_folders ( folderwithpath, folderinfo, queued) VALUES '; $sqlToRun = $sqlprefix . implode(', ', $insertFolderToDb); if (strlen($sqlToRun) > 1048576) { $insertFolderToDb = $this->array_split($insertFolderToDb, 4); $sqlToRun1 = $sqlprefix . implode(', ', $insertFolderToDb[0]); bfLog::log('sql size 1= ' . strlen($sqlToRun1)); $this->db->setQuery($sqlToRun1); $this->db->execute(); $sqlToRun2 = $sqlprefix . implode(', ', $insertFolderToDb[1]); bfLog::log('sql size 2= ' . strlen($sqlToRun2)); $this->db->setQuery($sqlToRun2); $this->db->execute(); $sqlToRun3 = $sqlprefix . implode(', ', $insertFolderToDb[2]); bfLog::log('sql size 3= ' . strlen($sqlToRun3)); $this->db->setQuery($sqlToRun3); $this->db->execute(); $sqlToRun4 = $sqlprefix . implode(', ', $insertFolderToDb[3]); bfLog::log('sql size 4= ' . strlen($sqlToRun4)); $this->db->setQuery($sqlToRun4); $this->db->execute(); } else { $this->db->setQuery($sqlToRun); $this->db->execute(); } } return []; } /** * THANK YOU WORDPRESS !!! I love you xxx. * * @return mixed|string|string[]|null */ private function wp_normalize_path($path) { $path = str_replace('\\', '/', $path); $path = preg_replace('|(?<=.)/+|', '/', $path); if (':' === substr($path, 1, 1)) { $path = ucfirst($path); } // Mine, removes // from the start of a path if ('/' === substr($path, 0, 1) && '/' === substr($path, 1, 1)) { $path = substr($path, 1, strlen($path) - 1); } return $path; } /** * Clean up a string, a path name. Wrapper to wp_normalize_path which does a better job than we did. * * @param string $str * * @return string */ private function _cleanupFileFolderName($str) { return $this->wp_normalize_path($str); } /** * Clean up the folder name and then get the right perms. * * @return string */ private function _getFolderPerms($folder) { $folder = $this->ensureRooted($this->_cleanupFileFolderName($folder)); $perms = substr(decoct(fileperms($folder)), 2); return $perms; } /** * Ensure that we are rooted to the JPATH_BASE. * * @param string $folder A filewithpath * * @return string */ private function ensureRooted($folder) { if (JPATH_BASE === '/' && '/' === substr($folder, 0, 1)) { return $folder; } $str = $this->wp_normalize_path( JPATH_BASE . str_replace($this->wp_normalize_path(JPATH_BASE), '', $this->wp_normalize_path( $this->_cleanupFileFolderName($folder) )) ); return $str; } private function removeJPATHBASE($str) { if (JPATH_BASE === '/' && '/' === substr($str, 0, 1)) { return $str; } return str_replace($this->wp_normalize_path(JPATH_BASE), '', $this->wp_normalize_path($this->ensureRooted($str))); } /** * Spilt an array. * * @param int $pieces * * @return array */ private function array_split($array, $pieces = 2) { if ($pieces < 2) { return [$array]; } $newCount = ceil(count($array) / $pieces); $a = array_slice($array, 0, $newCount); $b = $this->array_split(array_slice($array, $newCount), $pieces - 1); return array_merge([$a], $b); } private function getFolders($folder) { // Initialize variables $arr = []; $folder = trim($folder); if (! is_dir($folder) && ! is_dir($folder . \DIRECTORY_SEPARATOR) || is_link($folder . \DIRECTORY_SEPARATOR) || is_link( $folder ) || ! $folder) { return []; } if (@file_exists($folder . \DIRECTORY_SEPARATOR . '.myjoomla.ignore.folder')) { return []; } $dir = new \DirectoryIterator($folder); foreach ($dir as $fileinfo) { if (! $fileinfo->isDot() && $fileinfo->isDir()) { $arr[] = $this->removeJPATHBASE($folder . \DIRECTORY_SEPARATOR . $fileinfo->getFilename()); } } unset($dir); unset($fileinfo); unset($folder); return $arr; } /** * Function taken from Akeeba filesystem.php. * * @copyright Copyright (c)2009 Nicholas K. Dionysopoulos * @license GNU GPL version 3 or, at your option, any later version * * @version Id: scanner.php 158 2010-06-10 08:46:49Z nikosdion */ private function getFoldersOLD($folder) { // Initialize variables $arr = []; $false = false; $folder = trim($folder); if (! is_dir($folder) && ! is_dir($folder . \DIRECTORY_SEPARATOR) || is_link($folder . \DIRECTORY_SEPARATOR) || is_link( $folder ) || ! $folder) { return $false; } if (@file_exists($folder . \DIRECTORY_SEPARATOR . '.myjoomla.ignore.folder')) { return []; } $handle = @opendir($folder); if (false === $handle) { $handle = @opendir($folder . \DIRECTORY_SEPARATOR); } // If directory is not accessible, just return FALSE if (false === $handle) { return $false; } while ((false !== ($file = @readdir($handle)))) { if (('.' != $file) && ('..' != $file) && (null != trim($file))) { $ds = ('' == $folder) || (\DIRECTORY_SEPARATOR == $folder) || (\DIRECTORY_SEPARATOR == @substr( $folder, -1 )) || (\DIRECTORY_SEPARATOR == @substr($folder, -1)) ? '' : \DIRECTORY_SEPARATOR; $dir = trim($folder . $ds . $file); $isDir = @is_dir($dir); if ($isDir) { $arr[] = $this->removeJPATHBASE($folder . \DIRECTORY_SEPARATOR . $file); } } } @closedir($handle); return $arr; } /** * Set pointer to the next step. */ private function nextStepPlease($alsoSleep = false) { bfLog::log('Ticking over to the next step'); $this->step = $this->_steps->nextStepPlease(); if (true === $alsoSleep) { $this->saveState(false, __LINE__); } } /** * @see http://davidwalsh.name/php-file-extension * * @return string */ public function get_file_extension($file_name) { return substr(strrchr($file_name, '.'), 1); } /** * dummy method. */ private function requestscannerconfigAction() { $this->nextStepPlease(); } /** * I never get here unless all is done :). */ private function completeAction() { // Mark the audit as complete $this->scancomplete = 1; // cleanup @unlink('tmp/tmp.md5s'); @unlink('tmp/tmp.pattern'); @unlink('tmp/tmp.false'); @unlink('tmp/Folders'); @unlink('tmp/Files'); bfLog::log('===== AUDIT COMPLETE ====='); $this->logfinishAudit(); } /** * @deprecated * * Get information about the datbaase */ private function dbinfoAction() { // move onto the next step $this->nextStepPlease(); } /** * Do we have any backup tables. * * @return string */ private function _hasBakTables() { $config = Factory::getApplication()->getConfig(); $dbname = $config->get('db', ''); $this->db->setQuery("SHOW TABLES WHERE `Tables_in_{$dbname}` like 'bak_%'"); return $this->db->loadResult() ? 'TRUE' : 'FALSE'; } private function testconnectionAction() { if (isset($_SERVER['HTTP_HOST']) && (strpos($_SERVER['HTTP_HOST'], 'sg') !== false||strpos($_SERVER['HTTP_HOST'], 'siteground') !== false)) { $this->nextStepPlease(true); } // Ask Joomla API for some settings $config = Factory::getApplication()->getConfig(); try { // Send an email to see if we received it... Tests if the Joomla Global Config mailer settings are correct. $mailer = Factory::getMailer(); $sender = [$config->get('mailfrom'), $config->get('fromname')]; $mailer->setSender($sender); $mailer->addRecipient( '[email protected]' ); // This is not a real mailbox, its a service that reads the body of the email, and lets the mysites.guru service know the domain name. $mailer->setSubject('Audit Mailer Test'); // We do it this way for a reason, catching badly configured proxies and servers $s = ''; if (! empty($_SERVER['HTTPS']) && 'on' == $_SERVER['HTTPS']) { $s = 's'; } $protocol = substr(strtolower($_SERVER['SERVER_PROTOCOL']), 0, strpos(strtolower($_SERVER['SERVER_PROTOCOL']), '/')) . $s; $port = ('80' == $_SERVER['SERVER_PORT']) ? '' : (':' . $_SERVER['SERVER_PORT']); $uri = $protocol . '://' . $_SERVER['SERVER_NAME'] . $port . $_SERVER['REQUEST_URI']; $segments = explode('?', $uri, 2); $url = $segments[0]; $url = str_replace(['plugins/system/bfnetwork/bfAudit.php', 'plugins/system/bfnetwork/bfnetwork/bfAudit.php'], '', $url); $mailer->setBody($url); // ONLY THE URL OF THE SITE IS SENT - NO OTHER DATA $mailer->Send(); } catch (Exception $e) { } // move onto the next step $this->nextStepPlease(); } /** * @deprecated to snapshot */ private function compileextensionsAction() { $this->nextStepPlease(); } private function verifyextensionsAction() { require 'bfExtensions.php'; $ext = new bfExtensions(); $this->extensionsjson = $ext->getExtensions(); $this->nextStepPlease(); } /** * Report on the last 3 days worth of modified files, excluding ours. */ private function lookingupmodifiedfilesAction() { $time = strtotime('-3 days', time()); $sql = "SELECT COUNT(*) FROM bf_files WHERE filemtime > '%s' AND filewithpath NOT LIKE '/plugins/system/bfnetwork%%'"; $this->db->setQuery(sprintf($sql, $time)); $this->foundRecentlyModifiedFilesTotal = $this->db->LoadResult(); // move onto the next step $this->nextStepPlease(); } /** * Scan folders and save the files that we find in the database If we have saved large sql files that contain * queries then run those and tick over. */ private function initialscanningfilesAction() { if (file_exists('tmp/large.sql')) { bfLog::log('Running a cached LARGE SQL insert'); $sql = file_get_contents('tmp/large.sql'); if (trim($sql)) { $this->db->setQuery($sql); try { $this->db->execute(); } catch (\Exception $exception) { bfLog::log('====== SQL ERROR ======'); bfLog::log($exception->getMessage()); bfLog::log($this->db->getQuery()); } } unlink('tmp/large.sql'); $this->saveState(false, __LINE__); } if (file_exists('tmp/large1.sql')) { bfLog::log('Running a cached LARGE1 SQL insert'); $sql = file_get_contents('tmp/large1.sql'); if (trim($sql)) { $this->db->setQuery($sql); try { $this->db->execute(); } catch (\Exception $exception) { bfLog::log('====== SQL ERROR ======'); bfLog::log($exception->getMessage()); bfLog::log($this->db->getQuery()); } } unlink('tmp/large1.sql'); $this->saveState(false, __LINE__); } if (file_exists('tmp/large2.sql')) { bfLog::log('Running a cached LARGE2 SQL insert'); $sql = file_get_contents('tmp/large2.sql'); if (trim($sql)) { $this->db->setQuery($sql); try { $this->db->execute(); } catch (\Exception $exception) { bfLog::log('====== SQL ERROR ======'); bfLog::log($exception->getMessage()); bfLog::log($this->db->getQuery()); } } unlink('tmp/large2.sql'); $this->saveState(false, __LINE__); } if (file_exists('tmp/large3.sql')) { bfLog::log('Running a cached LARGE3 SQL insert'); $sql = file_get_contents('tmp/large3.sql'); if (trim($sql)) { $this->db->setQuery($sql); try { $this->db->execute(); } catch (\Exception $exception) { bfLog::log('====== SQL ERROR ======'); bfLog::log($exception->getMessage()); bfLog::log($this->db->getQuery()); } } unlink('tmp/large3.sql'); $this->saveState(false, __LINE__); } if (file_exists('tmp/large4.sql')) { bfLog::log('Running a cached LARGE4 SQL insert'); $sql = file_get_contents('tmp/large4.sql'); if (trim($sql)) { $this->db->setQuery($sql); try { $this->db->execute(); } catch (\Exception $exception) { bfLog::log('====== SQL ERROR ======'); bfLog::log($exception->getMessage()); bfLog::log($this->db->getQuery()); } } unlink('tmp/large4.sql'); $this->saveState(false, __LINE__); } if (file_exists('tmp/large5.sql')) { bfLog::log('Running a cached LARGE5 SQL insert'); $sql = file_get_contents('tmp/large5.sql'); if (trim($sql)) { $this->db->setQuery($sql); try { $this->db->execute(); } catch (\Exception $exception) { bfLog::log('====== SQL ERROR ======'); bfLog::log($exception->getMessage()); bfLog::log($this->db->getQuery()); } } unlink('tmp/large5.sql'); $this->saveState(false, __LINE__); } if (file_exists('tmp/large6.sql')) { bfLog::log('Running a cached LARGE6 SQL insert'); $sql = file_get_contents('tmp/large6.sql'); if (trim($sql)) { $this->db->setQuery($sql); try { $this->db->execute(); } catch (\Exception $exception) { bfLog::log('====== SQL ERROR ======'); bfLog::log($exception->getMessage()); bfLog::log($this->db->getQuery()); } } unlink('tmp/large6.sql'); $this->saveState(false, __LINE__); } // See how much is left $this->db->setQuery('SELECT COUNT(*) FROM bf_folders WHERE queued = 1'); $totalLeft = $this->db->loadResult(); // re-set the sql because mysqlpdo in Joomla borks when trying to run loadResult twice, with 0 - 00000, , :-( // Time wasted: days and days and days... $this->db->setQuery('SELECT COUNT(*) FROM bf_folders WHERE queued = 1'); // Nothing left so die if (! $totalLeft) { // Get all the files with core hash changes :-( $sql = 'SELECT f.id FROM bf_files AS f LEFT JOIN bf_core_hashes AS ch ON ch.filewithpath = f.filewithpath WHERE ch.hash != f.currenthash AND f.currenthash != "Unable To Calc Hash" AND f.currenthash != "Too Big To Hash" '; $this->db->setQuery($sql); if (method_exists($this->db, 'loadColumn')) { $ids = $this->db->loadColumn(); } else { $ids = $this->db->loadResultArray(); } if ($ids && count($ids)) { bfLog::log('Found ' . count($ids) . ' Core file hashes failed'); $sql = 'UPDATE bf_files SET hashfailed = 1 WHERE id IN (' . implode(', ', $ids) . ')'; file_put_contents('tmp/hashfailed.sql', $sql); } // set all the core file flags $sql = 'SELECT f.id FROM bf_files AS f WHERE filewithpath IN( SELECT filewithpath FROM bf_core_hashes )'; $this->db->setQuery($sql); if (method_exists($this->db, 'loadColumn')) { $ids = $this->db->loadColumn(); } else { $ids = $this->db->loadResultArray(); } if ($ids && count($ids)) { bfLog::log('Matched ' . count($ids) . ' Core files'); $sql = 'UPDATE bf_files SET iscorefile = 1 WHERE id IN (' . implode(', ', $ids) . ')'; file_put_contents('tmp/corefiles.sql', $sql); } $this->noMoreFilesToScan = true; $this->nextStepPlease(true); } $removeFoldersFromQueueIds = []; // yes run the query again, allows for the while loop nicely, also only // loop while we have time while ($this->db->loadResult() > 0 && $this->_timer->getTimeLeft() > _BF_CONFIG_FILES_TIMER_ONE) { // ok so we have a load of folders... if ($removeFoldersFromQueueIds && count($removeFoldersFromQueueIds)) { $this->db->setQuery( 'SELECT id, folderwithpath FROM bf_folders WHERE queued = 1 AND id NOT IN (' . implode( ', ', $removeFoldersFromQueueIds ) . ') ORDER BY id ASC LIMIT ' . _BF_CONFIG_FILES_COUNT_ONE ); } else { $this->db->setQuery( 'SELECT id, folderwithpath FROM bf_folders WHERE queued = 1 ORDER BY id ASC LIMIT ' . _BF_CONFIG_FILES_COUNT_ONE ); } $dirs_to_scan = $this->db->loadObjectList(); while (count($dirs_to_scan) && $this->_timer->getTimeLeft() > _BF_CONFIG_FILES_TIMER_ONE) { // sql values to imploe to the insert $sqlvalues = []; // get a diretory object to scan $dirToScanObj = array_pop($dirs_to_scan); // extract the folder $dirToScan = $dirToScanObj->folderwithpath; $dirToScan = str_replace('////', '/', $dirToScan); // remove this current dir form the scan queue - quickly incase we get into indefinite loop; //$this->removeFromQueue('folders', array($dirToScanObj->id)); $removeFoldersFromQueueIds[] = $dirToScanObj->id; // Make sure we have a absolute path to the folder $dirToScanWithPath = $this->ensureRooted($dirToScan); $filesInThisFolder = $this->getFiles($dirToScanWithPath); bfLog::log( 'Found ' . (is_array($filesInThisFolder) || $filesInThisFolder instanceof \Countable ? count( $filesInThisFolder ) : 0) . ' files in ' . $this->removeJPATHBASE($dirToScan) ); // If there are any files, and we have time left if ($filesInThisFolder && count($filesInThisFolder) && $this->_timer->getTimeLeft() > _BF_CONFIG_FILES_TIMER_TWO) { // for each file then get the info foreach ($filesInThisFolder as $file) { // ok are we getting short of time yet? if ($this->_timer->getTimeLeft() <= _BF_CONFIG_FILES_TIMER_TWO) { $this->db->setQuery('/*6*/ INSERT INTO bf_files (filewithpath, fileperms, filemtime, currenthash, size) VALUES ' . implode(', ', $sqlvalues)); $this->db->execute(); $this->removeFromQueue('folders', $removeFoldersFromQueueIds); $this->saveState(false, __LINE__); } // with full path $fileBase = $this->_cleanupFileFolderName($this->ensureRooted($dirToScanWithPath) . \DIRECTORY_SEPARATOR . $file); // Get the file Info... $fileInfo = $this->_getFileInfo($fileBase); // with no JPATH_BASE $fileBase = $this->removeJPATHBASE($fileBase); $fileBase = str_replace('"', '\"', $fileBase); // create the insert $sqlinsert = ' ("%s", "%s", "%s", "%s", "%s") '; // cache the insert so that we can insert many rows for performance $sqlvalues[] = sprintf( $sqlinsert, $fileBase, $fileInfo['perms'], $fileInfo['mtime'], $fileInfo['currenthash'], $fileInfo['size'] ); // count ++$this->foundFiles; } if (count($filesInThisFolder) > 200) { bfLog::log('Sleeping as we had more than 200 files in this folder... we are saving: ' . count($filesInThisFolder)); $sqlvaluesParts = $this->array_split($sqlvalues, 6); file_put_contents( 'tmp/large1.sql', '/*1*/ INSERT INTO bf_files (filewithpath, fileperms, filemtime, currenthash, size) VALUES ' . implode(', ', $sqlvaluesParts[0]) ); file_put_contents( 'tmp/large2.sql', '/*2*/ INSERT INTO bf_files (filewithpath, fileperms, filemtime, currenthash, size) VALUES ' . implode(', ', $sqlvaluesParts[1]) ); file_put_contents( 'tmp/large3.sql', '/*3*/ INSERT INTO bf_files (filewithpath, fileperms, filemtime, currenthash, size) VALUES ' . implode(', ', $sqlvaluesParts[2]) ); file_put_contents( 'tmp/large4.sql', '/*4*/ INSERT INTO bf_files (filewithpath, fileperms, filemtime, currenthash, size) VALUES ' . implode(', ', $sqlvaluesParts[3]) ); file_put_contents( 'tmp/large5.sql', '/*5*/ INSERT INTO bf_files (filewithpath, fileperms, filemtime, currenthash, size) VALUES ' . implode(', ', $sqlvaluesParts[4]) ); file_put_contents( 'tmp/large6.sql', '/*6*/ INSERT INTO bf_files (filewithpath, fileperms, filemtime, currenthash, size) VALUES ' . implode(', ', $sqlvaluesParts[5]) ); bfLog::log('Large SQL files stored for processing...'); $this->removeFromQueue('folders', $removeFoldersFromQueueIds); $this->saveState(false, __LINE__); } // Save to the database when we get short of time if ($this->_timer->getTimeLeft() <= _BF_CONFIG_FILES_TIMER_TWO) { try { $this->db->setQuery('/*5*/INSERT INTO bf_files (filewithpath, fileperms, filemtime, currenthash, size) VALUES ' . implode(', ', $sqlvalues)); $this->db->execute(); } catch (\Exception $exception) { bfLog::log('====== SQL ERROR ======'); bfLog::log($exception->getMessage()); bfLog::log($this->db->getQuery()); } $sqlvalues = []; $this->removeFromQueue('folders', $removeFoldersFromQueueIds); $this->saveState(false, __LINE__); } } // Save to the database if (is_array($sqlvalues) && count($sqlvalues)) { try { $this->db->setQuery('/*7*/INSERT INTO bf_files (filewithpath, fileperms, filemtime, currenthash, size) VALUES ' . implode(', ', $sqlvalues)); $this->db->execute(); } catch (\Exception $exception) { bfLog::log('====== SQL ERROR ======'); bfLog::log($exception->getMessage()); bfLog::log($this->db->getQuery()); } $sqlvalues = []; } // set up for the while loop again if (count($removeFoldersFromQueueIds)) { $this->db->setQuery( 'SELECT count(*) FROM bf_folders WHERE queued = 1 AND id NOT IN (' . implode(', ', $removeFoldersFromQueueIds) . ')' ); } else { $this->db->setQuery('SELECT COUNT(*) FROM bf_folders WHERE queued = 1'); } // are we nearly there yet? if ($this->_timer->getTimeLeft() <= _BF_CONFIG_FILES_TIMER_TWO) { $this->removeFromQueue('folders', $removeFoldersFromQueueIds); $this->saveState(false, __LINE__); } } } $this->removeFromQueue('folders', $removeFoldersFromQueueIds); } private function getFiles($folder) { // Initialize variables $arr = []; $folder = trim($folder); if (! is_dir($folder) && ! is_dir($folder . \DIRECTORY_SEPARATOR) || is_link($folder . \DIRECTORY_SEPARATOR) || is_link( $folder ) || ! $folder) { return $arr; } if (@file_exists($folder . \DIRECTORY_SEPARATOR . '.myjoomla.ignore.files')) { return $arr; } $dir = new \DirectoryIterator($folder); foreach ($dir as $fileinfo) { if (! $fileinfo->isDot() && $fileinfo->isFile()) { $arr[] = $this->_cleanupFileFolderName($fileinfo->getFilename()); } } unset($dir); unset($fileinfo); unset($folder); return $arr; } /** * @param array $updateIds * * @return array */ private function removeFromQueue($tbl, $updateIds = []) { if (count($updateIds)) { bfLog::log('Removing ' . count($updateIds) . ' ' . $tbl . ' from the queue'); $sql = 'UPDATE bf_' . $tbl . ' SET queued = 0 WHERE id IN (' . implode(', ', $updateIds) . ')'; $this->db->setQuery($sql); $this->db->execute(); } return []; } /** * There are a lot of error supression @'s in this method, mainly to handle fringe cases where we dont have * permissions or some other fringe error happens. * * @param string $file * * @return array */ private function _getFileInfo($file) { // clean up $file = stripslashes($this->_cleanupFileFolderName($file)); $fileInfo = []; // Get the File Permissions - if we are allowed (Hence the @) $fileInfo['perms'] = @substr(@decoct(@fileperms($file)), 2); // Get the File Modification Time - if we are allowed (Hence the @) $fileInfo['mtime'] = @filemtime($file); // Get the File Size - if we are allowed (Hence the @) $size = @filesize($file); $fileInfo['size'] = $size; if (! $fileInfo['size']) { $fileInfo['size'] = '0'; } // Out of range value for column 'size' when finding a huge file like a backup if ($fileInfo['size'] > 2147483640) { $fileInfo['size'] = 2147483640; } // only hash small files if ($size < 2097152) { // 2 megabyte = 2,097,152 bytes // We need a @ incase of "failed to // open stream: Permission denied" $hash = @md5_file($file); // something went wrong if (! $hash) { $hash = 'Unable To Calc Hash'; } } else { $hash = 'Too Big To Hash'; } // save the has $fileInfo['currenthash'] = $hash; return $fileInfo; } /** * Scan folders and find more files in them. */ private function initialscanningfoldersAction() { $deleteFoldersIds = []; $addToScanQueue = []; $break = false; $count = 0; // See if we have any folders to scan $this->db->setQuery('SELECT COUNT(*) FROM bf_folders WHERE queued = 1'); $totalLeft = $this->db->loadResult(); // re-set the sql because mysqlpdo in Joomla borks when trying to run loadResult twice, with 0 - 00000, , :-( // Time wasted: days and days and days... $this->db->setQuery('SELECT COUNT(*) FROM bf_folders WHERE queued = 1'); if (! $totalLeft) { $addToScanQueue = $this->addDirToScanQueue(['/'], 0); $this->toggleQueued('folders', 1); // move on, and die $this->noMoreFoldersToScan = true; $this->nextStepPlease(true); } // We have some folders to look into and we have some time left while ($this->db->loadResult() > 0 && $this->_timer->getTimeLeft() > _BF_CONFIG_FOLDERS_TIMER_ONE) { // ok so we have a load of folders... if (count($deleteFoldersIds)) { $this->db->setQuery( 'SELECT id, folderwithpath FROM bf_folders WHERE queued = 1 AND id NOT IN (' . implode( ',', $deleteFoldersIds ) . ') ORDER BY id ASC LIMIT ' . _BF_CONFIG_FOLDERS_COUNT_ONE ); } else { $this->db->setQuery( 'SELECT id, folderwithpath FROM bf_folders WHERE queued = 1 ORDER BY id ASC LIMIT ' . _BF_CONFIG_FOLDERS_COUNT_ONE ); } $dirs_to_scan = $this->db->loadObjectList(); while (count($dirs_to_scan) && $this->_timer->getTimeLeft() > _BF_CONFIG_FOLDERS_TIMER_ONE) { $dirToScanObj = array_pop($dirs_to_scan); $dirToScan = $this->wp_normalize_path($dirToScanObj->folderwithpath); // Redundant? if ($this->_timer->getTimeLeft() <= _BF_CONFIG_FOLDERS_TIMER_TWO) { $addToScanQueue = $this->addDirToScanQueue($addToScanQueue); $deleteFoldersIds = $this->removeFromQueue('folders', $deleteFoldersIds); $this->saveState(false, __LINE__); // Exits with reply } // Get the subdirectories in this folder and add to the list of folders to scan enforce only from base root... $dirToScanWithPath = $this->ensureRooted($dirToScan); // but if our$dirToScanWithPath is now blank it means a path of /var/www/var/www ! if (JPATH_BASE == $dirToScanWithPath) { // need this else we loop when /home/public_html/home/public_html is found!! $dirToScanWithPath = $this->wp_normalize_path(JPATH_BASE . $dirToScan); } $subDirectorys = $this->getFolders($dirToScanWithPath); bfLog::log( 'Found ' . (is_array($subDirectorys) || $subDirectorys instanceof \Countable ? count( $subDirectorys ) : 0) . ' subfolders in ' . $this->removeJPATHBASE($dirToScan) ); if ((is_array($subDirectorys) || $subDirectorys instanceof \Countable) && count($subDirectorys)) { foreach ($subDirectorys as $folder) { $folder = $this->wp_normalize_path($folder); $addToScanQueue[] = $folder; } } else { bfLog::log('Found NO subfolders in ' . $this->removeJPATHBASE($dirToScan)); } $deleteFoldersIds[] = $dirToScanObj->id; if ((count($deleteFoldersIds) > 1000) || (count( $addToScanQueue ) > 1000) || $this->_timer->getTimeLeft() <= _BF_CONFIG_FOLDERS_TIMER_TWO) { // remove this current dir form the scan queue; sleep(2); $addToScanQueue = $this->addDirToScanQueue($addToScanQueue); $deleteFoldersIds = $this->removeFromQueue('folders', $deleteFoldersIds); $this->saveState(false, __LINE__); // Exits with reply } } $this->db->setQuery( 'SELECT count(*) FROM bf_folders WHERE queued = 1 AND id NOT IN (' . implode(',', $deleteFoldersIds) . ')' ); } $this->addDirToScanQueue($addToScanQueue); $this->removeFromQueue('folders', $deleteFoldersIds); $this->saveState(false, __LINE__); // Exits with reply } private function toggleQueued($tblSuffix, $queued) { $sql = 'UPDATE bf_' . $tblSuffix . ' SET queued = ' . $queued; $this->db->setQuery($sql); $this->db->execute(); } /** * Deep scan. */ private function deepscanAction() { $doSpeedup = null; if (file_exists('tmp/corefiles.sql')) { bfLog::log('Found core files - marking them as such'); $this->db->setQuery(file_get_contents('tmp/corefiles.sql')); $this->db->execute(); unlink('tmp/corefiles.sql'); $this->saveState(false, __LINE__); } if (file_exists('tmp/hashfailed.sql')) { bfLog::log('Found modified core files - adding to deepscan'); $this->db->setQuery(file_get_contents('tmp/hashfailed.sql')); $this->db->execute(); unlink('tmp/hashfailed.sql'); $this->saveState(false, __LINE__); } if (file_exists(__DIR__ . '/tmp/currentlyAuditingFile.php')) { bfLog::log('Found currentlyAuditingFile - setting it skipped'); $file = explode('||', file_get_contents(__DIR__ . '/tmp/currentlyAuditingFile.php'))[1]; $this->db->setQuery('UPDATE bf_files SET queued = 0, skipped = 1 WHERE filewithpath = "' . $file . '"'); $this->db->execute(); $this->skipped = $this->skipped + 1; unlink(__DIR__ . '/tmp/currentlyAuditingFile.php'); } try { // if we are not complete $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE queued = 1'); $queueCount = $this->db->loadResult(); if (0 == $queueCount && true == $this->deepscancomplete) { // left here in case needed again. } if (! $queueCount && ! $this->deepscancomplete) { bfLog::log('Adding files to the scan queue'); /** * Yes I know that this sql gives places for hackers to hide, however its based on extensive, active, * experience and is often changed to reflect current trends. */ $sql = "UPdATE bf_files SET queued = 1 WHERE ( SIZE < 1000000 -- 1mb Limit AND SIZE > 0 -- Must have content! ) AND ( iscorefile = 0 -- No Core Files OR iscorefile IS NULL -- No Core Files - NULL !== 0 -- IDIOT PHIL!!!! OR hashfailed = 1 -- Or core files which hash has failed ) AND -- Filter out the probably ok file extensions and other stuff we are 'fairly' happy to ignore ( currenthash != 'f96aa8838dffa02a4a8438f2a8596025' -- ok blank index.html file AND filewithpath NOT LIKE '/plugins/system/bfnetwork%' -- Our stuff AND filewithpath NOT LIKE '%.DS_Store%' -- Mac finder files AND filewithpath NOT LIKE '%.zip' -- cant preg_match inside a zip! AND filewithpath NOT LIKE '%.gzip' -- cant preg_match inside a zip! AND filewithpath NOT LIKE '%.gz' -- cant preg_match inside a zip! AND filewithpath NOT LIKE '%.doc' AND filewithpath NOT LIKE '%.dat' -- dat used by JCH Optimize, never seen anything bad in dat files AND filewithpath NOT LIKE '%.docx' AND filewithpath NOT LIKE '%.xls' AND filewithpath NOT LIKE '%.ppt' AND filewithpath NOT LIKE '%.pdf' AND filewithpath NOT LIKE '%.rtf' -- never seen anything bad in a rtf AND filewithpath NOT LIKE '%.mno' AND filewithpath NOT LIKE '%.ashx' AND filewithpath NOT LIKE '%.psd' -- Photoshop, normally a massive file too AND filewithpath NOT LIKE '%.wott' -- font file AND filewithpath NOT LIKE '%.ttf' -- font file AND filewithpath NOT LIKE '%.css' -- plain text css, never seen Joomla hack in css file AND filewithpath NOT LIKE '%.swf' -- flash AND filewithpath NOT LIKE '%.flv' -- flash AND filewithpath NOT LIKE '%.po' -- language files AND filewithpath NOT LIKE '%.mo' AND filewithpath NOT LIKE '%.pot' AND filewithpath NOT LIKE '%.eot' AND filewithpath NOT LIKE '%.webp' AND filewithpath NOT LIKE '%.ini' AND filewithpath NOT LIKE '%.svg' AND filewithpath NOT LIKE '%.mpeg' -- No need to audit inside audio files, never seen a Joomla hack in these AND filewithpath NOT LIKE '%.mvk' -- No need to audit inside audio files, never seen a Joomla hack in these AND filewithpath NOT LIKE '%.mp3' -- No need to audit inside audio files, never seen a Joomla hack in these AND filewithpath NOT LIKE '%.less' AND filewithpath NOT LIKE '%.sql' AND filewithpath NOT LIKE '%.wsdl' AND filewithpath NOT LIKE '%.woff' AND filewithpath NOT LIKE '%.woff2' AND filewithpath NOT LIKE '%.otf' AND filewithpath NOT LIKE '%.xml' -- never seen a hack in an xml file AND filewithpath NOT LIKE '%.php_expire' -- Expired cache file AND filewithpath NOT LIKE '%.jp' -- Akeeba backup files AND filewithpath NOT LIKE '%/akeeba_json.%' -- Akeeba json state file AND filewithpath NOT LIKE '%/administrator/components/com_akeeba/backup/akeeba%' -- Akeeba json state file AND filewithpath NOT LIKE '%/administrator/components/com_akeebabackup/backup/akeeba%' -- Akeeba json state file AND filewithpath NOT LIKE '%/akeeba_%' -- Akeeba json state file AND filewithpath NOT LIKE '%/cacert.pem' -- cacert.pem AND filewithpath NOT LIKE '%/GeoIP.dat' -- never seen a hack in an GeoIP.dat file but the one in RSFirewall/Admin Tools kills the audit :-( AND filewithpath NOT LIKE '%/ca-certificates.crt' -- never seen a hack in an ca-certificates.crt file but the one in RSFirewall/Admin Tools kills the audit :-( AND filewithpath NOT LIKE '%error_log' -- PHP error logs, we alert to ALL these in another check AND filewithpath NOT LIKE '%/stats/webalizer.current' -- Crappy file AND filewithpath NOT LIKE '%/stats/usage_%.html' -- Crappy file AND filewithpath NOT LIKE '%/components/libraries/cmslib/cache/cache__%' -- Massive folder of cache files AND filewithpath NOT LIKE '%/plugins/system/akgeoip/lib/vendor/guzzle/guzzle/%' -- Akeeba GeoIP Docs AND filewithpath NOT LIKE '%/components/com_jce/editor/tiny_mce/plugins/code/img/icons.gif' -- JCE Code icons AND filewithpath NOT LIKE '%/components/com_jce/editor/libraries/js/pdf.js' -- JCE PDF JS 900kb+ )"; $this->db->setQuery($sql); $this->db->execute(); // DEQUEUE known clean files not changed bfLog::log('DONE Adding files to the scan queue db table'); bfLog::log('Retrieving global whitelist from cdn'); $url = 'https://cdn.mysites.guru/public/global/whitelist'; $options = [ 'http' => [ 'method' => 'GET', 'header' => "Accept-language: en\r\n" . 'User-Agent: ' . $_SERVER['HTTP_HOST'] . "\r\n", ], ]; $context = stream_context_create($options); // get the data from the request $whitelist = file_get_contents($url, false, $context); // F.M.L - I hate crap servers! if (! $whitelist) { $ch = curl_init(); // Set up bare minimum CURL Options needed for mysites.guru curl_setopt($ch, \CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, \CURLOPT_HEADER, false); curl_setopt($ch, \CURLOPT_URL, $url); curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, \CURLOPT_USERAGENT, $_SERVER['HTTP_HOST']); // Attempt to download using CURL and CURLOPT_SSL_VERIFYPEER set to TRUE $whitelist = curl_exec($ch); // Did we succeed in getting something????? if (! $whitelist) { /* * ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** * * Ok try without validation of the SSL (gulp) but this is needed on some servers without a pem file * and we need to be compatible as possible - even on crappy webhosts when they need us most ;-( */ curl_setopt($ch, \CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, \CURLOPT_SSL_VERIFYHOST, false); // Second Attempt to download using CURL and CURLOPT_SSL_VERIFYPEER set to FALSE (gulp) $whitelist = curl_exec($ch); } curl_close($ch); } if (! $whitelist) { bfEncrypt::reply( bfReply::ERROR, 'We could not download a required file from the CDN (w) - seek assistance from [email protected]' ); } $whitelistSQL = 'UPDATE bf_files SET queued = 0, falsepositive = 1 WHERE currenthash IN (' . $whitelist . ')'; $this->db->setQuery($whitelistSQL); bfLog::log('Applying global whitelist from cdn to db'); $this->db->execute(); bfLog::log('Global whitelist applied!'); bfLog::log('Retrieving global hacklist from cdn'); $url = 'https://cdn.mysites.guru/public/global/hacklist'; $options = [ 'http' => [ 'method' => 'GET', 'header' => "Accept-language: en\r\n" . 'User-Agent: ' . $_SERVER['HTTP_HOST'] . "\r\n", ], ]; $context = stream_context_create($options); // get the data from the request $hacklist = file_get_contents($url, false, $context); // F.M.L - I hate crap servers! if (! $hacklist) { $ch = curl_init(); // Set up bare minimum CURL Options needed for mysites.guru curl_setopt($ch, \CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, \CURLOPT_HEADER, false); curl_setopt($ch, \CURLOPT_URL, $url); curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, \CURLOPT_USERAGENT, $_SERVER['HTTP_HOST']); // Attempt to download using CURL and CURLOPT_SSL_VERIFYPEER set to TRUE $hacklist = curl_exec($ch); // Did we succeed in getting something????? if (! $hacklist) { /* * ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** CRAPPY SERVER ALERT ** * * Ok try without validation of the SSL (gulp) but this is needed on some servers without a pem file * and we need to be compatible as possible - even on crappy webhosts when they need us most ;-( */ curl_setopt($ch, \CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, \CURLOPT_SSL_VERIFYHOST, false); // Second Attempt to download using CURL and CURLOPT_SSL_VERIFYPEER set to FALSE (gulp) $hacklist = curl_exec($ch); } curl_close($ch); } if (! $hacklist) { bfEncrypt::reply( bfReply::ERROR, 'We could not download a required file from the CDN (h) - seek assistance from [email protected]' ); } $hacklistSQL = 'UPDATE bf_files SET queued = 0, suspectcontent = 1, hacked = 1 WHERE currenthash IN (' . $hacklist . ')'; $this->db->setQuery($hacklistSQL); bfLog::log('Applying global hacklist from cdn to db'); $this->db->execute(); bfLog::log('Global hacklist applied!'); // Ensure we have at least 1 $this->db->setQuery('UPdATE bf_files SET queued = 1 LIMIT 5'); $this->db->execute(); // Cache our patterns $pattern = file_get_contents('tmp/tmp.pattern'); if (file_exists('tmp/tmp.pattern.lastmd5')) { $lastPatternmd5 = file_get_contents('tmp/tmp.pattern.lastmd5'); } else { $lastPatternmd5 = ''; } // if encrypted - decrypt if ('RC4:' == substr($pattern, 0, 4)) { $pattern = base64_decode(substr($pattern, 4, strlen($pattern) - 4)); $RC4 = new Crypt_RC4(); $RC4->setKey('NotMeantToBeSecure'); // just to hide from other server side scanners $pattern = $RC4->decrypt($pattern); $patternChunks = explode('|__SPLIT_SEPARATOR__|', $pattern); /* * When developing/debugging we might want to cache the unencrypted patterns * We don't do this normally because some webhost scanners see them as hacks * when they are not! */ //file_put_contents('tmp/tmp.pattern.unenc', $pattern); //sss bfLog::log('LAST PATTERN TEST = ' . $lastPatternmd5 . '==' . md5($pattern)); if ($lastPatternmd5 == md5($pattern)) { bfLog::log('SPEEDUP - Yes, we will speedup'); $doSpeedup = true; } else { bfLog::log('SPEEDUP - No, we will not speedup'); $doSpeedup = false; } } if ($doSpeedup && (_BF_SPEED == 'DEFAULT' || _BF_SPEED == 'FAST')) { $this->db->setQuery('SHOW TABLES LIKE "bf_files_last"'); if ($this->db->loadResult()) { $speedupSQL = 'UPDATE bf_files AS NEWTABLE INNER JOIN ( SELECT bf_files_last.filewithpath, bf_files_last.suspectcontent, bf_files_last.falsepositive, bf_files_last.encrypted FROM bf_files_last LEFT JOIN bf_files ON bf_files_last.filewithpath = bf_files.filewithpath WHERE bf_files_last.currenthash = bf_files.currenthash AND bf_files_last.filemtime = bf_files.filemtime AND bf_files_last.fileperms = bf_files.fileperms AND bf_files_last.filewithpath = bf_files.filewithpath ) AS OLDTABLE ON NEWTABLE.filewithpath = OLDTABLE.filewithpath SET NEWTABLE.filewithpath = OLDTABLE.filewithpath, NEWTABLE.suspectcontent = OLDTABLE.suspectcontent, NEWTABLE.falsepositive = OLDTABLE.falsepositive, NEWTABLE.encrypted = OLDTABLE.encrypted, NEWTABLE.needscompat = OLDTABLE.needscompat, NEWTABLE.queued = 0 WHERE OLDTABLE.suspectcontent != 1 '; bfLog::log('SPEEDUP - saving the sql to run for the speedup'); file_put_contents('tmp/speedup.sql', $speedupSQL); } } // ok this took a lot of time, so to be careful we will re-tick... $this->saveState(false, __LINE__); } $pattern = file_get_contents('tmp/tmp.pattern'); // if encrypted - decrypt if ('RC4:' == substr($pattern, 0, 4)) { $pattern = base64_decode(substr($pattern, 4, strlen($pattern) - 4)); $RC4 = new Crypt_RC4(); $RC4->setKey('NotMeantToBeSecure'); // just to hide from other server side scanners $pattern = $RC4->decrypt($pattern); $patternChunks = explode('|__SPLIT_SEPARATOR__|', $pattern); } if (file_exists('tmp/speedup.sql') && (_BF_SPEED == 'DEFAULT' || _BF_SPEED == 'FAST')) { bfLog::log('SPEEDUP Found speedup sql files - removing files to deepscan'); // get contents and delete the file quickly in case the sql fails and then we dont end up in a loop $speedSQL = file_get_contents('tmp/speedup.sql'); bfLog::log('SPEEDUP - removing speedup file after applying it'); unlink('tmp/speedup.sql'); $this->db->setQuery('SHOW TABLES LIKE "bf_files_last"'); if ($this->db->loadResult()) { $this->db->setQuery('ANALYZE TABLE `bf_files_last`'); $this->db->execute(); $this->db->setQuery($speedSQL); try { $this->db->execute(); } catch (\Exception $exception) { } } // force at least one file to be audited to prevent broken loop $this->db->setQuery('UPDATE bf_files SET queued = 1 LIMIT 5'); $this->db->execute(); $this->saveState(false, __LINE__); } else { if (file_exists(__DIR__ . '/tmp/speedup.sql')) { bfLog::log('SPEEDUP - removing speedup file without applying it as _BF_SPEED = ' . _BF_SPEED); @unlink(__DIR__ . '/tmp/speedup.sql'); } } $this->db->setQuery('SELECT COUNT(*) FROM bf_files'); if (! $this->db->loadResult()) { bfLog::log('NO FILES!!!'); bfEncrypt::reply( bfReply::ERROR, 'No files to audit - please seek support from [email protected] as something is drastically wrong on your server' ); } // A nice while loop while we have time left if (count($this->getmergedIds())) { $this->db->setQuery( 'SELECT count(*) FROM bf_files WHERE queued = 1 AND id NOT IN (' . implode(',', $this->getmergedIds()) . ')' ); } else { $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE queued = 1'); } while ($this->db->loadResult() > 0 && $this->_timer->getTimeLeft() > _BF_CONFIG_DEEPSCAN_TIMER_ONE) { // ok so we have a load of files.... if (count($this->getmergedIds())) { $this->db->setQuery( 'SELECT * FROM bf_files WHERE queued = 1 AND id NOT IN (' . implode( ',', $this->getmergedIds() ) . ') ORDER BY id ASC LIMIT ' . _BF_CONFIG_DEEPSCAN_COUNT_ONE ); } else { bfLog::log('Reconnecting to the database to prevent gone away messages'); $this->db->disconnect(); $this->db->connect(); $this->db->setQuery('SELECT * FROM bf_files WHERE queued = 1 ORDER BY id ASC LIMIT ' . _BF_CONFIG_DEEPSCAN_COUNT_ONE); } $files_to_scan = $this->db->loadObjectList(); // still, while we have files and time left while (count($files_to_scan) && $this->_timer->getTimeLeft() > _BF_CONFIG_DEEPSCAN_TIMER_ONE) { // get one file $file_to_scan = array_pop($files_to_scan); /** * If this is a large JS file like jquery.ui.src.js then we need more time dammit! Also need to do * this on WSDL files for some reason. */ $size = filesize(JPATH_BASE . $file_to_scan->filewithpath); if (( (strpos($file_to_scan->filewithpath, 'wsdl') || strpos($file_to_scan->filewithpath, 'jquery')) && $this->_timer->getTimeLeft() < 4 ) || ($size > 100000 && (_BF_SPEED != 'DEFAULT' && _BF_SPEED != 'FAST') && $this->_timer->getTimeLeft() < 4) ) { bfLog::log( 'Next file is a problematic JS/wsdl is of size ' . $size . ' and we only had ' . $this->_timer->getTimeLeft() . ' left so we are Zzz... and will come back next tick' ); $this->updateFilesFromDeepscan(__LINE__); $this->saveState(false, __LINE__); } // toggle if we can safely skip it } else if $skip = 0; // Is this a suspect file // Is this file encrypted // is this file an uploader // is this file a mailer $isSuspect = false; $encrypted = false; $isUploader = false; $isMailer = false; $isHacked = false; $needscompat = false; $file_extension = strtolower(pathinfo(JPATH_BASE . $file_to_scan->filewithpath, \PATHINFO_EXTENSION)); // If the file no longer exists then skip if (! file_exists(JPATH_BASE . $file_to_scan->filewithpath)) { bfLog::log('SKIP: FILE WAS SKIPPED AS DOES NOT EXIST!!! ' . $file_to_scan->filewithpath); $skip = -1; } elseif ('gif' == $file_extension) { if ($this->is_ani(JPATH_BASE . $file_to_scan->filewithpath)) { bfLog::log('SKIP: FILE WAS ANIMATED GIF - skipping ' . $file_to_scan->filewithpath); $skip = -2; } } elseif ('/backups/akeeba_json.php' == $file_to_scan->filewithpath) { bfLog::log('SKIP: skipping ' . $file_to_scan->filewithpath); $skip = -3; } elseif ('/stats/webalizer.current' == $file_to_scan->filewithpath) { bfLog::log('SKIP: skipping ' . $file_to_scan->filewithpath); $skip = -4; } elseif (preg_match('/\.(gif|jpg|png|ico|jpeg|bmp)/ism', basename($file_to_scan->filewithpath)) && $this->isValidImage($file_to_scan->filewithpath) ) { bfLog::log('SKIP: skipping VALID IMAGE ' . $file_to_scan->filewithpath); $skip = -6; } elseif (preg_match('/\/stats\/usage_.*\.html/', $file_to_scan->filewithpath)) { bfLog::log('SKIP: skipping ' . $file_to_scan->filewithpath); $skip = -5; } elseif (0 == $skip && filesize(JPATH_BASE . $file_to_scan->filewithpath) > 1024288) { bfLog::log('SKIP: FILE WAS OVER 1Mb - skipping ' . $file_to_scan->filewithpath); $skip = -7; } elseif ('/components/com_dtregister/assets/js/jquery-ui.js' == $file_to_scan->filewithpath) { $skip = -7; } elseif ('/administrator/components/com_akeeba/backup/akeeba.json.log' == $file_to_scan->filewithpath) { $skip = -7; } elseif (filesize(JPATH_BASE . $file_to_scan->filewithpath) > 1000000) { bfLog::log('SKIP: FILE WAS SKIPPED AS OVER 1000000!!! ' . $file_to_scan->filewithpath); $skip = -1; } if (0 !== $skip) { // mark it as false positive (-2) or skipped (-1) $sql = sprintf( "UPDATE bf_files SET queued = 0, suspectcontent = '%s' WHERE id = '%s'", $skip, $this->db->escape($file_to_scan->id) ); $this->db->setQuery($sql); $this->db->execute(); bfLog::log('SKIP:CONTINUE'); continue; // no more processing on this file - skipped } // cleanup $fff = JPATH_BASE . stripslashes($file_to_scan->filewithpath); // WINDOWS I HATE YOU! - bodge it $fff = str_replace('\:', '/:', $fff); // need a @ to prevent access denied $chunk = @file_get_contents($fff); // remove stuff that is likely to be marked as suspect, when we are happy its not... $chunk = $this->applyStringExceptions($chunk, $file_to_scan); // Not really a chunk now, as we load the whole file into memory if (trim($chunk)) { // Need at least 3 seconds to run the preg_match on // average slow machine if ($this->_timer->getTimeLeft() < _BF_CONFIG_DEEPSCAN_TIMER_TWO) { bfLog::log('Need at least 3 seconds to run the preg_match on average slow machine'); $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE queued = 1'); if (0 == $this->db->loadResult()) { bfLog::log(' ======== deepscancomplete ========'); $this->deepscancomplete = true; $this->updateFilesFromDeepscan(__LINE__); // save our latest pattern match // decrypt then save the md5! file_put_contents('tmp/tmp.pattern.lastmd5', md5($pattern)); $this->nextStepPlease(); } else { bfLog::log(' ======== deepscancomplete NOT COMPLETE========'); $this->updateFilesFromDeepscan(__LINE__); $this->saveState(false, __LINE__); } } // hard to audit c99 clones preg_match('/(auth_pass).*(default_use_ajax)/ism', $chunk, $matches); if (count($matches) >= 3) { $isSuspect = true; } else { if (preg_match('/\.php/', $file_to_scan->filewithpath)) { preg_match('/move_uploaded_file/ism', $chunk, $matches); if (count($matches) >= 1) { $isUploader = true; } preg_match('/[^a-zA-Z0-9\-]{1}\s*mail\s*\(/ism', $chunk, $matches); if (count($matches) >= 1) { $isMailer = true; } } // 100% Certain if a file matches this regex then its hacked if (preg_match('/index\.html\.bak\.bak/i', $chunk)) { $isHacked = true; } // 100% Certain if a file matches this regex then its hacked if (preg_match('/@include\s\"\\057hom/i', $chunk)) { $isHacked = true; } // 100% Certain if a file matches this regex then its hacked if (preg_match('/ndsw===undefined/i', $chunk)) { $isHacked = true; } if (preg_match('/ndsw===\"undefined\"/i', $chunk)) { $isHacked = true; } if (preg_match('/ndsj===\"undefined\"/i', $chunk)) { $isHacked = true; } // 100% Certain if a file matches this regex then its hacked if (preg_match('/143o\";/i', $chunk)) { $isHacked = true; } // 100% Certain if a file matches this regex then its hacked if (preg_match('/^\/.well-known\/acme-challenge\/index\.php/', $file_to_scan->filewithpath) || preg_match('/^\/.well-known\/pki-validation\/index\.php/', $file_to_scan->filewithpath) || preg_match('/^\/.well-known\/index\.php/', $file_to_scan->filewithpath) || preg_match('/thumbnail_.*\.(png|jpg|gif)\?*\.php/i', (string) $file_to_scan->filewithpath) ) { $isHacked = 1; } // ACYMailing hack Aug 2023 if (preg_match('/\$[a-zA-Z]{2,4}\s?=\s?\$_COOKIE\s?\;\s?\$[a-zA-Z]{2,4}\s?=\s?0/i', $chunk)) { $isHacked = true; } $retiredClasses = require 'bfCompatPluginClasses.php'; $needsCompat = false; foreach ($retiredClasses as $retiredClass) { // Let's not piss off Nic from Akeeba with his aliases if (false !== stripos((string) $file_to_scan->filewithpath, "akeeba") || false !== stripos((string) $file_to_scan->filewithpath, "admintools") || false !== stripos((string) $file_to_scan->filewithpath, "jce") ) { continue; } if ($needsCompat !== true && preg_match('#' . addslashes($retiredClass) . '#ism', $chunk)) { $needsCompat = true; } } if (! $isHacked) { // Test if suspect bfLog::log('Auditing File: ' . $file_to_scan->filewithpath . ' - ' . $file_to_scan->size . ' bytes'); file_put_contents( __DIR__ . '/tmp/currentlyAuditingFile.php', '<?php die(); ?>' . date('Y-m-d H:i:s') . '||' . $file_to_scan->filewithpath ); $c = 0; foreach ($patternChunks as $patternChunk) { if (true === $isSuspect) { continue; } bfLog::log('Auditing File with patternChunk: ' . $c++); $isSuspect = (preg_match('/' . $patternChunk . '/ism', $chunk) ? true : false); } @unlink(__DIR__ . '/tmp/currentlyAuditingFile.php'); } // Test If encrypted $regex = "/OOO000000|if\(!extension_loaded\('ionCube\sLoader'\)\)|<\?php\s@Zend;|This\sfile\swas\sencoded\sby\sthe.*Zend Encoder/i"; $encrypted = (preg_match($regex, $chunk) ? 1 : 0); } } else { bfLog::log('FILE WAS EMPTY!!! ' . $file_to_scan->filewithpath); } // free up memory unset($chunk); $encrypted = (int) $encrypted; $isSuspect = (int) $isSuspect; $isHacked = (int) $isHacked; if ($needsCompat === true) { bfLog::log(' + NEEDS COMPAT'); $this->_needsCompatIds[] = $file_to_scan->id; } if ($encrypted && $isSuspect) { bfLog::log(' + isEncrypted/suspect'); $this->_encryptedAndSuspectIds[] = $file_to_scan->id; } elseif ($isHacked) { bfLog::log(' + isHacked'); $this->_hackedIds[] = $file_to_scan->id; } elseif ($encrypted) { bfLog::log(' + isEncrypted'); $this->_encryptedIds[] = $file_to_scan->id; } elseif ($isSuspect) { bfLog::log(' + isSuspect'); $this->_suspectIds[] = $file_to_scan->id; } elseif ($isMailer) { bfLog::log(' + isMailer'); $this->_mailerIds[] = $file_to_scan->id; } elseif ($isUploader) { bfLog::log(' + isUploader'); $this->_uploaderIds[] = $file_to_scan->id; } else { bfLog::log(' + OK'); $this->_notencryptedAndSuspectIds[] = $file_to_scan->id; } if (_BF_SPEED == 'CRAPPYWEBHOST' || $this->_timer->getTimeLeft() < _BF_CONFIG_DEEPSCAN_TIMER_TWO) { bfLog::log('Reconnecting to the database to prevent gone away messages'); $this->db->disconnect(); $this->db->connect(); $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE queued = 1'); if (0 == $this->db->loadResult()) { bfLog::log(' ======== deepscancomplete ========'); $this->deepscancomplete = true; $this->updateFilesFromDeepscan(__LINE__); // save our latest pattern match // decrypt then save the md5! file_put_contents('tmp/tmp.pattern.lastmd5', md5($pattern)); $this->nextStepPlease(); } else { bfLog::log(' ======== deepscancomplete NOT COMPLETE========'); $this->updateFilesFromDeepscan(__LINE__); $this->saveState(false, __LINE__); } } } if ($this->_timer->getTimeLeft() < _BF_CONFIG_DEEPSCAN_TIMER_TWO) { bfLog::log('Reconnecting to the database to prevent gone away messages'); $this->db->disconnect(); $this->db->connect(); $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE queued = 1'); if (0 == $this->db->loadResult()) { bfLog::log(' ======== deepscancomplete ========'); $this->deepscancomplete = true; $this->updateFilesFromDeepscan(__LINE__); // save our latest pattern match // decrypt then save the md5! file_put_contents('tmp/tmp.pattern.lastmd5', md5($pattern)); $this->nextStepPlease(); } else { bfLog::log(' ======== deepscancomplete NOT COMPLETE========'); $this->updateFilesFromDeepscan(__LINE__); $this->saveState(false, __LINE__); } } bfLog::log('Reconnecting to the database to prevent gone away messages'); $this->db->disconnect(); $this->db->connect(); // needed to go back up to the top of the loop if (count($this->getmergedIds())) { $this->db->setQuery( 'SELECT count(*) FROM bf_files WHERE queued = 1 AND id NOT IN (' . implode(',', $this->getmergedIds()) . ')' ); } else { $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE queued = 1'); } } bfLog::log('Reconnecting to the database to prevent gone away messages'); $this->db->disconnect(); $this->db->connect(); if (count($this->getmergedIds())) { $this->db->setQuery( 'SELECT count(*) FROM bf_files WHERE queued = 1 AND id NOT IN (' . implode(',', $this->getmergedIds()) . ')' ); } else { $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE queued = 1'); } if (0 == $this->db->loadResult()) { bfLog::log(' ======== deepscancomplete ========'); $this->deepscancomplete = true; $this->updateFilesFromDeepscan(__LINE__); // save our latest pattern match // decrypt then save the md5! file_put_contents('tmp/tmp.pattern.lastmd5', md5($pattern)); $this->nextStepPlease(); } else { bfLog::log(' ======== deepscancomplete NOT COMPLETE========'); $this->updateFilesFromDeepscan(__LINE__); $this->saveState(false, __LINE__); } } catch (Exception $e) { // Just continue... if (! defined('_BF_LAST_BREATH')) { define('_BF_LAST_BREATH', $e->getMessage()); } bfLog::log(' ======== EXCEPTION =========' . $e->getMessage() . print_r($e, true) . $this->db->getQuery()); } } /** * @return array */ private function getmergedIds() { return array_merge( $this->_encryptedAndSuspectIds, $this->_notencryptedAndSuspectIds, $this->_encryptedIds, $this->_mailerIds, $this->_uploaderIds, $this->_suspectIds, $this->_hackedIds, $this->_needsCompatIds ); } private function updateFilesFromDeepscan($line) { bfLog::log('Reconnecting to the database to prevent gone away messages'); $this->db->disconnect(); $this->db->connect(); bfLog::log(' =updateFilesFromDeepscan called from line ' . $line); bfLog::log(' =Marking this number of files as _needsCompatIds= ' . count($this->_needsCompatIds)); if (count($this->_needsCompatIds)) { $sql = 'UPDATE bf_files SET needscompat = 1, queued = 0 WHERE id IN(%s)'; $sql = sprintf($sql, implode(', ', $this->_needsCompatIds)); $this->db->setQuery($sql); $this->db->execute(); $this->_needsCompatIds = []; } bfLog::log(' =Marking this number of files as _encryptedAndSuspectIds= ' . count($this->_encryptedAndSuspectIds)); if (count($this->_encryptedAndSuspectIds)) { $sql = 'UPDATE bf_files SET encrypted = %s, suspectcontent = %s, queued = 0 WHERE id IN(%s)'; $sql = sprintf($sql, 1, 1, implode(', ', $this->_encryptedAndSuspectIds)); $this->db->setQuery($sql); $this->db->execute(); $this->_encryptedAndSuspectIds = []; } bfLog::log(' =Marking this number of files as _encryptedIds = ' . count($this->_encryptedIds)); if (count($this->_encryptedIds)) { $sql = 'UPDATE bf_files SET encrypted = %s, suspectcontent = %s, queued = 0 WHERE id IN(%s)'; $sql = sprintf($sql, 1, 0, implode(', ', $this->_encryptedIds)); $this->db->setQuery($sql); $this->db->execute(); $this->_encryptedIds = []; } bfLog::log(' =Marking this number of files as _suspectIds = ' . count($this->_suspectIds)); if (count($this->_suspectIds)) { $sql = 'UPDATE bf_files SET encrypted = %s, suspectcontent = %s, queued = 0 WHERE id IN(%s)'; $sql = sprintf($sql, 0, 1, implode(', ', $this->_suspectIds)); $this->db->setQuery($sql); $this->db->execute(); $this->_suspectIds = []; } bfLog::log(' =Marking this number of files as NOT _notencryptedAndSuspectIds = ' . count($this->_notencryptedAndSuspectIds)); if (count($this->_notencryptedAndSuspectIds)) { $sql = 'UPDATE bf_files SET encrypted = %s, suspectcontent = %s, queued = 0 WHERE id IN(%s)'; $sql = sprintf($sql, 0, 0, implode(', ', $this->_notencryptedAndSuspectIds)); $this->db->setQuery($sql); $this->db->execute(); $this->_notencryptedAndSuspectIds = []; } bfLog::log(' =Marking this number of files as _mailer = ' . count($this->_mailerIds)); if (count($this->_mailerIds)) { $sql = 'UPDATE bf_files SET mailer = 1, queued = 0 WHERE id IN(%s)'; $sql = sprintf($sql, implode(', ', $this->_mailerIds)); $this->db->setQuery($sql); $this->db->execute(); $this->_mailerIds = []; } bfLog::log(' =Marking this number of files as _uploader = ' . count($this->_uploaderIds)); if (count($this->_uploaderIds)) { $sql = 'UPDATE bf_files SET `uploader` = 1, queued = 0 WHERE id IN(%s)'; $sql = sprintf($sql, implode(', ', $this->_uploaderIds)); $this->db->setQuery($sql); $this->db->execute(); $this->_uploaderIds = []; } bfLog::log(' =Marking this number of files as _hacked = ' . count($this->_hackedIds)); if (count($this->_hackedIds)) { $sql = 'UPDATE bf_files SET `hacked` = 1, suspectcontent = 1, queued = 0 WHERE id IN(%s)'; $sql = sprintf($sql, implode(', ', $this->_hackedIds)); $this->db->setQuery($sql); $this->db->execute(); $this->_hackedIds = []; } } /** * An animated gif contains multiple "frames", with each frame having a header made up of: * - a static 4-byte sequence (\x00\x21\xF9\x04) * - 4 variable bytes * - a static 2-byte sequence (\x00\x2C). * * @see http://www.php.net/manual/en/function.imagecreatefromgif.php#88005 * @thanks Mike H. * * We read through the file til we reach the end of the file, or we've found * at least 2 frame headers * * @param string $filename complete path to the file * * @return bool */ private function is_ani($filename) { $chunk = null; if (! ($fh = @fopen($filename, 'r'))) { return false; } $count = 0; while (! feof($fh) && $count < 2) { $chunk = fread($fh, 1024 * 100); } //read 100kb at a time $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00\x2C#s', $chunk, $matches); fclose($fh); return $count > 1; } /** * Not the correct way to check for a valid image but "good enough" for our purposes Fast and cross PHP version * compatible... * * @return bool */ private function isValidImage($path) { bfLog::log('isValidImage? ' . JPATH_BASE . $path); $a = @getimagesize(JPATH_BASE . $path); $image_type = $a[2]; if (in_array($image_type, [\IMAGETYPE_GIF, \IMAGETYPE_JPEG, \IMAGETYPE_PNG, \IMAGETYPE_BMP])) { return true; } return false; } /** * Lets munge the files contents to reduce the number of false hits we get for eval and stuff Try to keep this list * as small as possible. * * @return string */ private function applyStringExceptions($chunk, $file_to_scan) { $chunk = str_replace('matheval', '_RETRACTED_', $chunk); // NoNumber Extensions $chunk = str_replace('parseVal', '_RETRACTED_', $chunk); // com_avreloaded $chunk = str_replace('$this->_data = unserialize(base64_decode($rdata));', '_RETRACTED_', $chunk); // plugins/system/k2/k2.php $chunk = str_replace('$output = \'<div style="display:none', '_RETRACTED_', $chunk); // com_community Extensions $chunk = str_replace('doubleval', '_RETRACTED_', $chunk); // com_breezingforms Extensions $chunk = str_replace('Zend_Json::decode(base64_decode', '_RETRACTED_', $chunk); // Joomla Core $chunk = str_replace('setRedirect(base64_decode($return)', '_RETRACTED_', $chunk); $chunk = str_replace('passthru(\'kill -9 \' . $pid);', '_RETRACTED_', $chunk); $chunk = str_replace('system(\'export HOME="\' . $info[\'dir\'] . \'"\');', '_RETRACTED_', $chunk); $chunk = str_replace( '\'c' . 'u' . 'r' . 'l' . '_version\',\'current\',\'cvsclient_connect\',\'cvsclient_log\'', '_RETRACTED_', $chunk ); $chunk = str_replace('$mainframe->redirect(base64_decode', '_RETRACTED_', $chunk); $chunk = str_replace('$return = base64_encode(base64_decode($return).\'#content\');', '_RETRACTED_', $chunk); //1.5.26 $chunk = str_replace('json_decode(base64_decode', '_RETRACTED_', $chunk); //2.5.8+ Returns a stdClass so cannot be eval'ed // com_weblinks $chunk = str_replace('JUri::isInternal(base64_decode', '_RETRACTED_', $chunk); // highside.js // $chunk = str_replace("y[o.position == 'above' ? 'p1' : 'p2'] = o.offsetHeight;", '_RETRACTED_', $chunk); // Admincredible // /components/com_admincredible/libraries/vendor/oauth-php/library/OAuthRequestVerifier.php $chunk = str_replace('if(isset($_REQUEST[\'oauth_signature\']))', '_RETRACTED_', $chunk); // com_k2 $chunk = str_replace('<div style="display:none">\'.JHTML::_(\'select.ra', '_RETRACTED_', $chunk); $chunk = str_replace('use exec() rather than shell_exec(), to play b', '_RETRACTED_', $chunk); // Master Htaccess File from Akeeba if (strpos($file_to_scan->filewithpath, 'htaccess')) { $chunk = str_replace('RewriteCond %{HTTP_REFERER} (<|>|\'|%0A|%0D|%27|%3C|%3E|%00) [NC,OR]', '_RETRACTED_', $chunk); $chunk = str_replace('RewriteCond %{HTTP_REFERER} ([a-zA-Z0-9]{32}) [NC]', '_RETRACTED_', $chunk); } // Joomla & Akeeba distribute cacert.pem which has Wells Fargo in it $chunk = str_replace('Wells Fargo Root CA', '_RETRACTED_', $chunk); // Gantry $chunk = str_replace('if (!function_exists(\'c' . 'u' . 'r' . 'l_version\'))', '_RETRACTED_', $chunk); // Smarty Template $chunk = str_replace('$smarty->_eval', '_RETRACTED_', $chunk); // JCE $chunk = str_replace('$version = ' . 'c' . 'u' . 'r' . 'l' . '_version();', '_RETRACTED_', $chunk); $chunk = str_replace('$ssl_supported = ($version[\'features\'] & C' . 'U' . 'R' . 'L' . '_VERSION_SSL);', '_RETRACTED_', $chunk); // Sparkline $chunk = str_replace('this.shapes[shape.id] = \'p1\';', '_RETRACTED_', $chunk); // com_rsform $chunk = str_replace('eval($form->', '_RETRACTED_', $chunk); // akeeba $chunk = str_replace('base64_decode(\'eyJhcHAiOiJqZn', '_RETRACTED_', $chunk); $chunk = str_replace('unserialize(base64_decode', '_RETRACTED_', $chunk); $iframe = '<iframe style="width: 0px; height: 0px; border: none;" frameborder="0" marginheight="0" marginwidth="0" height="0" width="0"'; $chunk = str_replace($iframe, '_RETRACTED_', $chunk); // jQuery $iframe = "<iframe frameborder='0' width='0' height='0'/>"; $chunk = str_replace($iframe, '_RETRACTED_', $chunk); // Google Tag Manager $iframe = 'height="0" width="0" style="display:none;visibility:hidden'; $chunk = str_replace($iframe, '_RETRACTED_', $chunk); // /media/foundry/2.1/scripts/jplayer.js $iframe = '0000" width="0" height="0">'; $chunk = str_replace($iframe, '_RETRACTED_', $chunk); return $chunk; } /** * Find out some basic information about this site and its setup. */ private function bestpracticesecurityAction() { bfLog::log('============================================='); bfLog::log('=========bestpracticesecurityAction=========='); bfLog::log('============================================='); $this->platform = 'Joomla'; //8192029 $this->db->setQuery("UPDATE bf_files SET hacked = 1, suspectcontent = 1 WHERE size = '8192029'"); $this->db->execute(); // flag filenames that are 100% a hack // first get a subset to check $this->db->setQuery("select * from bf_files WHERE falsepositive is null AND (iscorefile is null and hashfailed is null) AND filewithpath NOT LIKE '%Diff3.php' AND filewithpath NOT LIKE '%com_gantry/models/template.php.suspected' AND filewithpath NOT LIKE '%tcpdf.php.suspected' AND filewithpath NOT LIKE '%favicon_unused.ico' AND filewithpath NOT LIKE '%favicon_houven.ico' AND filewithpath NOT LIKE '%favicon_master.ico' AND filewithpath NOT LIKE '%favicon_joomla.ico' AND filewithpath NOT LIKE '%favicon_backup.ico' AND ( filewithpath like '%cache\-%' or filewithpath like '%\/\.%\.ico' or filewithpath like '%favicon\_%' or filewithpath like '%cache\_%' or filewithpath like '\/libraries\/joomla\/exporter\.php' or filewithpath like '%db\.php' or filewithpath like '%sql%\.php%' or filewithpath like '%diff%\.php%' or filewithpath like '%proxy%\.php%' or filewithpath like '%dirs%\.php%' or filewithpath like '%start%\.php%' or filewithpath like '%\.suspected' or filewithpath like '%timezone_tranositions_get%' or filewithpath like '%stream_bucketd_make_writeable%' or filewithpath like '%countt_chars%' or filewithpath like '%variantf_imp%' or filewithpath like '%com_contact_info%' or filewithpath like '%banner_copys%' or filewithpath like '%x\.php' or filewithpath like '%cgi\-%' or filewithpath like '%backup\-%' or filewithpath like '%sort\-%' or filewithpath like '%memcache\-%' or filewithpath like '%sql\-%' or filewithpath like '%reverse\-%' or filewithpath like '%conf\-%' or filewithpath like '%cache\-%' or filewithpath like '%bin\-%' or filewithpath like '%utf8\-%' ) "); $hacked = $this->db->loadObjectList(); bfLog::log('hackCheck = count - ' . count($hacked)); foreach ($hacked as $row) { bfLog::log('hackCheck = row - ' . $row->filewithpath); // run it though a PHP regex that is very specific on what its looking for as the mysql one is very very dodgy in old mysql versions if (preg_match( '!.*\/(cache-[0-9]{2}[a-z]\.php|\.[0-9a-z]{8}\.ico|cache_tpeowiol|1ndex\.php|favicon\_[a-z0-9]{6}\.ico|db[0-9]{2}\.php|cp1251-[0-9a-z]{3}\.php|sql\-[0-9]{2}[a-z]\.php|diff[0-9]{1,2}\.php|proxy[0-9]{1,2}\.php|dirs[0-9]{1,2}\.php|start[0-9]{1,2}\.php|.*\.suspected|libraries\/joomla\/exporter\.php|timezone_tranositions_get\.php|cmhiuup\.php|stream_bucketd_make_writeable\.php|countt_chars\.php|variantf_imp\.php|com_contact_info\.php|banner_copys\.php|(cgi|backup|sort|memcache|sql|reverse|conf|cache|bin|utf8)\-([a-z][0-9]*|[0-9]*|[a-z][a-z]|[0-9][a-z]|[a-z][a-z][a-z]|[0-9][a-z][0-9]|[0-9][a-z][a-z]|[a-z][0-9][a-z])\.php$)!', $row->filewithpath )) { bfLog::log('hackCheck = row IS HACKED - ' . $row->filewithpath); $this->db->setQuery('UPDATE bf_files SET hacked = 1, suspectcontent = 1 WHERE id = ' . $row->id); $this->db->execute(); } } // Am I hacked? $this->hacked = $this->checkIfHackedSite(); // Mark PHP in places PHP should not be! if ($ids = $this->_phpInWrongPlaces()) { $this->db->setQuery('UPDATE bf_files SET `suspectcontent` = 1 WHERE id IN (' . implode(',', $ids) . ')'); $this->db->execute(); } // Remove OUR stuff as we dont need to report on that $this->db->setQuery("DELETE FROM bf_files WHERE filewithpath = '/plugins/system/j15_bfnetwork.xml' OR filewithpath = '/plugins/system/j25_30_bfnetwork.xml' OR filewithpath = '%.myjoomla.ignore.files' OR filewithpath = '%.myjoomla.ignore.folder' OR filewithpath LIKE '/plugins/system/bfnetwork%'"); $this->db->execute(); // Remove OUR stuff as we dont need to report on that $this->db->setQuery("DELETE FROM bf_folders WHERE folderwithpath LIKE '/plugins/system/bfnetwork%'"); $this->db->execute(); // Report count of all .htaccess files $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE filewithpath LIKE "%.htaccess"'); $this->htaccess_files = $this->db->LoadResult(); // Report count of all files with 777 permissions $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE fileperms LIKE "%777%"'); $this->files_777 = $this->db->LoadResult(); $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE size = 0'); $this->zerobytes = $this->db->LoadResult(); $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE needscompat = 1'); $this->needscompat = $this->db->LoadResult(); $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE skipped = 1'); $this->skipped = $this->db->LoadResult(); // Report count of all folders with 777 permissions $this->db->setQuery('SELECT COUNT(*) FROM bf_folders WHERE folderinfo LIKE "%777%"'); $this->folders_777 = $this->db->LoadResult(); // Report all hidden folders like .git or .svn $this->db->setQuery('SELECT COUNT(*) FROM bf_folders WHERE folderwithpath LIKE "%/.%"'); $this->hidden_folders = $this->db->LoadResult(); // Report all hidden files like .htaccess .hack $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE filewithpath LIKE "%/.%"'); $this->hidden_files = $this->db->LoadResult(); // Report nested Joomla versions $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE filewithpath LIKE "%/administrator/index.php"'); $this->nestedinstalls = $this->db->LoadResult(); // Report file what might have been renamed to hide $this->db->setQuery( 'SELECT COUNT(*) FROM bf_files WHERE filewithpath LIKE "%.old%" OR filewithpath LIKE "%.bak%" OR filewithpath LIKE "%.backup%"' ); $this->renamedtohidefiles = $this->db->LoadResult(); // Report any error_log files $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE filewithpath LIKE "%error_log"'); $this->error_logs_seen = $this->db->LoadResult(); // Report files in the /tmp folder $this->db->setQuery('SELECT count(*) FROM bf_files WHERE filewithpath LIKE "/tmp%" AND filewithpath != "/tmp/index.html"'); $this->tmp_install_folders = $this->db->LoadResult(); // Report any encrypted files $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE encrypted = 1'); $this->encrypted_files = $this->db->LoadResult(); // Report suspect files $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE suspectcontent = 1 or hacked = 1'); $this->suspectfiles = $this->db->LoadResult(); // Report mailer files $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE mailer = 1'); $this->mailer = $this->db->LoadResult(); // Report uploader files $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE uploader = 1'); $this->uploader = $this->db->LoadResult(); // php.ini files $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE filewithpath LIKE "%php.ini%" OR filewithpath LIKE "%.user.ini%"'); $this->phpiniseen = $this->db->LoadResult(); // look for akeeba sql files $this->db->setQuery('SELECT count(*) FROM bf_files WHERE ( (filewithpath LIKE \'%.sql\' or filewithpath LIKE \'%sql/site.%\') and (iscorefile = 0 or iscorefile is null) )'); $this->sqlfilesseen = $this->db->LoadResult(); $this->db->setQuery('SELECT count(*) FROM bf_files WHERE filewithpath LIKE \'%DS_Store%\''); $this->dotunderscorefilesseen = $this->db->LoadResult(); $this->db->setQuery('SELECT count(*) FROM bf_files WHERE filewithpath LIKE \'%admintools_breaches.log%\''); $this->admintoolbreaches = $this->db->LoadResult(); // count of non core files $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE iscorefile is null'); $this->notcorefiles = $this->db->LoadResult(); $sql = "SELECT count(*) from `bf_core_hashes` WHERE filewithpath NOT IN ( SELECT filewithpath from bf_files ) AND filewithpath NOT LIKE '/installation/%' AND filewithpath != '/robots.txt.dist' AND filewithpath != '/administrator/manifests/packages/pkg_weblinks.xml' AND filewithpath != '/' AND filewithpath != '/robots.txt.dist' AND filewithpath != '/web.config.txt' AND filewithpath != '/joomla.xml' AND filewithpath != '/build.xml' AND filewithpath != '/LICENSE.txt' AND filewithpath != '/README.txt' AND filewithpath != '/htaccess.txt' AND filewithpath != '/LICENSES.php' AND filewithpath != '/configuration.php-dist' AND filewithpath != '/CHANGELOG.php' AND filewithpath != '/COPYRIGHT.php' AND filewithpath != '/CREDITS.php' AND filewithpath != '/INSTALL.php' AND filewithpath != '/LICENSE.php' AND filewithpath != '/CONTRIBUTING.md' AND filewithpath != '/phpunit.xml.dist' AND filewithpath != '/README.md' AND filewithpath != '/.travis.yml' AND filewithpath != '/travisci-phpunit.xml' AND filewithpath != '/images/banners/osmbanner1.png' AND filewithpath != '/images/banners/osmbanner2.png' AND filewithpath != '/images/banners/shop-ad-books.jpg' AND filewithpath != '/images/banners/shop-ad.jpg' AND filewithpath != '/images/banners/white.png' AND filewithpath != '/images/headers/blue-flower.jpg' AND filewithpath != '/images/headers/maple.jpg' AND filewithpath != '/images/headers/raindrops.jpg' AND filewithpath != '/images/headers/walden-pond.jpg' AND filewithpath != '/images/headers/windows.jpg' AND filewithpath != '/images/joomla_black.gif' AND filewithpath != '/images/joomla_black.png' AND filewithpath != '/images/joomla_green.gif' AND filewithpath != '/images/joomla_logo_black.jpg' AND filewithpath != '/images/powered_by.png' AND filewithpath != '/images/sampledata/fruitshop/apple.jpg' AND filewithpath != '/images/sampledata/fruitshop/bananas_2.jpg' AND filewithpath != '/images/sampledata/fruitshop/fruits.gif' AND filewithpath != '/images/sampledata/fruitshop/tamarind.jpg' AND filewithpath != '/images/sampledata/parks/animals/180px_koala_ag1.jpg' AND filewithpath != '/images/sampledata/parks/animals/180px_wobbegong.jpg' AND filewithpath != '/images/sampledata/parks/animals/200px_phyllopteryx_taeniolatus1.jpg' AND filewithpath != '/images/sampledata/parks/animals/220px_spottedquoll_2005_seanmcclean.jpg' AND filewithpath != '/images/sampledata/parks/animals/789px_spottedquoll_2005_seanmcclean.jpg' AND filewithpath != '/images/sampledata/parks/animals/800px_koala_ag1.jpg' AND filewithpath != '/images/sampledata/parks/animals/800px_phyllopteryx_taeniolatus1.jpg' AND filewithpath != '/images/sampledata/parks/animals/800px_wobbegong.jpg' AND filewithpath != '/images/sampledata/parks/banner_cradle.jpg' AND filewithpath != '/images/sampledata/parks/landscape/120px_pinnacles_western_australia.jpg' AND filewithpath != '/images/sampledata/parks/landscape/120px_rainforest_bluemountainsnsw.jpg' AND filewithpath != '/images/sampledata/parks/landscape/180px_ormiston_pound.jpg' AND filewithpath != '/images/sampledata/parks/landscape/250px_cradle_mountain_seen_from_barn_bluff.jpg' AND filewithpath != '/images/sampledata/parks/landscape/727px_rainforest_bluemountainsnsw.jpg' AND filewithpath != '/images/sampledata/parks/landscape/800px_cradle_mountain_seen_from_barn_bluff.jpg' AND filewithpath != '/images/sampledata/parks/landscape/800px_ormiston_pound.jpg' AND filewithpath != '/images/sampledata/parks/landscape/800px_pinnacles_western_australia.jpg' AND filewithpath != '/images/sampledata/parks/parks.gif' "; $this->db->setQuery($sql); $this->missingcorefiles = $this->db->LoadResult(); $this->db->setQuery('SHOW TABLES LIKE "bf_files_last"'); if ($this->db->loadResult()) { $sql = 'select count(*) from `bf_files` as new LEFT JOIN bf_files_last as old ON old.filewithpath = new.filewithpath WHERE old.currenthash != new.currenthash'; $this->db->setQuery($sql); $this->modifiedfilessincelastaudit = $this->db->LoadResult(); } // has_robots_modified $sql = 'SELECT c.ch as core_hash, my.ch as my_hash FROM ( SELECT core.hash as ch FROM bf_core_hashes AS core WHERE core.filewithpath = "/robots.txt" OR core.filewithpath = "/robots.txt.dist" LIMIT 1 ) as c, ( SELECT bf_files.currenthash as ch FROM bf_files WHERE bf_files.filewithpath = "/robots.txt" LIMIT 1 ) as my'; $this->db->setQuery($sql); $row = $this->db->loadAssocList(); if ($row) { if ($row[0]['core_hash'] && $row[0]['my_hash'] && ($row[0]['core_hash'] === $row[0]['my_hash'])) { $this->has_robots_modified = 0; } else { $this->has_robots_modified = 1; } } else { $this->has_robots_modified = 0; } // Files over 2Mb $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE SIZE > 2097152'); $this->large_files = $this->db->LoadResult(); // Doing this again here now that I refactored the audit for perfomance I now need to do this much later on :-( $this->hashfailedcount = $this->gethashfailurecountAction(true); // Report if we have default user ids $this->user_hasdefaultuserids = $this->_hasDefaultUserids(); // PhP in wrong places $phpInWrongPlaces = $this->_phpInWrongPlaces(); $this->phpinwrongplace = $phpInWrongPlaces ? count($phpInWrongPlaces) : 0; /* * @todo add these http://en.wikipedia.org/wiki/List_of_archive_formats */ // Report all archives $this->db->setQuery('SELECT COUNT(*) FROM bf_files WHERE filewithpath LIKE "%.zip" OR filewithpath LIKE "%.tar" OR filewithpath LIKE "%.tar.gz" OR filewithpath LIKE "%.bz2" OR filewithpath LIKE "%.gzip" OR filewithpath LIKE "%.bzip2"'); $this->archive_files = $this->db->LoadResult(); $this->nextStepPlease(true); } /** * Run some very specific checks to see if this site is hacked or not. */ private function checkIfHackedSite() { $this->db->setQuery('SELECT count(*) FROM bf_files WHERE hacked = 1'); return $this->db->loadResult(); } private function _phpInWrongPlaces() { $idsSql = "SELECT id FROM bf_files AS b WHERE filewithpath REGEXP '^/images/.*\.php$'"; // OR filewithpath REGEXP '^/media/.*\.php$' $this->db->setQuery($idsSql); if (method_exists($this->db, 'loadColumn')) { $ids = $this->db->loadColumn(); } else { $ids = $this->db->loadResultArray(); } return $ids; } /** * Count how many core files failed their hash checks. */ private function gethashfailurecountAction($internal = false) { $sql = 'SELECT COUNT(*) FROM bf_files WHERE iscorefile = 1 AND hashfailed = 1'; $this->db->setQuery($sql); $this->hashfailedcount = $this->db->LoadResult(); if (false === $internal) { // move onto the next step $this->nextStepPlease(); } else { return $this->hashfailedcount; } } /** * Do we have any default ids. * * @return int */ private function _hasDefaultUserids() { $this->db->setQuery('SELECT COUNT(*) FROM #__users WHERE id IN (62 , 42)'); return $this->db->loadResult(); } }