' . $filename . '
' . $src . 'must return an array * + Joomla 6.0.4 / 5.4.4 * + Wordpress 6.9.4 * * ======= * version 2.2.2 (by ConseilGouz) * + Joomla 4.4.13 / 4.4.14 * + Joomla 5.4.2 / 5.4.3 * + Joomla 6.0.2 / 6.0.3 * + some J! extensions updates * + update monitored_folders.json (joomla 6 compatibility) * + Wordpress 6.9.1 * + some WP extensions updates * * ======= * version 2.2.1 (by ConseilGouz) * + Joomla 5.4.1 * + Joomla 6.0.1 * + some J! extensions updates * * ======= * version 2.2.0 (by ConseilGouz) * + Joomla 5.4.0 * + Joomla 6.0.0 * + Wordpress 6.8.3 * * ======= * version 2.1.10 (by ConseilGouz) * + Joomla 5.3.4 * * version 2.1.9 (by ConseilGouz) * + Joomla 5.3.3 * + WP 6.8.2 * + some J! extensions updates * + some WP extensions updates * * version 2.1.8 (by ConseilGouz) * + Joomla 5.3.2 * + some J! extensions updates * + some WP extensions updates * * version 2.1.7 * + Joomla 5.3.1 * + WP 6.8.1 * + some WP extensions updates * * version 2.1.6 * + Joomla 5.3.0 * + some WP extensions updates * * version 2.1.5 * + Joomla 5.2.6 * + some WP extensions updates * * version 2.1.4 * + other.json and whitelist.json files lost (too small) * * version 2.1.3 * + Add some Wordpress extensions * + make_hashes : display todo list + go button * * version 2.1.2 * + Extensions directory list using github API * * version 2.1.1 * + Extensions hashes load if missing * + clean up other.json file * * version 2.1.0 * + Moving project to afuj * + Add extensions hashes * * version 2.0.3 * + Prevent empty files to be scanned * + Immediately show the listing of files having detected as being a virus (blacklist) or * containing a virus (edited file having a virus load) * * version 2.0.2 * + Revert to PHP 8.0 compatibility * * version 2.0.1 * + Add the "_COOKIE" pattern in aesecure_quickscan_pattern.json * * version 2.0 * + PHP 8.2 compatibility * + look for hashes in hashes directory * * version 1.2 * + Rewrite for downloading all settings and signatures files from GitHub * + Add a lot more signatures in these lists: blacklist, whitelist, other and edited json * + Ad more patterns for viruses detection * + Reformat the code of the scanner * * version 1.1.12 * + Add support for Grr, mediawiki, piwik and pmb * + Solve an issue with session_start() for some hosts * * version 1.1.11 * + Add support for Grav * * version 1.1.10 * + Add support for phpMyAdmin * * version 1.1.9 * + Solve an error with session_start (on some hoster, the creation of the session gives a fatal error due to incorrect path) * * version 1.1.8 * + Solve an error with the link to the FAQ * + Better handling of languages files * * version 1.1.7 * + Add localizations (class aeSecureLanguage) * * version 1.1.6 * + Improve the detection of the list of files by immediatly skipping whitelisted files. On a site of 4.900 files, the scanner will be able to detect that * only 11 files should be scanned if 4.889 are already white listed. This way, the scanner will be really fast. * * version 1.1.5 * + Small change to correctly handle Joomla 3.5.0 with a newer way to determine the version number (no more dollar sign before variables name) * * version 1.1.4 * + Add aesecure_quickscan.whitelist.json as a file to download from avonture.be to speed up the processing and reduce the number of false positive * + Add a lot of new signatures in the blacklist * * version 1.1.3 * + Add a timeout for the CURL request * * version 1.1.2 * + Support of concrete5, contao (aka previously called Typolight), dolibarr, eFront, EspoCRM, formaLMS, phpBB, phpList, * SilverStripe and x3cms * * version 1.1.1 * + Monitored folders for Joomla: files present in a native Joomla's folder (part of the CMS) will * be analysed * - If not part of the distribution (intrusion) * - If part of the distribution but with an another hash (hacked file or, at least, altered one) * * version 1.1.0 * + Support CakePHP, Drupal, Magento, PrestaShop (on top of Joomla and WordPress) * + Improved security by no more loading core Joomla files * + Advanced menu (left side) * + Allow to activate debug and expert mode (without any changes in the code) * + Allow to specify how many files to process by cycle (without any changes in the code) * + Allow to specify with type of files to ignore (archives, images, medias, ...) * * Avoid __DIR__. * * __DIR__ is the folder where the running script is started so, perhaps, things like * c:/sites/hacked/. In most of case, it's correct because the script file has been * saved there... but not always: think to symbolic links. * The file can be saved f.i. in c:/repository/aesecure_quickscan/aesecure_quickscan.php * and a symlink has been made in c:/sites/hacked/. We want that __DIR__ points to the * hacked site but won't be the case with symlink. __DIR__ is where the file IS REALLY. * * So, don't use __DIR__ but c:/sites/hacked/ */ define('REPO', 'https://github.com/AFUJ/quickscan/'); define('DIR', str_replace('/', DIRECTORY_SEPARATOR, dirname((string) $_SERVER['SCRIPT_FILENAME']))); define('FILE', str_replace('/', DIRECTORY_SEPARATOR, basename((string) $_SERVER['SCRIPT_FILENAME']))); // Don't allow to kill this script when demo mode is enabled // Don't show the "Enable expert mode" checkbox in Demo mode define('DEMO', false); define('DEBUG', false); // Enable debugging (Note: there is no progress bar in debug mode) define('FULLDEBUG', false); // Output a lot of information define('VERSION', '2.2.3'); // Version number of this script define('EXPERT', false); // Display Kill file button and allow to specify a folder define('MAX_SIZE', 1 * 1024 * 1024); // One megabyte: skip files when filesize is greater than this max size. define('MAXFILESBYCYCLE', 500); // Number of files to process by cycle, reduce this figure if you receive HTTP error 504 - Gateway timeout define('CONTEXT_NBRCHARS', 100); // When a suspicious pattern is found, the portion of code where this pattern is found will be displayed. The portion is xxx characters before the pattern; the pattern and the same number of characters after it. define('SHOWMD5', false); // Allow to generate a hash file define('PROGRESSBARFREQUENCY', 3); // Frequency of updates for the progress bar. In seconds. define('MEMORY_LIMIT', '256M'); // DEBUG MODE ONLY - Maximum memory limit that will be used define('CURL_TIMEOUT', 2); // Max number of seconds before the timeout when requesting a JSON file from avonture.be // Download URL for the file with CMS hashes define('DOWNLOAD_URL', 'https://raw.githubusercontent.com/AFUJ/quickscan/master/'); define('DOWNLOAD_URL_DIR', 'https://api.github.com/repos/AFUJ/quickscan/contents/'); define('MD5', ''); define('DIRNOTFOUND', 'Directory not found'); // List of extensions, by "category". Add an extension if you want to skip that files when // skipping the category define('ExtArchives', '7z, bak, gz, gzip, jpa, tar, zip'); define('ExtDocuments', 'doc, docx, pdf, ppt, pptx, xls, xlsx'); define('ExtFonts', 'eot, otf, ttf, ttf2, woff, woff2'); define('ExtImages', 'bmp, eps, gif, ico, icon, jpeg, jpg, png, psd, svg, tiff, webp'); define('ExtMedia', 'css, js, less'); define('ExtSoundMovies', 'aiff, asf, avi, fla, flv, f4v, m4v, mkv, mov, mp3, mp4, mpeg, mpg, ogg, ogv, swf, wav, webm, wma'); define('ExtText', 'ini, json, log, md, mo, po, sql, text, txt, xml, xsl'); define('CRLF', "\r\n"); define('DS', DIRECTORY_SEPARATOR); // Register error handling functions set_error_handler(function ($code, $string, $file, $line): never { throw new ErrorException($string, 0, $code, $file, $line); }); register_shutdown_function(function () { $memory = 'ini_get memory_limit=' . ini_get('memory_limit') . ' | ' . 'memory used=' . aeSecureFct::getMemoryUsed(); $error = error_get_last(); }); class aeSecureDebug { /** * Debugging mode state (On / Off). * * * @access private */ private static bool $debugMode = false; /** * Instantiate the class. * * @param bool $debugMode False will hide errors in the browser * True will activate a verbose mode * * @return void */ public function __construct($debugMode = false) { // Informs PHP where to store errors ini_set('error_log', DIR . 'aesecure_quickscan_error_log'); // Initialize the debug mode self::setDebugMode($debugMode); } /** * Set the debugging mode. * * @param bool $onOff * * @return void */ public static function setDebugMode($onOff = false) { static::$debugMode = $onOff; // When debug mode is on, we want to see every messages; even notice. if (true === static::$debugMode) { ini_set('display_errors', '1'); ini_set('display_startup_errors', '1'); ini_set('html_errors', '1'); ini_set('docref_root', 'http://www.php.net/'); ini_set( 'error_prepend_string', "
Your system configuration doesn\'t allow to download the file.
' . 'Please click ' . 'here to ' . 'manually download the file, then open your ' . 'FTP client and send the downloaded file to your ' . 'website folder.
' . 'Once this is done, just refresh this page.
' . 'Note: the filename should be ' . static::$sFileName . '
'; return $sReturn; } /** * Detect if the CURL library is loaded. */ private function iscURLEnabled() { return (!function_exists('curl_init') && !function_exists('curl_setopt') && !function_exists('curl_exec') && !function_exists('curl_close')) ? false : true; } /** * If the file is there and has a size of 0 byte, * it's a failure, the file wasn't downloaded. */ private function removeIfNull() { if (file_exists(static::$sFileName)) { if (filesize(static::$sFileName) < 10) { unlink(static::$sFileName); } } } } class aeSecureDownload { /** * Download a file from GitHub like "aesecure_quickscan_pattern.json", ... * See the DOWNLOAD_URL constant for the URL. * * @param [type] $file * @param mixed $uri * * @return void */ public static function get($file, $uri) { try { // Try to download $aeDownload = new Download('Quickscan'); $aeDownload->debugMode(DEBUG); // Be sure to have only one "/" and not two if (trim('' !== $uri)) { $uri = ltrim(rtrim((string) $uri, '/'), '/') . '/'; } $url = rtrim(DOWNLOAD_URL, '/') . '/' . $uri . basename((string) $file); $aeDownload->setURL($url); $aeDownload->setFileName($file); $wReturn = $aeDownload->download(); if (0 !== $wReturn) { $sErrorMsg = $aeDownload->getErrorMessage($wReturn); } } catch (Exception $e) { $wReturn = 1001; $sErrorMsg = $e->getMessage(); } unset($aeDownload); } /** * Download a directory from GitHub like "hashes/J!extensions", ... * See the DOWNLOAD_URL_DIR constant for the URL. * * @param [type] $dir * @param mixed $uri * * @return void */ public static function getDir($dir) { try { // Try to download $aeDownload = new Download('Quickscan'); $aeDownload->debugMode(DEBUG); // Be sure to have only one "/" and not two $url = rtrim(DOWNLOAD_URL_DIR, '/') . '/' . $dir.'?ref=master'; $aeDownload->setURL($url); $wReturn = $aeDownload->downloadGetDir(); if (is_int($wReturn)) { // error $sErrorMsg = $aeDownload->getErrorMessage($wReturn); } else { // contains a directory $json_array = json_decode($wReturn); unset($aeDownload); return $json_array ; } } catch (Exception $e) { $wReturn = 1001; $sErrorMsg = $e->getMessage(); } unset($aeDownload); } } /** * Add localization; read an external json file with translations. */ class aeSecureLanguage { public const DEFAULT_LANGUAGE = 'en-GB'; // Filename pattern for languages files public const LANG_FILE = 'aesecure_quickscan_lang_%s.json'; // Hard-coded list of supported languages // @See https://github.com/afuj/quickscan for xxx_lang_xxxx.json files public const SUPPORTED_LANGUAGES = 'en;en-GB;fr;fr-FR;nl;nl-BE'; private string $_filename = ''; private $_lang = null; private $_arrLanguage = null; private bool $_bLoaded = false; private $supportedLanguages = null; private $browserLanguages = null; protected static $instance = null; public function __construct($lang = null) { $aeSession = aeSecureSession::getInstance(); if (null == $lang) { $lang = str_replace('_', '-', (string) aeSecureFct::getParam('lang', 'string', '', 5)); } // Initialize the list of supported languages $this->supportedLanguages = explode(';', self::SUPPORTED_LANGUAGES); // Get the list of languages supported by the Browser and by aeSecure // (presence of the language's file) self::getBrowserLanguage(); if (in_array($lang, $this->supportedLanguages)) { // Perfect match // The language (f.i. nl-BE) is supported; we've a nl-BE.json file; use it $result = $lang; } elseif (in_array(substr((string) $lang, 0, 2), $this->supportedLanguages)) { // If the user ask for f.i. en-US and we've a file for "en", use that file. // For instance en-GB $result = substr((string) $lang, 0, 2); } else { // No, not found. Use the languages supported by the browser and check if aeSecure // support that language $result = ''; // Search for a perfect match so if the language is en_US, try to find en_US.json // and not en_GB.json foreach ($this->browserLanguages as $lang => $value) { if (in_array($lang, $this->supportedLanguages)) { $result = $lang; break; } } // If $result is still empty, no perfect match so search on the language and not // language and country. So, if the language is en-US and if a file en-GB is found, get it. if ('' == $result) { $result = 'en-GB'; foreach ($this->browserLanguages as $lang => $value) { // Check if there is a language file (f.i. if $lang is "fr" // (and not "fr_FR"), the glob function will return // the list of files like fr*.json if (in_array(substr((string) $lang, 0, 2), $this->supportedLanguages)) { $result = substr((string) $lang, 0, 2); break; } } } // Still not? Use en-GB by default if ('' == $result) { $result = self::DEFAULT_LANGUAGE; } } $aeSession->set('Lang', $lang); // Max 5 characters $lang = substr((string) $lang, 0, 5); // Just be sure to have en-GB and not f.i. EN-GB or en_gb $lang = strtolower(substr($lang, 0, 2)) . '-' . strtoupper(substr($lang, -2)); if ('en-US' == $lang) { $lang = 'en-GB'; } $this->_lang = $lang; $this->_filename = DIR . DS . sprintf(self::LANG_FILE, $this->_lang); $this->_bLoaded = false; if (!file_exists($this->_filename)) { // Try to download if not present aeSecureDownload::get($this->_filename, 'settings/'); } if (file_exists($this->_filename)) { $string = file_get_contents($this->_filename); $string = str_replace('\\u', '\u', $string); if (null === json_decode($string, true, 512, JSON_THROW_ON_ERROR)) { die('There is a problem in ' . $this->_filename . '. Probably an invalid json file' .
html_entity_decode($string) . '');
}
$this->_arrLanguage = json_decode($string, true, 512, JSON_THROW_ON_ERROR);
$this->_bLoaded = true;
}
// If the parametrized file isn't found (f.i. the user set fr-FR has
// preferred language and the file is not
// present), then use by default en-GB
if (!$this->_bLoaded) {
// Try to download if not present
$this->_filename = DIR . DS . sprintf(self::LANG_FILE, self::DEFAULT_LANGUAGE);
if (!file_exists($this->_filename)) {
aeSecureDownload::get($this->_filename, 'settings/');
}
if (file_exists($this->_filename)) {
$string = file_get_contents($this->_filename);
$string = str_replace('\\u', '\u', $string);
if (null === json_decode($string, true, 512, JSON_THROW_ON_ERROR)) {
die('There is a problem in ' . $this->_filename . '. ' .
'Probably an invalid json file ' .
html_entity_decode($string) . '');
}
$this->_arrLanguage = json_decode($string, true, 512, JSON_THROW_ON_ERROR);
$this->_bLoaded = true;
}
}
// Still not? Use the first language file that is present
if ((!$this->_bLoaded) && (count($this->supportedLanguages) > 0)) {
foreach ($this->supportedLanguages as $key => $value) {
$this->_filename = DIR . DS . sprintf(self::LANG_FILE, $value);
if (file_exists($this->_filename)) {
$string = file_get_contents($this->_filename);
$string = str_replace('\\u', '\u', $string);
if (null === json_decode($string, true, 512, JSON_THROW_ON_ERROR)) {
die('There is a problem in ' . $this->_filename .
'. Probably an invalid json file ' .
html_entity_decode($string) . '');
}
$this->_arrLanguage = json_decode($string, true, 512, JSON_THROW_ON_ERROR);
$this->_bLoaded = true;
$this->_lang = $value;
break;
}
}
}
return true;
}
public function ready(): bool
{
return $this->_bLoaded;
}
/**
* Translation functionality, search the CODE in the json file and returns its
* value (the translated text).
*/
public function get(string $code): string
{
$sText = '';
if (isset($this->_arrLanguage[$code])) {
$sText = $this->_arrLanguage[$code];
}
return $sText;
}
public function getlang(): string
{
return $this->_lang;
}
/**
* $language can be initialized or not. If not, the script will detect supported
* languages as defined in the user's browser. If initialized, should be something
* like 'en-GB', 'fr-FR', ...
*/
public static function getInstance(?string $lang = null): self
{
if (null === self::$instance) {
self::$instance = new aeSecureLanguage($lang);
}
return self::$instance;
}
/**
* Read the HTTP_ACCEPT_LANGUAGE browser info to determine the best language
* to use for aeSecure based on the browser's preferences.
*
* @return string Returns f.i. en-GB, fr-FR, nl-NL, ...
*/
private function getBrowserLanguage(): string
{
$default = null;
$httplanguages = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
if (empty($httplanguages)) {
return $default;
}
$this->browserLanguages = [];
$result = '';
// $this->browserLanguages is an array, sorted by priority order, of the
// supported languages; for instance:
// array
// 'fr' => float 1
// 'en_US' => float 0.8
// 'en' => float 0.6
foreach (preg_split('/,\s*/', (string) $httplanguages) as $accept) {
$result = preg_match('/^([a-z]{1,8}(?:[-_][a-z]{1,8})*)(?:;\s*' .
'q=(0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?))?$/i', (string) $accept, $match);
if (!$result) {
continue;
}
$quality = (isset($match[2]) ? (float)$match[2] : 1.0);
$countries = explode('-', $match[1]);
$region = array_shift($countries);
$country_sub = explode('_', $region);
$region = array_shift($country_sub);
foreach ($countries as $country) {
$this->browserLanguages[$region . '-' . strtoupper($country)] = $quality;
}
foreach ($country_sub as $country) {
$this->browserLanguages[$region . '-' . strtoupper($country)] = $quality;
}
$this->browserLanguages[$region] = $quality;
}
return true;
}
}
/**
* A few helping functions.
*/
class aeSecureFct
{
/**
* Remove special characters, f.i clean('a|"bc!@£de^&$f g') will return 'abcdef-g'.
*/
public static function sanitize(string $string): string
{
// Replaces all spaces with hyphens.
$string = str_replace(' ', '-', $string);
// Removes special chars.
return (string) preg_replace('/[^A-Za-z0-9\-]/', '', $string);
}
/**
* Generic function for adding a js in the HTML response.
*
* @param type $localfile
* @param type $weblocation
* @param mixed $defer
*
* @return string
*/
public static function addJavascript($localfile, $weblocation = '', $defer = false)
{
$return = '';
// Perhaps the script (aesecure_quickscan.php) is a symbolic link so __DIR__
// is the folder where the real file can be found and SCRIPT_FILENAME his link,
// the line below should therefore not be used anymore
if (is_file(str_replace('/', DS, dirname((string) $_SERVER['SCRIPT_FILENAME'])) . DS . $localfile)) {
$return = '';
} else {
if ('' != $weblocation) {
$return = '';
}
}
return $return;
}
/**
* Generic function for adding a css in the HTML response.
*
* @param type $localfile
* @param type $weblocation
*
* @return string
*/
public static function addStylesheet($localfile, $weblocation = '')
{
$return = '';
// Perhaps the script (aesecure_quickscan.php) is a symbolic link so __DIR__ is the
// folder where the real file can be found and SCRIPT_FILENAME his link, the line
// below should therefore not be used anymore
if (is_file(str_replace('/', DS, dirname((string) $_SERVER['SCRIPT_FILENAME'])) . DS . $localfile)) {
$return = '';
} else {
if ('' != $weblocation) {
$return = '';
}
}
return $return;
}
public static function human_filesize($bytes, $decimals = 2)
{
$sz = 'BKMGTP';
$factor = intval(floor((strlen((string) $bytes) - 1) / 3));
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor];
}
/**
* Return a string like '1 an 10 mois 6 jours 3 heures'... ie the age of f.i. a file.
*
* echo aeSecureFct::time_elapsed_string(filemtime($filename))
*/
public static function time_elapsed_string(int $ptime): string
{
$diff = time() - $ptime;
$calc_times = [];
$timeleft = [];
// Prepare array, depending on the output we want to get.
$calc_times[] = ['an', 'ans', 31557600];
$calc_times[] = ['mois', 'mois', 2592000];
$calc_times[] = ['jour', 'jour', 86400];
$calc_times[] = ['heure', 'heures', 3600];
$calc_times[] = ['minute', 'minutes', 60];
$calc_times[] = ['seconde', 'secondes', 1];
foreach ($calc_times as $timedata) {
[$time_sing, $time_plur, $offset] = $timedata;
if ($diff >= $offset) {
$left = floor($diff / $offset);
$diff -= ($left * $offset);
$timeleft[] = "{$left} " . (1 == $left ? $time_sing : $time_plur);
}
}
return $timeleft ? (time() > $ptime ? null : '-') . implode(' ', $timeleft) : 0;
}
/**
* Return true when the call to the php script has been done through an ajax request.
*/
public static function isAjaxRequest(): bool
{
$bAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
('XMLHttpRequest' == $_SERVER['HTTP_X_REQUESTED_WITH']));
return $bAjax;
}
/**
* Return the memory usage when this function is called. By calling this function
* at different place in the code, it's then possible to determine which part is
* eating a lot of memory.
*
* @return type
*/
public static function getMemoryUsed()
{
$mem_usage = memory_get_peak_usage(true);
return ($mem_usage < 1048576)
? round($mem_usage / 1024, 2) . ' kb'
: round($mem_usage / 1048576, 2) . ' mb';
}
/**
* Safely read values from posted forms ($_POST).
*
* @param mixed $type
*/
public static function getParam(string $name, $type = 'string', mixed $default = '', int $maxlen = 0): mixed
{
$tmp = '';
$return = $default;
if (isset($_POST[$name])) {
if (in_array($type, ['int', 'integer'])) {
$return = htmlspecialchars((string) $_POST[$name], ENT_QUOTES); // filter_input(INPUT_POST, $name, FILTER_SANITIZE_NUMBER_INT);
} elseif ('boolean' == $type) {
// false = 5 characters
$tmp = substr(htmlspecialchars((string) $_POST[$name], ENT_QUOTES), 0, 5); // substr(filter_input(INPUT_POST, $name, FILTER_SANITIZE_STRING), 0, 5);
$return = (in_array(strtolower($tmp), ['on', 'true'])) ? true : false;
} elseif ('string' == $type) {
$return = htmlspecialchars((string) $_POST[$name], ENT_QUOTES); //filter_input(INPUT_POST, $name, FILTER_SANITIZE_STRING);
if ($maxlen > 0) {
$return = substr($return, 0, $maxlen);
}
} elseif ('unsafe' == $type) {
$return = $_POST[$name];
}
} else {
$aeSession = aeSecureSession::getInstance();
// Get from the $_GET only in debug mode or for very few parameters like "lang" (to allow to switch between
// languages) and "aes" (boolean set to 1 when QuickScan is started from within the aeSecure Firewall interface)
if ((true === $aeSession->get('Debug', DEBUG)) || in_array($name, ['aes', 'lang'])) {
if (isset($_GET[$name])) {
if (in_array($type, ['int', 'integer'])) {
$return = htmlspecialchars((string) $_GET[$name], ENT_QUOTES); //filter_input(INPUT_GET, $name, FILTER_SANITIZE_NUMBER_INT);
} elseif ('boolean' == $type) {
// false = 5 characters
$tmp = substr(htmlspecialchars((string) $_GET[$name], ENT_QUOTES), 0, 5);
$return = (in_array(strtolower($tmp), ['1', 'on', 'true'])) ? true : false;
} elseif ('string' == $type) {
$return = htmlspecialchars((string) $_GET[$name], ENT_QUOTES);
} elseif ('unsafe' == $type) {
$return = $_GET[$name];
}
}
}
}
if ('boolean' == $type) {
$return = (in_array($return, ['on', '1']) ? true : false);
}
return $return;
}
}
/**
* Logging functionality.
*/
class aeSecureLog
{
private $_sLogFile = null;
protected static $instance = null;
public function __construct($sLogFile, $killFile = true)
{
$this->_sLogFile = $sLogFile;
if ((true == $killFile) && (file_exists($this->_sLogFile)) && (is_writable($this->_sLogFile))) {
unlink($this->_sLogFile);
}
return true;
}
public function kill()
{
if ((file_exists($this->_sLogFile)) && (is_writable($this->_sLogFile))) {
unlink($this->_sLogFile);
}
}
public function filename()
{
return $this->_sLogFile;
}
/**
* Add a line in the $sLogFile log file.
*
* @param type $sLine
*
* @return type
*/
public function addLog($sLine)
{
if (!is_writable(dirname((string) $this->_sLogFile))) {
return;
}
if ('' != $this->_sLogFile) {
if ($handle = fopen($this->_sLogFile, 'a')) {
fwrite($handle, (string) ($sLine . "\n"));
fclose($handle);
}
}
}
/**
* @param type $sLogFile Name of the logfile that will be used
* @param type $killFile Default True : kill the logfile if present when starting the run
*
* @return type
*/
public static function getInstance($sLogFile = null, $killFile = false)
{
if (null != $sLogFile) {
if (null === self::$instance) {
self::$instance = new aeSecureLog($sLogFile, $killFile);
}
}
return self::$instance;
}
}
/**
* Working with files and folders.
*/
class aeSecureFiles
{
protected $aeSession = null;
protected $aeLog = null;
protected static $instance = null;
public function __construct()
{
$this->aeSession = aeSecureSession::getInstance();
}
public function SeeFile(?string $filename = null): ?string
{
$return = null;
if (null != $filename) {
if ((is_file($filename)) && is_readable($filename)) {
$return = file_get_contents($filename);
}
}
return $return;
}
/**
* Kill physically a file.
*
* @return type -1 if the file has been removed successfully
*/
public function KillFile(?string $filename = null): int
{
$return = 0;
if (null == $filename) {
return $return;
}
if ((is_file($filename)) && is_writable($filename)) {
try {
if (true === $this->aeSession->get('Debug', DEBUG)) {
if (null == $this->aeLog) {
$this->aeLog = aeSecureLog::getInstance();
}
$this->aeLog->addLog('*** Kill ' . $filename . ' ***');
}
unlink($filename);
if (!is_file($filename)) {
$return = -1;
}
} catch (Exception $e) {
$return = -999;
}
} else {
$return = -50;
}
echo $return;
}
/**
* Remove recursively folders
* (f.i. rrmdir(__DIR__/hashes/cms/joomla/2.5.27) will kill the full tree below
* the specified folder).
*
* @param bool $killroot If true, the folder himself will be removed.
* rrmdir(__DIR__/hashes/cms/joomla/2.5.27, true) ==>
* remove folder 2.5.27 too and not only his children
*/
public function rrmdir(string $folder, bool $killroot = false, array $arrIgnoreFiles = ['.htaccess', 'index.html']): bool
{
try {
if (!is_dir($folder) && file_exists($folder)) {
try {
if (!is_writable($folder)) {
$bResult = @chmod($folder, octdec('755'));
}
return @unlink($folder);
} catch (Exception $e) {
return false;
}
}
foreach (scandir($folder) as $file) {
if ('.' == $file || '..' == $file || in_array($file, $arrIgnoreFiles)) {
continue;
}
if (!self::rrmdir($folder . DS . $file, $killroot, $arrIgnoreFiles)) {
if (!is_writable($folder . DS . $file)) {
$bResult = chmod($folder . DS . $file, octdec('755'));
}
if (!self::rrmdir($folder . DS . $file, $killroot, $arrIgnoreFiles)) {
return false;
}
}
}
// Remove the folder only if not read-only and empty
if ((is_writable($folder)) && (0 === count(glob("$folder/*")))) {
@rmdir($folder);
}
return true;
} catch (Exception $ex) {
if (true === $this->aeSession->get('Debug', DEBUG)) {
echo '' . print_r($ex, true) . ''; } return false; } } public static function getInstance(): self { if (null === self::$instance) { self::$instance = new aeSecureFiles(); } return self::$instance; } public static function getFileMimeType(string $filename): ?string { $mime_type = null; if (is_file($filename)) { $finfo = null; if (class_exists('info')) { // return mime type $finfo = new finfo(FILEINFO_MIME); } if ($finfo) { $file_info = $finfo->file($filename); $mime_type = substr($file_info, 0, strpos($file_info, ';')); } else { // Requires to enable "extension=php_fileinfo.dll" on a Windows machine if (function_exists('finfo_open')) { $finfo = finfo_open(FILEINFO_MIME_TYPE); } if ($finfo) { $mime_type = finfo_file($finfo, $filename); finfo_close($finfo); } } } return $mime_type; } /** * Return true if the file contains text content. */ public static function isTextFileContent(string $filename): string { $mimeType = aeSecureFiles::getFileMimeType($filename); $isTextType = false; // Try to determine if it's a text file; in that case the MIME type is // something like text/plain or text/richtext i.e. starting with the "text/" prefix $isTextType = ('text/' == substr((string) $mimeType, 0, 5)); // A few mimetype are also text like application/javascript if (!$isTextType) { $isTextType = in_array($mimeType, ['application/xml']); // Still not? Try to use the file's extension to determine this if (!$isTextType) { $ext = pathinfo($filename, PATHINFO_EXTENSION); $isTextType = in_array($ext, ['css', 'csv', 'eot', 'html', 'htm', 'ini', 'js', 'json', 'php', 'sh', 'svg', 'txt', 'xml']); } } return $isTextType; } } /** * Session helper. */ class aeSecureSession { protected static $instance = null; protected static $prefix = 'aeS_'; public function __construct($bDestroy = false) { // server should keep session data for AT LEAST 1 hour try { ini_set('session.gc_maxlifetime', 3600); // each client should remember their session id for EXACTLY 1 hour session_set_cookie_params(3600); } catch (\Exception $exception) { } if (!isset($_SESSION)) { try { session_start(); } catch (Exception $e) { // 1.1.9 - On some hoster the path where to store session is incorrectly // set and this gives a fatal error // Handle this and use the /tmp folder in this case. try { session_destroy(); session_save_path(sys_get_temp_dir()); try { session_start(); } catch (Exception $e) { // 1.1.12 // Still not? Use the current dir session_destroy(); session_save_path(DIR); session_start(); } } catch (\Exception $exception) { } } } if ($bDestroy) { session_destroy(); } return true; } public static function getInstance($bDestroy = false): self { if (null === self::$instance) { self::$instance = new aeSecureSession($bDestroy); } return self::$instance; } public static function set(string $name, mixed $value): void { $_SESSION[static::$prefix . $name] = $value; } public static function get(string $name, mixed $defaultvalue): mixed { return $_SESSION[static::$prefix . $name] ?? $defaultvalue; } } /** * CMS functionnalities. */ class aeSecureCMS { public const SUPPORTED_CMS = 'aesecure_quickscan_supported_cms.json'; /** * Try to determine if the site is a CMS site and in that case, get the CMS version. */ public static function getInfo(string $directory): array { $CMS = ''; $MainVersion = ''; $Version = ''; $FullVersion = ''; // Try to derive the root folder $root = rtrim($directory, DS) . DS; // Get the list of CMS (it's a json string stored in a constant) // and try to find a CMS $file = DIR . DS . self::SUPPORTED_CMS; if (!is_file($file)) { aeSecureDownload::get($file, 'settings/'); } if (!is_file($file)) { die(sprintf('Sorry, the file [%s] is missing', basename($file))); } $arrCMS = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); foreach ($arrCMS as $key => $value) { if (method_exists('aeSecureCMS', 'is' . $key)) { $method = self::class . '::is' . $key; // PHP 8.2 [$return, $CMS, $Filename, $FullVersion, $MainVersion, $Version] = call_user_func($method, $root); if (true === $return) { break; } } } return [(string) $CMS, (string) $FullVersion, (string) $MainVersion, (string) $Version, (string) $root]; } /** * Detect if the CMS is Cake. */ private static function iscake(string $root): array { $filename = rtrim($root, DS) . DS . 'admin' . DS . 'cake' . DS . 'config' . DS . 'config.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*\\Cake\.version\'\] *= *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'Cake', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is Concrete5. */ private static function isConcrete5(string $root): array { $filename = rtrim($root, DS) . DS . 'concrete' . DS . 'config' . DS . 'concrete.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*\'version\' *\=\> *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'Concrete5', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is Contao. */ private static function isContao(string $root): array { $filename = rtrim($root, DS) . DS . 'system' . DS . 'config' . DS . 'constants.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*VERSION\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $MainVersion = (count($arrMatches) > 0) ? $arrMatches[1][0] : ''; preg_match('/.*BUILD\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = $MainVersion . '.' . ((count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''); return [true, 'Contao', $filename, $FullVersion, $MainVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is Dolibarr. */ private static function isDolibarr(string $root): array { $filename = rtrim($root, DS) . DS . 'htdocs' . DS . 'filefunc.inc.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*DOL_VERSION\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'Dolibarr', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is Drupal. */ private static function isDrupal(string $root): array { $filename = rtrim($root, DS) . DS . 'modules' . DS . 'system' . DS . 'system.module'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*define\(\'VERSION\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? $arrMatches[1][0] : ''; return [true, 'Drupal', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is eFront. */ private static function iseFront(string $root): array { $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'configuration.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*G_VERSION_NUM\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'eFront', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is EspoCRM. */ private static function isEspoCRM(string $root): array { $filename = rtrim($root, DS) . DS . 'data' . DS . 'config.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*version\' \=\> *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'EspoCRM', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is FormaLMS. */ private static function isFormaLMS(string $root): array { $filename = rtrim($root, DS) . DS . 'appCore' . DS . 'index.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*_file_version_\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'FormaLMS', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is Grav. */ private static function isGrav(string $root): array { $filename = rtrim($root, DS) . DS . 'system' . DS . 'defines.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*define\(\'GRAV_VERSION\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'Grav', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is GRR. */ private static function isGrr(string $root): array { $filename = rtrim($root, DS) . DS . 'include' . DS . 'misc.inc.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/\\$version_grr[[:blank:]]=[[:blank:]]"(.*)"/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'GRR', $filename, $FullVersion, $FullVersion, $FullVersion, $root]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is Joomla. */ private static function isJoomla(string $root): array { if ( (($wpos = strpos($root, DS . 'administrator' . DS)) > 0) || (($wpos = strpos($root, DS . 'bin' . DS)) > 0) || (($wpos = strpos($root, DS . 'cache' . DS)) > 0) || (($wpos = strpos($root, DS . 'cli' . DS)) > 0) || (($wpos = strpos($root, DS . 'components' . DS)) > 0) || (($wpos = strpos($root, DS . 'images' . DS)) > 0) || (($wpos = strpos($root, DS . 'includes' . DS)) > 0) || (($wpos = strpos($root, DS . 'language' . DS)) > 0) || (($wpos = strpos($root, DS . 'layouts' . DS)) > 0) || (($wpos = strpos($root, DS . 'libraries' . DS)) > 0) || (($wpos = strpos($root, DS . 'logs' . DS)) > 0) || (($wpos = strpos($root, DS . 'media' . DS)) > 0) || (($wpos = strpos($root, DS . 'modules' . DS)) > 0) || (($wpos = strpos($root, DS . 'plugins' . DS)) > 0) || (($wpos = strpos($root, DS . 'templates' . DS)) > 0) || (($wpos = strpos($root, DS . 'tmp' . DS)) > 0) ) { $root = substr($root, 0, $wpos); } // Now, $root is probably the website root. Check if we can found a Joomla!® instance $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'src' . DS . 'Version.php'; // Now, $root is probably the website root. Check if we can found a Joomla!® instance if (!file_exists($filename)) { $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'cms' . DS . 'version' . DS . 'version.php'; } if (!file_exists($filename)) { $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'joomla' . DS . 'version.php'; } if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/MAJOR_VERSION = (\d+)/'; if (preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE) > 0) { // As from Joomla 4, RELEASE, DEV_LEVEL, ... are removed. $arr = ['MAJOR_VERSION' => 0, 'MINOR_VERSION' => 0, 'PATCH_VERSION' => 0, 'RELDATE' => 0, 'RELTIME' => 0, 'RELTZ' => 0]; } else { $arr = ['RELEASE' => 0, 'DEV_LEVEL' => 0, 'DEV_STATUS' => 0, 'RELDATE' => 0, 'RELTIME' => 0, 'RELTZ' => 0]; } foreach ($arr as $key => $value) { // Use [[:blank:]] and not just a space character because sometimes the // version.php file contains something other than a space // this is the case for J1.5.26 // Note : since J3.5, variables are now constants and without the preceding dollar sign so // before J3.5, it was $RELEASE f.i., since 3.5, it's just RELEASE $pattern = '/.*\$?' . $key . "[[:blank:]]*=[[:blank:]]*'?([0-9A-Za-z\-\.]*)'?/"; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); if (count($arrMatches) > 0) { $arr[$key] = $arrMatches[1][0]; } } if (isset($arr['MAJOR_VERSION'])) { // Joomla 3.8.2 or greater $MainVersion = $arr['MAJOR_VERSION'] . '.' . $arr['MINOR_VERSION']; $Version = $arr['MAJOR_VERSION'] . '.' . $arr['MINOR_VERSION'] . '.' . $arr['PATCH_VERSION']; $FullVersion = $arr['MAJOR_VERSION'] . '.' . $arr['MINOR_VERSION'] . '.' . $arr['PATCH_VERSION'] . ' ' . '(' . $arr['RELDATE'] . ' ' . $arr['RELTIME'] . ' ' . $arr['RELTZ'] . ')'; } else { $MainVersion = $arr['RELEASE']; $Version = $arr['RELEASE'] . '.' . $arr['DEV_LEVEL']; $FullVersion = $arr['RELEASE'] . '.' . $arr['DEV_LEVEL'] . ' (' . $arr['DEV_STATUS'] . ') ' . '(' . $arr['RELDATE'] . ' ' . $arr['RELTIME'] . ' ' . $arr['RELTZ'] . ')'; } return [true, 'Joomla', $filename, (string) $FullVersion, (string) $MainVersion, (string) $Version]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is Magento. */ private static function isMagento(string $root): array { $filename = rtrim($root, DS) . DS . 'app' . DS . 'Mage.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $arr = ['major' => 0, 'minor' => 0, 'revision' => 0, 'patch' => 0]; foreach ($arr as $key => $value) { preg_match('/.*\'' . $key . '\' *=\> *\'(\\d+)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); if (count($arrMatches) > 0) { (string) $arr[$key] = $arrMatches[1][0]; } } $FullVersion = $arr['major'] . '.' . $arr['minor'] . '.' . $arr['revision'] . '.' . $arr['patch']; return [true, 'Magento', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is MediaWiki. */ private static function isMediaWiki(string $root): array { $filename = rtrim($root, DS) . DS . 'includes' . DS . 'DefaultSettings.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/.*\$wgVersion \= \'(\\d+\\.\\d+\\.\\d+)\'/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'MediaWiki', $filename, $FullVersion, $FullVersion, $FullVersion, $root]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is phpBB. */ private static function isphpBB(string $root): array { $filename = rtrim($root, DS) . DS . 'styles' . DS . 'prosilver' . DS . 'style.cfg'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/.*phpbb_version \= (\\d+\\.\\d+\\.\\d+)/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'phpBB', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is phpList. */ private static function isphpList(string $root): array { $filename = rtrim($root, DS) . DS . 'public_html' . DS . 'lists' . DS . 'admin' . DS . 'init.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*"VERSION", "(.*)"/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'phpList', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the framework is phpMyAdmin. */ private static function isphpmyadmin(string $root): array { $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'config.class.php'; if (!file_exists($filename)) { $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'config.php'; } if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/.*PMA_VERSION\', *\'(\\d+\\.\\d+\\.\\d+)\'/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'phpmyadmin', $filename, $FullVersion, $FullVersion, $FullVersion, $root]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the framework is PMP. */ private static function isPMB(string $root): array { $filename = rtrim($root, DS) . DS . 'includes' . DS . 'config.inc.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/\\$pmb_version_brut[[:blank:]]=[[:blank:]]"(\\d+\\.\\d+\\.\\d+(.*))"/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'PMB', $filename, $FullVersion, $FullVersion, $FullVersion, $root]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is Prestashop. */ private static function isPrestashop(string $root): array { $filename = rtrim($root, DS) . DS . 'config' . DS . 'settings.inc.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/.*_PS_VERSION_\', *\'(.*)\'/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'Prestashop', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is SilverStripe. */ private static function isSilverStripe(string $root): array { $filename = rtrim($root, DS) . DS . 'cms' . DS . 'silverstripe_version'; if (file_exists($filename)) { $FullVersion = (string) file_get_contents($filename); return [true, 'silverstripe', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is WordPress. */ private static function isWordPress(string $root): array { $filename = ''; if (strpos($root, 'wp-admin') > 0) { $filename = rtrim(substr($filename, 0, strpos($root, 'wp-admin')), DS) . DS . 'wp-includes' . DS . 'version.php'; } elseif (strpos($root, 'wp-content') > 0) { $filename = rtrim(substr($filename, 0, strpos($root, 'wp-content')), DS) . DS . 'wp-includes' . DS . 'version.php'; } elseif (strpos($root, 'wp-includes') > 0) { $filename = rtrim(substr($filename, 0, strpos($root, 'wp-includes')), DS) . DS . 'wp-includes' . DS . 'version.php'; } else { $filename = rtrim($root, DS) . DS . 'wp-includes' . DS . 'version.php'; if (!file_exists($filename)) { $filename = rtrim(dirname($root), DS) . DS . 'wp-includes' . DS . 'version.php'; } } if (file_exists($filename)) { // --------------------------- // --- WordPress --- // --------------------------- $CMS = 'Wordpress'; $configuration = file_get_contents($filename); $pattern = '/.*\\$wp_version *= *\'(.*)\'/'; preg_match($pattern, $configuration, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; // Get the website root folder $root = dirname(dirname($filename)); return [true, $CMS, $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } /** * Detect if the CMS is x3cms. */ private static function isx3cms(string $root): array { $filename = rtrim($root, DS) . DS . 'INSTALL' . DS . 'index.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/.*\'X4VERSION\', *\'(\\d+\\.\\d+\\.\\d+)\'/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'x3cms', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return [false, false, false, false, false, false]; } } } /** * PHP, JS and HTML code related to the progress bar functionnality. */ class aeSecureProgressBar { protected $aeSession = null; private $_filename = null; private $_ID = null; private $_CSS = 'progress-bar'; private int $_frequency = 1000; // refresh the progress bar each xx seconds (1000=one second) private int $_start = 0; // f.i. 0 (current step in the progress bar) private int $_end = 0; // f.i. 100 (number of steps) private int $_pct = 0; // calculated progression in percentage protected static $instance = null; public function __construct($ID, $Class) { $this->_ID = $ID; $this->_CSS = $Class; $this->_filename = str_replace('.php', '.tmp', __FILE__); $this->_start = 0; $this->_end = 100; $this->_pct = 0; // Refresh frequency $this->_frequency = PROGRESSBARFREQUENCY * 1000; $this->aeSession = aeSecureSession::getInstance(); return true; } /** * Getter & Setter for the Start position of the progress bar (by default, 0). */ public function getStart(): int { return $this->_start; } public function setStart(int $value = 0): void { $this->_start = $value; } /** * Getter & Setter for the End position of the progress bar (by default, 100). */ public function getEnd(): int { return 0 == $this->_end ? 1 : $this->_end; } public function setEnd(int $value = 100): void { $this->_end = $value; } /** * Remove the temporary file with the progress indicator. */ public function clean() { if (file_exists($this->_filename)) { unlink($this->_filename); } } /** * Increment the position of the progress bar percentage (write the percentage * in the temporary file that will be used by the ajax Progress bar). * * @return bool */ public function incTaskCount() { if (true === $this->aeSession->get('Debug', DEBUG)) { return false; } ++$this->_start; if (($this->_start / $this->getEnd()) > $this->_pct) { $this->_pct = intval(intval($this->_start) / $this->getEnd()); // ******************************************************************* // ******************************************************************* // ******************************************************************* // // The session should be closed otherwise Ajax request won't be called // asynchronously and the progress bar won't be incremented // http://stackoverflow.com/questions/3506574 session_write_close(); // // ******************************************************************* if ($handle = fopen($this->_filename, 'w+')) { fwrite($handle, (int)($this->_pct * 100)); fclose($handle); } } return true; } /** * Return the current progress value; read it from a file. */ public function getProgress() { if (true === $this->aeSession->get('Debug', DEBUG)) { return false; } header('Content-Type: application/json'); header('Cache-Control: no-cache'); if (file_exists($this->_filename)) { echo json_encode(['pct' => file_get_contents($this->_filename)], JSON_THROW_ON_ERROR); } else { echo json_encode(['pct' => '100']); } try { ob_end_flush(); flush(); } catch (Exception $e) { } die(); } /** * Generate the HTML code for the progress bar. * * @return type */ public function getHTML() { if (true === $this->aeSession->get('Debug', DEBUG)) { return false; } echo ''; } /** * Generate the JS code for the progress bar (initialization and show evolution * during the scanning). * * @param mixed $what */ public function getJSFunction($what = 'function') { if (true === $this->aeSession->get('Debug', DEBUG)) { return false; } switch ($what) { case 'initialize': // Variables needed for the progress bar JS code echo 'progressFct=null;previousPct=0;'; break; case 'ajax_before': // The long process will start; initialize the progress bar and show it echo 'previousPct=0;' . '$(".' . $this->_CSS . '").attr("aria-valuenow", 0);' . '$(".' . $this->_CSS . '").css("width","0%");' . '$(".' . $this->_CSS . '").html($(".' . $this->_CSS . '").attr("aria-valuenow") + "%");' . '$("#' . $this->_ID . '").fadeIn(300); ' . 'progressFct=setInterval( function () {getProgress();},' . $this->_frequency . ');'; break; case 'ajax_success': // The long process is now finished, put the progress bar to 100% and then hide it echo '$(".' . $this->_CSS . '").attr("aria-valuenow", 100);' . '$(".' . $this->_CSS . '").css("width","100%");' . '$(".' . $this->_CSS . '").html($(".' . $this->_CSS . '").attr("aria-valuenow") + "%");' . 'clearTimeout(progressFct);' . '$("#' . $this->_ID . '").fadeOut(300);'; break; case 'function': // The long process is running, update the progress bar echo 'function getProgress() { $.ajax({ url:"' . FILE . '", data:"task=progress", type:"' . ((true === $this->aeSession->get('Debug', DEBUG)) ? 'GET' : 'POST') . '", async:true, timeout: 600000, // Scanning a site can be very long cache:false, dataType:"json", success: function(json) { percentage=parseInt(json.pct); if (percentage>=100) { $(".' . $this->_CSS . '").attr("aria-valuenow", 100); $(".' . $this->_CSS . '").css("width","100%"); $(".' . $this->_CSS . '").html($(".' . $this->_CSS . '").attr("aria-valuenow") + "%"); clearTimeout(progressFct); $("#' . $this->_ID . '").fadeOut(300); } else { if (percentage>previousPct) { if ($("#gettingFiles").length) $("#gettingFiles").hide(); if (percentage>100) percentage=100; $(".' . $this->_CSS . '").attr("aria-valuenow", Math.round(percentage)); $(".' . $this->_CSS . '").css("width", percentage + "%"); $(".' . $this->_CSS . '").html($(".' . $this->_CSS . '").attr("aria-valuenow") + "%"); $("#' . $this->_ID . '").show(); previousPct=percentage; } } } // success }); return; } // function getProgress()'; break; } } /** * @param type $ID ID to give to the HTML progress bar container * @param type $Class CSS class to give to the HTML container * * @return type */ public static function getInstance($ID = 'ajaxResultPct', $Class = 'progress-bar') { if (null === self::$instance) { self::$instance = new aeSecureProgressBar($ID, $Class); } return self::$instance; } } /** * The scanner himself. */ class aeSecureScan { // hash of files already scanned and are viruses (the file is a virus) public const BLACKLIST = 'aesecure_quickscan_blacklist.json'; // JSON for the detected CMS (f. i. aesecure_quickscan_J!3.9.0.json for a Joomla 3.9.0 version) public const CMS = 'aesecure_quickscan_%s.json'; // hash of files already scanned and where a virus was found (the file contains a virus) public const EDITED = 'aesecure_quickscan_edited.json'; // hash of files that can be considered as safe public const OTHER = 'aesecure_quickscan_other.json'; // JSON with patterns to scan for finding viruses public const PATTERN = 'aesecure_quickscan_pattern.json'; // hash of files that can be considered as safe public const WHITELIST = 'aesecure_quickscan_whitelist.json'; // List of supported CMS public const SUPPORTED_CMS = 'aesecure_quickscan_supported_cms.json'; public const FOLDERS = 'aesecure_quickscan_folders.json'; protected $aeFiles = null; protected $aeLanguage = null; protected $aeLog = null; protected $aeSession = null; protected $aeProgress = null; private $_directory = null; // Folder to scan private $_start = 0; // When processing files by block (files #1 till #2500, #2501 till #5000, ...) start is the "from" part f.i. 2501 private int $_end = 0; // and _end will be the end part f.i. 5000 private $_arrCMS = null; // Used by the hash functions private $_arrRegex = null; // Array with regex patterns to match private $_arrCMSHashes = null; // Array with the hash of the installed CMS private $_arrWhiteListHashes = null; // Array with whitelisted files (files from the CMS core) private $_arrOtherHashes = null; // Array with whitelisted files (files whitelisted during aeSecure DeepScan runs) private $_arrBlackListHashes = null; // Array with blacklisted files (the file is a virus) private $_arrEditedHashes = null; // Array with hash of edited files (a virus was appended in a file) private $_arrExtHashes = null; // Array with the hash of the installed CMS extensions public function __construct() { date_default_timezone_set('Europe/Brussels'); setlocale(LC_TIME, 'fr_FR.utf8', 'fra'); // Instanciate objects $this->aeLanguage = aeSecureLanguage::getInstance(); $this->aeFiles = aeSecureFiles::getInstance(); $this->aeProgress = aeSecureProgressBar::getInstance(); $this->aeSession = aeSecureSession::getInstance(); // Create the logfile if (true === $this->aeSession->get('Debug', DEBUG)) { $this->aeLog = aeSecureLog::getInstance(str_replace('.php', '.files.tmp', __FILE__)); } $rootFolder = DIR; $this->aeSession->set('folder', ''); // By default, scan the current directory. // In Expert mode, allow to use a session to store the name of the folder // Get the folder to process if (true === $this->aeSession->get('Expert', EXPERT)) { $folder = base64_decode((string) aeSecureFct::getParam('folder', 'string', '')); if ('' == $folder) { $folder = $this->aeSession->get('folder', $rootFolder); } // Check if the user has specified a folder in the user entry form if (is_dir($folder)) { $this->aeSession->set('folder', $folder); } } else { $tmp = $this->aeSession->get('folder', ''); if ('' == $tmp) { $this->aeSession->set('folder', $rootFolder); } } // When running the scan for f.i. only 1000 files and not all files present on the server, // the start parameter will f.i. be set to 0 while the end parameter will be set to 1000. // This is just like a pagination so processing the 1000 next files will be : // start=1000 and end=1000. $this->_start = aeSecureFct::getParam('start', 'integer', 0); $this->_end = $this->_start + aeSecureFct::getParam('end', 'integer', 0); $this->_directory = $rootFolder; if ($this->aeSession->get('Expert', EXPERT)) { $this->_directory = trim((string) $this->aeSession->get('folder', $rootFolder)); if ('' == $this->_directory) { $this->_directory = $rootFolder; } } $this->_arrCMS = [['joomla' => 'J!'], ['wordpress' => 'WP']]; if (!is_file($file = DIR . DS . self::PATTERN)) { aeSecureDownload::get($file, 'settings/'); } return true; } public function directory() { return $this->_directory; } /** * The aesecure_quickscan_pattern.json is using constant for the disclaimer info. * These constants should be replaced by their translated text. * * @param type $disclaimer * * @return string */ public function getDisclaimerText($disclaimer) { $return = match ($disclaimer) { 'HIGHPROBALITY' => $this->aeLanguage->get('HIGHPROBALITY'), 'HIGHPROBALITYFALSEGIF' => $this->aeLanguage->get('HIGHPROBALITYFALSEGIF'), 'WARNINGBASE64ENCODEDPATTERN' => $this->aeLanguage->get('WARNINGBASE64ENCODEDPATTERN'), 'HIGHPROBALITYBADSITE' => $this->aeLanguage->get('HIGHPROBALITYBADSITE'), 'HIGHPROBALITYBASE64KEYWORD' => $this->aeLanguage->get('HIGHPROBALITYBASE64KEYWORD'), 'WARNINGNOTMANDATORYAVIRUS' => $this->aeLanguage->get('WARNINGNOTMANDATORYAVIRUS'), default => $disclaimer . ((true === $this->aeSession->get('Debug', DEBUG)) ? ' *please add translation*' : ''), }; return $return; } /** * Add a file in the whitelist. */ public function WhiteList(?string $filename = null) { // Don't white list files in demo mode, simulate that everything was ok (return -1) if (DEMO) { return -1; } if (!is_file($file = DIR . DS . self::WHITELIST)) { aeSecureDownload::get($file, 'settings/'); } if (!is_file($file)) { die(sprintf('Sorry, the file [%s] is missing', basename($file))); } if (file_exists($filename)) { $json = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); $sha = md5_file($filename); if (!(isset($json[$sha]))) { $json[$sha] = 1; } asort($json); // Output the file with all hashes $fp = fopen($file, 'w'); fwrite($fp, json_encode($json, JSON_THROW_ON_ERROR)); fclose($fp); unset($fp); } return -1; } /** * Process the action (like running the scan or displaying the progress bar). */ public function Process() { if (!is_file($file = DIR . DS . self::PATTERN)) { aeSecureDownload::get($file, 'settings/'); } if (!is_file($file)) { die(sprintf('Sorry, the file [%s] is missing', basename($file))); } $this->_arrRegex = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); // Process the task if any, from POST or GET depending on the debug mode state // Get the folder to process $this->_directory = DIR; if ($this->aeSession->get('Expert', EXPERT)) { $this->_directory = trim((string) $this->aeSession->get('folder', DIR)); if ('' == $this->_directory) { $this->_directory = DIR; } } $task = aeSecureFct::getParam('task', 'string', ''); if ('' !== $task) { switch ($task) { case 'checkwhitelist': { // Detect the existence of the whitelist.json file $filename = DIR . DS . self::WHITELIST; echo file_exists($filename) ? 1 : 0; die(); break; } case 'byebye': { // Don't kill files in demo mode, simulate that everything was ok (return -1) if (DEMO) { die(-1); } // keepwhitelist is a variable posted by the Ajax request and will // inform if the script should or not remove the user's whitelist file. $bKeepWhiteList = aeSecureFct::getParam('keepwhitelist', 'boolean', true); // get CMS for later use $file = DIR . DS . self::SUPPORTED_CMS; $arrCMS = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); $arr = aeSecureCMS::getInfo($this->_directory); $CMS = $arr[0]; $prefix = $arrCMS[strtolower($CMS)]['prefix']; if (true !== $this->aeSession->get('Debug', DEBUG)) { // Get the list of aeSecure QuickScan JSON // Need to use "DIR . DS" so filenames will be absolute which is needed $arrDeleteFiles = glob(DIR . DS . 'aesecure_quickscan_*.json'); // And don't scan this script also $arrDeleteFiles[] = DIR . DS . FILE; $arrDeleteFiles[] = DIR . DS . 'aesecure.png'; $arrDeleteFiles[] = DIR . DS . 'banner.svg'; $arrDeleteFiles[] = DIR . DS . 'LICENCE'; $arrDeleteFiles[] = DIR . DS . 'make_hashes.php'; $arrDeleteFiles[] = DIR . DS . 'octocat.tmpl'; $arrDeleteFiles[] = DIR . DS . 'readme.md'; $arrDeleteFiles[] = DIR . DS . 'images' . DS . 'expert.png'; $arrDeleteFiles[] = DIR . DS . 'images' . DS . 'files.png'; $arrDeleteFiles[] = DIR . DS . 'images' . DS . 'files_extended.png'; $arrDeleteFiles[] = DIR . DS . 'images' . DS . 'interface.png'; $arrDeleteFiles[] = DIR . DS . 'images' . DS . 'nothing_to_scan.png'; $arrDeleteFiles[] = DIR . DS . 'images' . DS . 'virus_of_mine.png'; // Kill the debug log file if present if (null !== $this->aeLog) { $arrDeleteFiles[] = $this->aeLog->filename(); } foreach ($arrDeleteFiles as $filename) { $bDelete = true; if ($filename == DIR . DS . self::WHITELIST) { // If $bKeepWhiteList=true, we can't delete the file $bDelete = !$bKeepWhiteList; } if ($bDelete) { if (is_file($filename) && is_readable($filename)) { unlink($filename); } } } // remove quickscan directories $arrDeleteFolders = ['settings', 'utils']; if (!$bKeepWhiteList) { // delete all hashes directories $arrDeleteFolders[] = 'hashes'; } else { // delete only CMS hashes directories $arrDeleteFolders[] = 'hashes'.DS.'joomla'; $arrDeleteFolders[] = 'hashes'.DS.'wordpress'; } foreach ($arrDeleteFolders as $folder) { $this->aeFiles->rrmdir(DIR . DS . $folder, true, []); } if ($bKeepWhiteList) { // just keep customer extensions hashes // get std extensions list $url = 'hashes/'. $prefix . 'extensions'; $dir = self::remotedir($url); foreach ($dir as $filename) { $file = DIR . DS . 'hashes'.DS.$prefix.'extensions'.DS.$filename; if (is_file($file) && is_readable($file)) { // std extension hash : remove it unlink($file); } } } } die('
' . $src . ''); break; } case 'seefile': { if (DEMO) { // Don't return source code in DEMO mode echo '
' . $src . '' . print_r($ex, true) . ''; } } } unset($arrSkipFiles); // Put the array in a session variable and remember the processed folder $this->aeSession->set('Folder', $this->_directory); $this->aeSession->set('arrFiles', json_encode($arrFiles, JSON_THROW_ON_ERROR)); } else { // The user is running the script once more for the same folder // ==> don't scan the disk again, just user the session variable to speed up the process $arrFiles = json_decode((string) $arrFiles, null, 512, JSON_THROW_ON_ERROR); } if (null == $arrFiles) { $arrFiles = json_decode((string) $this->aeSession->get('arrFiles', null), null, 512, JSON_THROW_ON_ERROR); } unset($this->arrOtherHashes, $this->arrWhiteListHashes, $this->arrBlackListHashes, $this->arrCMSHashes); if (true == $echo) { try { header('Content-Type: application/json'); header('Cache-Control: no-cache'); } catch (\Exception $exception) { } sort($arrFilesBlacklisted); echo sprintf( '{"count":%d,"whitelisted":%d,"blacklisted":%d,"blacklisting":%s,"edited":%d,"editlisting":%s,"skipped":%d}', count($arrFiles), $wNbrWhitelisted ?? 0, $wNbrBlacklisted ?? 0, json_encode($arrFilesBlacklisted), $wNbrEdited ?? 0, json_encode($arrFilesEditlisted), $wNbrSkipped ?? 0 ); // Prevent a warning; flush only if there is something to flush try { while (ob_get_level() > 0) { ob_end_flush(); } flush(); } catch (\Exception $e) { } die(); } else { return true; } } /** * Start the scan. * * @global type $sLogFile * @global type $sProgressFile */ private function doScan(): string|bool { $aeLanguage = aeSecureLanguage::getInstance(); try { if (!get_cfg_var('safe_mode')) { // set_time_limit isn't used when safe_mode is active // No max execution time @ini_set('max_execution_time', '0'); // Remove time limit; avoid 504 HTTP errors @ini_set('set_time_limit', '0'); } } catch (Exception $e) { } // Allocate the maximum allowed memory to the script (-1 = no limit) @ini_set('memory_limit', (true !== $this->aeSession->get('Debug', DEBUG)) ? -1 : MEMORY_LIMIT); $wFile = 0; $wCount = 0; $wFound = 0; $wProcessedFile = 0; $wSkipSize = 0; $wSkipChmod = 0; $wSkipHashes = 0; $wUnreadable = 0; if (!is_dir($this->_directory)) { echo '
' . trim(str_replace('<', '<', file_get_contents($filename))) . '' .
'';
$output_line = str_replace('$FOUND$', $FOUND, $OutputTemplate);
if (FULLDEBUG && !aeSecureFct::isAjaxRequest()) {
echo sprintf(
'%s IS BLACKLISTED, VIRUS FOUND' . trim(str_replace('<', '<', file_get_contents($filename))) . '' .
'';
$output_line = str_replace('$FOUND$', $FOUND, $OutputTemplate);
if (FULLDEBUG && !aeSecureFct::isAjaxRequest()) {
echo sprintf(
'%s CONTAINS A VIRUS' . str_replace(
$keyword,
'' . $keyword . '',
trim(str_replace('<', '<', $sContext))
) . '' .
'';
}
}
}
}
unset($arrMatch);
}
if ($bInfected) {
$output_line = str_replace('$FOUND$', $FOUND, $OutputTemplate);
}
unset($content);
}
}
if ($bInfected) {
++$wFound;
}
unset($content);
} else {
++$wSkipSize;
if (true === $this->aeSession->get('Debug', DEBUG)) {
$this->aeLog->addLog('Scanning #' . ($wFile + 1) . '. ' .
$filename . ' SKIP; Too big.');
}
// The filename wasn't whitelisted, show it.
$bFound = true;
$output_line = '