| Current Path : /home/u4uwebca/public_html/4uweb.design/plugins/system/bfnetwork/bfnetwork/ |
| Current File : /home/u4uwebca/public_html/4uweb.design/plugins/system/bfnetwork/bfnetwork/bfAuditor.php |
<?php
/*
* @package bfNetwork
* @copyright Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 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 phil@phil-taylor.com
*/
/**
* Class bfAudit.
*/
final class bfAudit
{
/**
* @var bfTimer Our timer class
*/
public $_timer;
/**
* @var JDatabaseMysql The database Connector
*/
private $db;
/**
* @var array
*/
private $_encryptedAndSuspectIds = array();
/**
* @var array
*/
private $_encryptedIds = array();
/**
* @var array
*/
private $_suspectIds = array();
/**
* @var array
*/
private $_uploaderIds = array();
/**
* @var array
*/
private $_hackedIds = array();
/**
* @var array
*/
private $_mailerIds = array();
/**
* @var array
*/
private $_notencryptedAndSuspectIds = array();
/**
* @var
*/
private $alreadyAddedRootDirs = false;
/**
* @var
*/
private $foundDirs;
/**
* @var
*/
private $foundFiles;
/**
* @var
*/
private $suspectfiles;
/**
* @var bool
*/
private $noMoreFoldersToScan = false;
/**
* @var bool
*/
private $noMoreFilesToScan = false;
/**
* @var bool
*/
private $deepscancomplete = false;
/**
* @var int
*/
private $tickOver = 0;
/**
* @var
*/
private $startTime;
/**
* @var
*/
private $endTime;
/**
* @var
*/
private $version;
/**
* @var
*/
private $platform;
/**
* @var
*/
private $scancomplete;
/**
* @var
*/
private $foundRecentlyModifiedFilesTotal;
/**
* @var
*/
private $hashfailedcount;
/**
* @var int
*/
private $step;
/**
* @var
*/
private $connectorversion;
/**
* @var
*/
private $files_777;
/**
* @var
*/
private $hacked;
/**
* @var
*/
private $zerobytes;
/**
* @var
*/
private $folders_777;
/**
* @var
*/
private $hidden_folders;
/**
* @var
*/
private $hidden_files;
/**
* @var
*/
private $renamedtohidefiles;
/**
* @var
*/
private $nestedinstalls;
/**
* @var
*/
private $error_logs_seen;
/**
* @var
*/
private $encrypted_files;
/**
* @var
*/
private $large_files;
/**
* @var
*/
private $has_robots_modified;
/**
* @var
*/
private $user_hasdefaultuserids;
/**
* @var
*/
private $archive_files;
/**
* @var
*/
private $htaccess_files;
/**
* @var
*/
private $phpiniseen;
/**
* @var
*/
private $uploader;
/**
* @var
*/
private $mailer;
/**
* @var
*/
private $max_allowed_packet;
/**
* @var
*/
private $phpinwrongplace;
/**
* @var
*/
private $notcorefiles;
/**
* @var
*/
private $missingcorefiles;
/**
* @var
*/
private $modifiedfilessincelastaudit;
/**
* @var
*/
private $tmp_install_folders;
/**
* @var
*/
private $sqlfilesseen;
/**
* @var
*/
private $admintoolbreaches;
/**
* @var
*/
private $dotunderscorefilesseen;
/**
* 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();
// init Joomla
require 'bfInitJoomla.php';
/*
* 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)) {
// reset the state
$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->query();
$url = base64_decode($request->NOTENCRYPTED['md5s']);
$options = array(
'http' => array(
'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) {
bfEncrypt::reply(bfReply::ERROR, 'We could not download a required file from the CDN (Nothing downloaded!) - seek assistance from phil@phil-taylor.com');
}
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 phil@phil-taylor.com');
}
$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 = array();
foreach ($dataLines as $line) {
$parts = explode("\t", $line);
// Do it this way for speed, 1 query instead of 4000+ queries!
$values[] = sprintf('("/%s", "%s")', $parts[0], $parts[1]);
}
// import now!
$this->db->setQuery($sql.implode(' , ', $values));
$this->db->query();
// memory cleanup
unset($parts);
unset($dataLines);
unset($data);
}
// get the base bfnetwork folder
$base = dirname(__FILE__);
// 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 = array(
'http' => array(
'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 phil@phil-taylor.com');
}
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_writeable(dirname(__FILE__).'/tmp')) {
@chmod(dirname(__FILE__).'/tmp', 0755);
}
// Argh!
if (!is_writeable(dirname(__FILE__).'/tmp')) {
@chmod(dirname(__FILE__).'/tmp', 0777);
}
// Give Up!
if (!is_writeable(dirname(__FILE__).'/tmp')) {
bfEncrypt::reply(bfReply::ERROR, 'Our '.dirname(__FILE__).'/tmp folder on your site is not writable!');
}
// attempt to ensure our folder is writable
if (!is_writeable(dirname(__FILE__))) {
@chmod(dirname(__FILE__), 0755);
}
// Argh!
if (!is_writeable(dirname(__FILE__))) {
@chmod(dirname(__FILE__), 0777);
}
// Give Up!
if (!is_writeable(dirname(__FILE__))) {
bfEncrypt::reply(bfReply::ERROR, dirname(__FILE__).'/ 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
require 'bfInitJoomla.php';
$this->db = JFactory::getDBO();
// ok then, while were are here lets look up
// the Joomla version we are in
$VERSION = new JVersion();
$this->version = $VERSION->getShortVersion();
}
/**
* 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->query();
}
$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->query();
}
// 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);
if (!$this->db->query()) {
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
}
}
// remove any tmp files we might have created
@unlink(dirname(__FILE__).'/tmp/tmp.md5s');
@unlink(dirname(__FILE__).'/tmp/tmp.pattern');
@unlink(dirname(__FILE__).'/tmp/tmp.pattern.unenc');
@unlink(dirname(__FILE__).'/tmp/tmp.false');
@unlink(dirname(__FILE__).'/tmp/tmp.log');
@unlink(dirname(__FILE__).'/tmp/tmp.ob');
@unlink(dirname(__FILE__).'/tmp/large.sql');
@unlink(dirname(__FILE__).'/tmp/large1.sql');
@unlink(dirname(__FILE__).'/tmp/large2.sql');
@unlink(dirname(__FILE__).'/tmp/large3.sql');
@unlink(dirname(__FILE__).'/tmp/large4.sql');
@unlink(dirname(__FILE__).'/tmp/large5.sql');
@unlink(dirname(__FILE__).'/tmp/large6.sql');
@unlink(dirname(__FILE__).'/tmp/speedup.sql');
@unlink(dirname(__FILE__).'/tmp/STATE');
@unlink(dirname(__FILE__).'/tmp/STATE.php');
@unlink(dirname(__FILE__).'/tmp/Folders');
@unlink(dirname(__FILE__).'/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(array('<?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;
$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->dbPing();
$this->db->setQuery('SELECT count(*) FROM bf_'.$tbl.' WHERE queued = 1');
return $this->db->loadResult();
}
private function dbPing()
{
bfLog::log(' == 1 pinging to the db with class ');
if (null === $this->db) {
$this->db = JFactory::getDbo();
}
if (!$this->db->connected()) {
if (method_exists($this->db, 'getConnection')) {
switch (get_class($this->db->getConnection())) {
case 'mysql':
@mysql_ping($this->db->getConnection());
break;
case 'mysqli':
mysqli_ping($this->db->getConnection());
break;
}
} else {
// Joomla 1.freaking.5
switch (get_class($this->db->name)) {
case 'mysql':
@mysql_ping($this->db->_resource);
break;
case 'mysqli':
mysqli_ping($this->db->_resource);
break;
}
}
}
}
/**
* 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)
{
if (!count($arr)) {
return array();
}
// 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 = array();
foreach ($arr as $folder) {
// clean up
$folder = $this->_cleanupFileFolderName($folder);
// $folder = trim(str_replace('\\', '/', $folder));
// $folder = str_replace('////', '/', $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->_cleanupFileFolderName(JPATH_BASE.DIRECTORY_SEPARATOR.$folder))
&& !is_dir($this->_cleanupFileFolderName(JPATH_BASE.DIRECTORY_SEPARATOR.$folder.DIRECTORY_SEPARATOR))
|| is_link($this->_cleanupFileFolderName(JPATH_BASE.DIRECTORY_SEPARATOR.$folder.DIRECTORY_SEPARATOR))
|| is_link($this->_cleanupFileFolderName(JPATH_BASE.DIRECTORY_SEPARATOR.$folder)) || !$folder
) {
continue;
}
$perms = $this->_getFolderPerms($folder);
$insertFolderToDb[] = " ( '".addslashes($folder)."', '".$perms."', ".$queued.')';
}
if (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->query();
$sqlToRun2 = $sqlprefix.implode(', ', $insertFolderToDb[1]);
bfLog::log('sql size 2= '.strlen($sqlToRun2));
$this->db->setQuery($sqlToRun2);
$this->db->query();
$sqlToRun3 = $sqlprefix.implode(', ', $insertFolderToDb[2]);
bfLog::log('sql size 3= '.strlen($sqlToRun3));
$this->db->setQuery($sqlToRun3);
$this->db->query();
$sqlToRun4 = $sqlprefix.implode(', ', $insertFolderToDb[3]);
bfLog::log('sql size 4= '.strlen($sqlToRun4));
$this->db->setQuery($sqlToRun4);
$this->db->query();
} else {
$this->db->setQuery($sqlToRun);
$this->db->query();
}
}
return array();
}
/**
* THANK YOU WORDPRESS !!! I love you xxx.
*
* @param $path
*
* @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.
*
* @param $folder
*
* @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 $array
* @param int $pieces
*
* @return array
*/
private function array_split($array, $pieces = 2)
{
if ($pieces < 2) {
return array($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(array($a), $b);
}
/**
* 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 getFolders($folder)
{
// Initialize variables
$arr = array();
$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 array();
}
$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
*
* @param $file_name
*
* @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 = JFactory::getApplication('site');
$dbname = $config->getCfg('db', '');
$this->db->setQuery("SHOW TABLES WHERE `Tables_in_{$dbname}` like 'bak_%'");
return $this->db->loadResult() ? 'TRUE' : 'FALSE';
}
private function testconnectionAction()
{
// Ask Joomla API for some settings
$config = JFactory::getApplication('site');
// Get some Joomla version
$VERSION = new JVersion();
if ('1.5' == $VERSION->RELEASE) {
$this->nextStepPlease();
return;
}
try {
// Send an email to see if we received it... Tests if the Joomla Global Config mailer settings are correct.
$mailer = JFactory::getMailer();
$sender = array(
$config->getCfg('mailfrom'),
$config->getCfg('fromname'), );
$mailer->setSender($sender);
$mailer->addRecipient('AuditMailerTest@myjoomla.io'); // 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(array('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);
$this->db->query();
}
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);
$this->db->query();
}
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);
$this->db->query();
}
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);
$this->db->query();
}
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);
$this->db->query();
}
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);
$this->db->query();
}
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);
$this->db->query();
}
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';
$this->db->setQuery($sql);
if (method_exists($this->db, 'loadColumn')) {
$ids = $this->db->loadColumn();
} else {
$ids = $this->db->loadResultArray();
}
if (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 (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 = array();
// 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 (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 = array();
// 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);
// $dirToScanWithPath = JPATH_BASE.DIRECTORY_SEPARATOR.str_replace(JPATH_BASE, '', $dirToScan);
$filesInThisFolder = $this->getFiles($dirToScanWithPath);
bfLog::log('Found '.count($filesInThisFolder).' files in '.$this->removeJPATHBASE($dirToScan));
// If there are any files, and we have time left
if (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));
if (!$this->db->query()) {
bfLog::log($this->db->getErrorMsg());
}
$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);
// 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) {
$this->db->setQuery('/*5*/INSERT INTO bf_files
(filewithpath, fileperms, filemtime, currenthash, size) VALUES '
.implode(', ', $sqlvalues));
if (!$this->db->query()) {
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
$sqlvalues = array();
$this->removeFromQueue('folders', $removeFoldersFromQueueIds);
$this->saveState(false, __LINE__);
}
}
// Save to the database
if (is_array($sqlvalues) && count($sqlvalues)) {
$this->db->setQuery('/*7*/INSERT INTO bf_files
(filewithpath, fileperms, filemtime, currenthash, size) VALUES '
.implode(', ', $sqlvalues));
if (!$this->db->query()) {
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
$sqlvalues = array();
}
// 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);
}
/**
* Function taken from Akeeba filesystem.php.
*
* Akeeba Engine
* The modular PHP5 site backup engine
*
* @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 getFiles($folder)
{
// Initialize variables
$arr = array();
$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.files')) {
return array();
}
$handle = @opendir($folder);
if (false === $handle) {
$handle = @opendir($folder.'/');
}
// If directory is not accessible, just return FALSE
if (false === $handle) {
return $false;
}
while ((false !== ($file = @readdir($handle)))) {
if (('.' != $file) && ('..' != $file)) {
$ds = ('' == $folder) || (DIRECTORY_SEPARATOR == $folder) || (DIRECTORY_SEPARATOR == @substr($folder, -1)) || (DIRECTORY_SEPARATOR == @substr($folder, -1)) ? '' : DIRECTORY_SEPARATOR;
$dir = $folder.$ds.$file;
$isDir = @is_dir($dir);
if (!$isDir) {
$arr[] = $this->_cleanupFileFolderName($file);
}
}
}
@closedir($handle);
return $arr;
}
/**
* @param $tbl
* @param array $updateIds
*
* @return array
*/
private function removeFromQueue($tbl, $updateIds = array())
{
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);
if (!$this->db->query()) {
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
}
return array();
}
/**
* 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 = array();
// 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';
}
// only hash small files
if ($size < 1048576) { // 1 megabyte = 1048576 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 = array();
$addToScanQueue = array();
$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(array('/'), 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();
$COUNTER = 0;
while (count($dirs_to_scan) && $this->_timer->getTimeLeft() > _BF_CONFIG_FOLDERS_TIMER_ONE) {
$dirToScanObj = array_pop($dirs_to_scan);
// $dirToScan = stripslashes($dirToScanObj->folderwithpath);
// $dirToScan = str_replace('////', '/', $dirToScan);
$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 = JPATH_BASE.preg_replace('#^'.JPATH_BASE.'#i', '', $dirToScan, 1);
$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 '.count($subDirectorys).' subfolders in '.$this->removeJPATHBASE($dirToScan));
if (count($subDirectorys)) {
foreach ($subDirectorys as $folder) {
// $folder = str_replace('////', '/', $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;
// $addToScanQueue = str_replace('////', '/', $addToScanQueue);
$addToScanQueue = $this->wp_normalize_path($addToScanQueue);
$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
}
/**
* @param $tblSuffix
* @param $queued
*/
private function toggleQueued($tblSuffix, $queued)
{
$sql = 'UPDATE bf_'.$tblSuffix.' SET queued = '.$queued;
$this->db->setQuery($sql);
$this->db->query();
}
/**
* Deep scan.
*/
private function deepscanAction()
{
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->query();
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->query();
unlink('tmp/hashfailed.sql');
$this->saveState(false, __LINE__);
}
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 = "SELECT id FROM bf_files
WHERE
(
SIZE < 800000 -- 0.4mb 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 '%.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 '%.png' -- never seen anything bad in a png
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 '%.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 '%.jpa' -- 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 '%/akeeba_backend.id%' -- Akeeba json state file
AND filewithpath NOT LIKE '%/akeeba_backend.php' -- Akeeba json state file
AND filewithpath NOT LIKE '%/akeeba_backend.log' -- Akeeba json state file
AND filewithpath NOT LIKE '%/akeeba_lazy.php' -- Akeeba json state file
AND filewithpath NOT LIKE '%/akeeba_frontend.php' -- 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);
$ids = array();
if (method_exists($this->db, 'loadColumn')) {
$ids = $this->db->loadColumn();
} else {
$ids = $this->db->loadResultArray();
}
bfLog::log('FOUND SOME IDS TO QUEUE: '.count($ids));
bfLog::log($this->db->getErrorMsg());
if (!count($ids)) {
bfLog::log('NO FILES TO DEEP SCAN = THIS CANNOT BE POSSIBLE RIGHT?');
bfEncrypt::reply(bfReply::ERROR, 'We could not identify any files to audit, please contact phil@phil-taylor.com to debug this for you.');
} else {
$sql = 'UPDaTE bf_files SET queued = 1 WHERE id IN ( %s )';
$len = strlen(sprintf($sql, implode(', ', $ids)));
$max_allowed_packet = $this->max_allowed_packet;
// ffs - I hate badly configured servers
if (!$this->max_allowed_packet) {
$this->db->setQuery('SHOW VARIABLES LIKE "max_allowed_packet"');
$max_allowed_packet = $this->db->loadObjectList();
if (!$this->max_allowed_packet) {
$max_allowed_packet = 1048576 / 1.2; // default - safety margin
}
}
bfLog::log(sprintf('The len is %s and the $max_allowed_packet is %s, and this means %s',
$len,
$max_allowed_packet,
($len > $max_allowed_packet)
));
if ($len > $max_allowed_packet) {
$parts = $this->array_split($ids, 4);
$sqlToRun1 = sprintf($sql, implode(', ', $parts[0]));
bfLog::log('sql size 1= '.strlen($sqlToRun1));
$this->db->setQuery($sqlToRun1);
$this->db->query();
$sqlToRun2 = sprintf($sql, implode(', ', $parts[1]));
bfLog::log('sql size 2= '.strlen($sqlToRun2));
$this->db->setQuery($sqlToRun2);
$this->db->query();
$sqlToRun3 = sprintf($sql, implode(', ', $parts[2]));
bfLog::log('sql size 3= '.strlen($sqlToRun3));
$this->db->setQuery($sqlToRun3);
$this->db->query();
$sqlToRun4 = sprintf($sql, implode(', ', $parts[3]));
bfLog::log('sql size 4= '.strlen($sqlToRun4));
$this->db->setQuery($sqlToRun4);
$this->db->query();
} else {
$sql = 'UPdATE bf_files SET queued = 1 WHERE id IN
(
'.implode(', ', $ids).'
)';
$this->db->setQuery($sql);
$this->db->query();
}
}
// DEQUEUE known clean files not changed
bfLog::log('DONE Adding '.count($ids).' files to the scan queue db table');
bfLog::log('Retrieving global whitelist from cdn');
$url = 'https://cdn.mysites.guru/public/global/whitelist';
$options = array(
'http' => array(
'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 phil@phil-taylor.com');
}
$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->query();
bfLog::log('Global whitelist applied!');
bfLog::log('Retrieving global hacklist from cdn');
$url = 'https://cdn.mysites.guru/public/global/hacklist';
$options = array(
'http' => array(
'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 phil@phil-taylor.com');
}
$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->query();
bfLog::log('Global hacklist applied!');
// 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);
/*
* When developing/debugging we might want to cache the unencrypted patterns
* We dont do the 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.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);
}
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($speedSQL);
$this->db->query();
}
// force at least one file to be audited to prevent broken loop
$this->db->setQuery('UPDATE bf_files SET queued = 1 WHERE filewithpath = "/configuration.php"');
$this->db->query();
$this->saveState(false, __LINE__);
} else {
if (file_exists(dirname(__FILE__).'/tmp/speedup.sql')) {
bfLog::log('SPEEDUP - removing speedup file without applying it as _BF_SPEED = '._BF_SPEED);
@unlink(dirname(__FILE__).'/tmp/speedup.sql');
}
}
// 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 {
$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;
$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) > 800000) {
bfLog::log('SKIP: FILE WAS SKIPPED AS OVER 8000000!!! '.$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,
addslashes($file_to_scan->id)
);
$this->db->setQuery($sql);
$this->db->query();
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->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('/143o\";/i', $chunk)) {
$isHacked = true;
}
// 100% Certain if a file matches this regex then its hacked
// UTF-8 for GLOBALS
// if (preg_match('/\\x47\\x4c\\x4fB\\x41\\x4c\\x53/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)
) {
$isHacked = 1;
}
if (!$isHacked) {
// Test if suspect
bfLog::log('Auditing File: '.$file_to_scan->filewithpath.' - '.$file_to_scan->size.' bytes');
$isSuspect = (preg_match('/'.$pattern.'/ism', $chunk) ? true : false);
}
// 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 ($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) {
$this->updateFilesFromDeepscan(__LINE__);
$this->saveState(false, __LINE__);
}
}
if ($this->_timer->getTimeLeft() < _BF_CONFIG_DEEPSCAN_TIMER_TWO) {
$this->updateFilesFromDeepscan(__LINE__);
$this->saveState(false, __LINE__);
}
// 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');
}
}
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().$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
);
}
/**
* @param $line
*/
private function updateFilesFromDeepscan($line)
{
// reconnect to the database
$this->db = JFactory::getDBO();
$this->dbPing();
bfLog::log(' =updateFilesFromDeepscan called from line '.$line);
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);
if ($this->db->query()) {
bfLog::log(' = Removed success = ');
} else {
bfLog::log('=============================================');
bfLog::log($this->db->getErrorMsg().$sql);
bfLog::log('=============================================');
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
$this->_encryptedAndSuspectIds = array();
}
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);
if ($this->db->query()) {
bfLog::log(' = Removed success = ');
} else {
bfLog::log('=============================================');
bfLog::log($this->db->getErrorMsg().$sql);
bfLog::log('=============================================');
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
$this->_encryptedIds = array();
}
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);
if ($this->db->query()) {
bfLog::log(' = Removed success = ');
} else {
bfLog::log('=============================================');
bfLog::log($this->db->getErrorMsg().$sql);
bfLog::log('=============================================');
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
$this->_suspectIds = array();
}
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);
if ($this->db->query()) {
bfLog::log(' = Removed success = ');
} else {
bfLog::log('=============================================');
bfLog::log($this->db->getErrorMsg().$sql);
bfLog::log('=============================================');
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
$this->_notencryptedAndSuspectIds = array();
}
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);
if ($this->db->query()) {
bfLog::log(' = mailer success = ');
} else {
bfLog::log('=============================================');
bfLog::log($this->db->getErrorMsg().$sql);
bfLog::log('=============================================');
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
$this->_mailerIds = array();
}
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);
if ($this->db->query()) {
bfLog::log(' = mailer success = ');
} else {
bfLog::log('=============================================');
bfLog::log($this->db->getErrorMsg().$sql);
bfLog::log('=============================================');
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
$this->_uploaderIds = array();
}
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);
if ($this->db->query()) {
bfLog::log(' = _hackedIds success = ');
} else {
bfLog::log('=============================================');
bfLog::log($this->db->getErrorMsg().$sql);
bfLog::log('=============================================');
bfEncrypt::reply(bfReply::ERROR, $this->db->getErrorMsg());
}
$this->_hackedIds = array();
}
}
/**
* 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 $filename string complete path to the file
*
* @return bool
*/
private function is_ani($filename)
{
if (!($fh = @fopen($filename, 'rb'))) {
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...
*
* @param $path
*
* @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, array(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.
*
* @param $chunk
* @param $file_to_scan
*
* @return mixed
*
* @todo add this to the configurable audit configuration
* @todo benchmark this against a preg_replace
*/
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);
// /media/foundry/2.1/scripts/jplayer.js
$iframe = '0000" width="0" height="0">';
$chunk = str_replace($iframe, '_RETRACTED_', $chunk);
// Google Tag MAanager
// <iframe src="//www.googletagmanager.com/ns.html?id=XXX-XXXXX" height="0" width="0" style="display:none;visibility:hidden"></iframe>
$chunk = preg_replace('#\<iframe\ssrc=\"\/\/www.googletagmanager.com\/ns\.html\?id\=.*\"\sheight\=\"0\"\swidth\=\"0\"\sstyle\=\"display:none;visibility:hidden\"\>\<\/iframe\>#ism', '', $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->query();
// 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|x\.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->query();
}
}
// 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->query();
}
// 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->query();
// 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->query();
// 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();
// 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();
}
/**
* @return mixed
*/
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();
}
}