shell bypass 403
<?php
/**
* @package admintools
* @copyright Copyright (c)2010-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Plugin\System\AdminTools\Feature;
defined('_JEXEC') || die;
use Akeeba\Plugin\System\AdminTools\Feature\Mixin\SuperUsersTrait;
use Akeeba\Plugin\System\AdminTools\Utility\RescueUrl;
use Exception;
use Joomla\Filesystem\Folder;
use Joomla\CMS\User\User;
class CriticalFilesMonitoring extends Base
{
use SuperUsersTrait;
/**
* Is this feature enabled?
*
* @return bool
*/
public function isEnabled()
{
return ($this->wafParams->getValue('criticalfiles', 0) == 1);
}
public function onAfterRender(): void
{
$mustSaveData = false;
$criticalFiles = $this->getCriticalFiles();
$loadedFiles = $this->load();
$alteredFiles = [];
$filesToSave = [];
if (is_string($criticalFiles))
{
$criticalFiles = array_map('trim', explode(',', $criticalFiles));
}
foreach ($criticalFiles ?: [] as $relPath)
{
$curInfo = $this->getFileInfo($relPath);
if ($curInfo == false)
{
// Did that file exist? If so, we need to save the critical files list.
if (is_array($loadedFiles) && array_key_exists($relPath, $loadedFiles))
{
$mustSaveData = true;
}
continue;
}
$filesToSave[$relPath] = $curInfo;
// Did the file change?
$oldInfo = null;
if (isset($loadedFiles[$relPath]))
{
$oldInfo = $loadedFiles[$relPath];
}
if ($oldInfo !== $curInfo)
{
$mustSaveData = true;
$alteredFiles[$relPath] = [$oldInfo, $curInfo];
}
}
if ($mustSaveData)
{
$this->save($filesToSave);
}
// Send out the email only if we have some altered files AND we have some previous data (otherwise we will scary
// the user as soon as they enable the feature)
if (!empty($alteredFiles) && $loadedFiles)
{
$this->sendEmail($alteredFiles);
}
}
/**
* Get the critical files, i.e. the files which get most commonly hacked in Joomla: configuration.php, the index.php
* files in front- and backend and the index.php, error.php and component.php files of the installed templates.
*
* @return array The list of critical files (relative paths)
*/
protected function getCriticalFiles()
{
$criticalFiles = [
'configuration.php',
'index.php',
'administrator/index.php',
];
$templateFiles = ['index.php', 'error.php', 'component.php'];
try
{
$templates = Folder::folders(JPATH_SITE . '/templates');
}
catch (Exception $e)
{
$templates = [];
}
if (is_array($templates) && !empty($templates))
{
foreach ($templates as $template)
{
foreach ($templateFiles as $templateFile)
{
$relPath = 'templates/' . $template . '/' . $templateFile;
if (file_exists(JPATH_SITE . '/' . $relPath))
{
$criticalFiles[] = $relPath;
}
}
}
}
try
{
$templates = Folder::folders(JPATH_ADMINISTRATOR . '/templates');
}
catch (Exception $e)
{
$templates = [];
}
if (is_array($templates) && !empty($templates))
{
foreach ($templates as $template)
{
foreach ($templateFiles as $templateFile)
{
$relPath = 'templates/' . $template . '/' . $templateFile;
if (file_exists(JPATH_ADMINISTRATOR . '/' . $relPath))
{
$criticalFiles[] = $relPath;
}
}
}
}
return $criticalFiles;
}
/**
* Returns information about a file
*
* @param string $relPath The path to the file relative to the site's root
*
* @return null|array Null if the file is not there, object with information otherwise
*/
protected function getFileInfo($relPath)
{
$absolutePath = JPATH_SITE . '/' . $relPath;
if (!file_exists($absolutePath) || !is_file($absolutePath) || !is_readable($absolutePath))
{
return null;
}
return [
'size' => @filesize($absolutePath),
'timestamp' => filemtime($absolutePath),
'md5' => @hash_file('md5', $absolutePath),
'sha1' => @hash_file('sha1', $absolutePath),
];
}
/**
* Load the critical file information from the database
*
* @return array
*/
protected function load()
{
$db = $this->db;
$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
->select($db->quoteName('value'))
->from($db->quoteName('#__admintools_storage'))
->where($db->quoteName('key') . ' = ' . $db->quote('criticalfiles'));
$db->setQuery($query);
$error = 0;
try
{
$jsonData = $db->loadResult();
}
catch (Exception $e)
{
$error = $e->getCode();
}
if (method_exists($db, 'getErrorNum') && $db->getErrorNum())
{
$error = $db->getErrorNum();
}
if ($error)
{
$jsonData = null;
}
if (empty($jsonData))
{
return [];
}
return json_decode($jsonData, true);
}
/**
* Save the critical file information to the database
*
* @param array $fileList The list of critical file information
*
* @return void
*/
protected function save(array $fileList)
{
$db = $this->db;
$data = json_encode($fileList);
$db->transactionStart();
$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
->delete($db->quoteName('#__admintools_storage'))
->where($db->quoteName('key') . ' = ' . $db->quote('criticalfiles'));
$db->setQuery($query);
$db->execute();
$object = (object) [
'key' => 'criticalfiles',
'value' => $data,
];
try
{
$db->insertObject('#__admintools_storage', $object);
}
catch (Exception $e)
{
// Ignore this
}
$db->transactionCommit();
}
/**
* Sends a warning email to the addresses set up to receive security exception emails
*
* @param array $alteredFiles The files which were modified
*
* @return void
*/
private function sendEmail($alteredFiles)
{
if (empty($alteredFiles))
{
// What are you doing here? There are no altered files.
return;
}
// Load the component's administrator translation files
$jlang = $this->app->getLanguage();
$jlang->load('com_admintools', JPATH_ADMINISTRATOR, 'en-GB', true);
$jlang->load('com_admintools', JPATH_ADMINISTRATOR, $jlang->getDefault(), true);
$jlang->load('com_admintools', JPATH_ADMINISTRATOR, null, true);
// Convert the list of modified files to plain text and HTML lists
$plainTextInfo = implode(
"\n", array_map(
function ($line) {
return "* $line";
}, array_keys($alteredFiles)
)
);
$htmlInfo = implode(
"\n", array_map(
function ($line) {
return "<li>$line</li>";
}, array_keys($alteredFiles)
)
);
$htmlInfo = empty($htmlInfo) ? '' : "<ul>\n$htmlInfo</ul>";
// Construct the replacement table
$substitutions = [
'INFO' => $plainTextInfo,
'INFO_HTML' => $htmlInfo,
];
try
{
/**
* The email recipients are taken from one of the following sources:
*
* - Email this address on monitored files change (`criticalfiles_email`).
* - Email this address after an automatic IP ban (`emailafteripautoban`).
* - Super Users which are not Blocked, and have Receive Email enabled.
*
* The first source to return non-zero items wins.
*/
$recipients = $this->userListFromConfiguredEmailList('criticalfiles_email')
?: $this->userListFromConfiguredEmailList('emailafteripautoban')
?: array_filter($this->getSuperUserObjects(), fn(User $user) => $user->sendEmail);
foreach ($recipients as $recipient)
{
if (empty($recipient) || !$recipient instanceof User)
{
continue;
}
$data = array_merge(RescueUrl::getRescueInformation($recipient->email), $substitutions);
$this->exceptionsHandler->sendEmail('com_admintools.criticalfiles', $recipient, $data);
}
}
catch (Exception $e)
{
// Joomla! 3.5 and later throw an exception when crap happens instead of suppressing it and returning false
}
}
}