shell bypass 403
<?php
/**
* @package admintools
* @copyright Copyright (c)2010-2024 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Component\AdminTools\Administrator\Model;
defined('_JEXEC') or die;
use Akeeba\Component\AdminTools\Administrator\Helper\ServerTechnology;
use Akeeba\Component\AdminTools\Administrator\Helper\Storage;
use Akeeba\Component\AdminTools\Administrator\Scanner\Complexify;
use Exception;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\UserHelper;
use Joomla\Database\ParameterType;
use Akeeba\Plugin\System\AdminTools\Extension\AdminTools;
use Joomla\Utilities\IpHelper as Ip;
use RuntimeException;
use Throwable;
#[\AllowDynamicProperties]
class ControlpanelModel extends BaseDatabaseModel
{
/**
* The extension ID of the System - Admin Tools plugin
*
* @var int
*/
static $pluginId = null;
/**
* Get the extension ID of the System - Admin Tools plugin
*
* @return int
*/
public function getPluginID(): ?int
{
if (is_null(static::$pluginId))
{
// type name params id
$plugin = PluginHelper::getPlugin('system', 'admintools');
static::$pluginId = empty($plugin) ? null : $plugin->id;
}
// Joomla! 5 does not return the plugin IDs for disabled plugins. MAXIMUM EFFORT!
if (is_null(static::$pluginId))
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('extension_id'))
->from($db->quoteName('#__extensions'))
->where(
[
$db->quoteName('type') . ' = ' . $db->quote('plugin'),
$db->quoteName('folder') . ' = ' . $db->quote('system'),
$db->quoteName('element') . ' = ' . $db->quote('admintools'),
]
);
try
{
static::$pluginId = $db->setQuery($query)->loadResult() ?: null;
}
catch (Throwable $e)
{
static::$pluginId = null;
}
}
return static::$pluginId;
}
/**
* Makes sure our system plugin is really the very first system plugin to execute
*
* @return void
*/
public function reorderPlugin()
{
// Get our plugin's ID
$id = $this->getPluginID();
// The plugin is not enabled, there's no point in continuing
if (!PluginHelper::isEnabled('system', 'admintools') || empty($id))
{
return;
}
// Get a list of ordering values per ID
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select([
$db->qn('extension_id'),
$db->qn('ordering'),
])
->from($db->qn('#__extensions'))
->where($db->qn('type') . ' = ' . $db->q('plugin'))
->where($db->qn('folder') . ' = ' . $db->q('system'))
->order($db->qn('ordering') . ' ASC');
$orderingPerId = $db->setQuery($query)->loadAssocList('extension_id', 'ordering');
$orderings = array_unique(array_values($orderingPerId));
$minOrdering = reset($orderings);
$myOrdering = $orderingPerId[$id];
reset($orderings);
$sharedOrderings = 0;
foreach ($orderingPerId as $order)
{
if ($order > $myOrdering)
{
break;
}
if ($order == $myOrdering)
{
$sharedOrderings++;
}
}
// Do I need to reorder the plugin?
if (($myOrdering > $minOrdering) || ($sharedOrderings > 1))
{
$query = $db->getQuery(true)
->update($db->qn('#__extensions'))
->set($db->qn('ordering') . ' = ' . $db->q($minOrdering - 1))
->where($db->qn('extension_id') . ' = ' . $db->q($id));
$db->setQuery($query);
$db->execute();
// Reset the Joomla! plugins cache
Factory::getApplication()->bootComponent('com_admintools')->getCacheCleanerService()->clearGroups(['com_plugins']);
}
}
/**
* Does the user need to enter a Download ID in the component's Options page?
*
* @return bool
*/
public function needsDownloadID(): bool
{
// Do I need a Download ID?
if (!ADMINTOOLS_PRO)
{
return false;
}
/** @var UpdatesModel $updateModel */
$updateModel = $this->getMVCFactory()->createModel('Updates', 'Administrator', ['ignore_request' => true]);
$dlid = $updateModel->sanitizeLicenseKey($updateModel->getLicenseKey());
return !$updateModel->isValidLicenseKey($dlid);
}
/**
* Checks all the available places if we just blocked our own IP?
*
* @param string $externalIp Additional IP address to check
*
* @return bool
*/
public function isMyIPBlocked($externalIp = null): bool
{
if ((defined('ADMINTOOLS_PRO') ? ADMINTOOLS_PRO : 0) != 1)
{
return false;
}
// First let's get the current IP of the user
$ipList = [$this->getVisitorIP(), $externalIp];
$ipList = array_filter($ipList, function ($x) {
return !empty($x);
});
// Check if the IP appears verbatim in the automatic IP block, history of automatic IP blocks or IP deny list
$db = $this->getDatabase();
$tables = [
'#__admintools_ipautoban',
'#__admintools_ipautobanhistory',
'#__admintools_ipblock',
];
foreach ($tables as $table)
{
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName($table))
->whereIn($db->quoteName('ip'), $ipList, ParameterType::STRING);
if (($db->setQuery($query)->loadResult() ?: 0) > 0)
{
return true;
}
}
return false;
}
/**
* Update the cached live site's URL for the front-end scheduling feature
*
* @return void
*/
public function updateMagicParameters()
{
$cParams = ComponentHelper::getParams($this->option);
$cParams->set('siteurl', Uri::root());
Factory::getApplication()->bootComponent('com_admintools')->getComponentParametersService()->save($cParams);
}
/**
* Performs some checks about Joomla configuration (log and tmp path correctly set)
*
* @return string|bool Warning message. Boolean FALSE if no warning is found.
*/
public function checkJoomlaConfiguration(bool $forceDisplayForDebug = false)
{
if ($forceDisplayForDebug)
{
return "This is a dummy string used for testing.";
}
// Get the absolute path to the site's root
$absoluteRoot = @realpath(JPATH_ROOT);
$siteRoot = empty($absoluteRoot) ? JPATH_ROOT : $absoluteRoot;
// First of all, do we have a VALID log folder?
$logDir = Factory::getApplication()->get('log_path');
if (!$logDir || !@is_writable($logDir))
{
return Text::_('COM_ADMINTOOLS_CONTROLPANEL_ERR_JCONFIG_INVALID_LOGDIR');
}
if ($siteRoot == $logDir)
{
return Text::_('COM_ADMINTOOLS_CONTROLPANEL_ERR_JCONFIG_LOGDIR_SITEROOT');
}
// Do we have a VALID tmp folder?
$tempDir = Factory::getApplication()->get('tmp_path');
if (!$tempDir || !@is_writable($tempDir))
{
return Text::_('COM_ADMINTOOLS_CONTROLPANEL_ERR_JCONFIG_INVALID_TMPDIR');
}
if ($siteRoot == $tempDir)
{
return Text::_('COM_ADMINTOOLS_CONTROLPANEL_ERR_JCONFIG_TMPDIR_SITEROOT');
}
return false;
}
/**
* Do I need to show the Quick Setup Wizard?
*
* @return bool
*/
public function needsQuickSetupWizard(): bool
{
$params = Storage::getInstance();
return $params->getValue('quickstart', 0) == 0;
}
/**
* Get the most likely visitor IP address, reported by the server
*
* @return string
*/
public function getVisitorIP(): string
{
$internalIP = Ip::getIp();
if ((strpos($internalIP, '::') === 0) && (strstr($internalIP, '.') !== false))
{
$internalIP = substr($internalIP, 2);
}
return $internalIP;
}
/**
* Is the System - Admin Tools plugin installed?
*
* @return bool
*
* @since 4.3.0
*/
public function isPluginInstalled(): bool
{
return !empty($this->getPluginID());
}
/**
* Is the System - Admin Tools plugin currently loaded?
*
* @return bool
*
* @since 4.3.0
*/
public function isPluginLoaded(): bool
{
return class_exists(AdminTools::class, false);
}
/**
* Is the Admintools.php file renamed?
*
* @return bool
*
* @since 4.3.0
*/
public function isMainPhpDisabled(): bool
{
$file = JPATH_PLUGINS . '/system/admintools/services/provider.php';
$folder = dirname($file);
$hasFolder = @file_exists($folder) && @is_dir($folder);
$hasFile = @file_exists($file) && @is_file($file);
if ($hasFolder && !$hasFile)
{
return true;
}
return false;
}
/**
* Rename the disabled Admintools.php file back to its proper, main.php, name.
*
* @return bool
*
* @since 4.3.0
*/
public function reenableMainPhp(): bool
{
$altName = $this->getRenamedMainPhp();
if (!$altName)
{
return false;
}
$to = JPATH_PLUGINS . '/system/admintools/services/provider.php';
$folder = dirname($to);
$from = $folder . '/' . $altName;
if (!@rename($from, $to))
{
$res = File::copy($from, $to) && File::delete($from);
}
return $res ?? true;
}
/**
* Get the file name under which Admintools.php has been renamed to
*
* @return string|null
*
* @since 4.3.0
*/
public function getRenamedMainPhp(): ?string
{
$possibleNames = [
'provider-disable.php',
'provider.php.bak',
'provider.bak.php',
'provider.bak',
'-provider.php',
];
$folder = JPATH_PLUGINS . '/system/admintools/services';
foreach ($possibleNames as $baseName)
{
if (@file_exists($folder . '/' . $baseName))
{
return $baseName;
}
}
return null;
}
/**
* Delete old log files (with a .log extension) always. If the logging feature is disabled (either the text debug
* log or logging in general) also delete the .php log files.
*
* @since 5.1.0
*/
public function deleteOldLogs()
{
$logpath = Factory::getApplication()->get('log_path');
$files = [
$logpath . DIRECTORY_SEPARATOR . 'admintools_blocked.log',
$logpath . DIRECTORY_SEPARATOR . 'admintools_blocked.log.1',
];
$wafParams = Storage::getInstance();
$textLogs = $wafParams->getValue('logfile', 0);
$allLogs = $wafParams->getValue('logbreaches', 1);
if (!$textLogs || !$allLogs)
{
$files = array_merge($files, [
$logpath . DIRECTORY_SEPARATOR . 'admintools_blocked.php',
$logpath . DIRECTORY_SEPARATOR . 'admintools_blocked.1.php',
]);
}
foreach ($files as $file)
{
File::delete($file);
}
}
/**
* Checks if the current contents of the server configuration file (ie .htaccess) match with the saved one.
*/
public function serverConfigEdited(): bool
{
// Core version? No need to continue
if (!defined('ADMINTOOLS_PRO') || !ADMINTOOLS_PRO)
{
return false;
}
// User decided to ignore any warning about manual edits
$cParams = ComponentHelper::getParams($this->option);
if (!$cParams->get('serverconfigwarn', 1))
{
return false;
}
$modelTech = '';
if (ServerTechnology::isNginxSupported() == 1)
{
$modelTech = 'Nginxconfmaker';
$checkLine = "Security Enhanced & Highly Optimized NginX Configuration File for Joomla!";
}
elseif (ServerTechnology::isWebConfigSupported() == 1)
{
$modelTech = 'Webconfigmaker';
$checkLine = "Security Enhanced & Highly Optimized .web.config File for Joomla!";
}
elseif (ServerTechnology::isHtaccessSupported() == 1)
{
$modelTech = 'Htaccessmaker';
$checkLine = "Security Enhanced & Highly Optimized .htaccess File for Joomla!";
}
else
{
// Can't understand the Server Technology we're on, let's stop here
return false;
}
try
{
/** @var ServerconfigmakerModel $serverModel */
$serverModel = $this->getMVCFactory()->createModel($modelTech, 'Administrator');
}
catch (Exception $e)
{
return false;
}
$serverFile = $serverModel->getConfigFileName(true);
if (!file_exists($serverFile))
{
return false;
}
$actualContents = file_get_contents($serverFile);
if ($actualContents === false)
{
return false;
}
// Check if the file was generated by Admin Tools
if (strpos($actualContents, $checkLine) === false)
{
return false;
}
if (strpos($actualContents, 'automatically generated by Admin Tools') === false)
{
return false;
}
$currentContents = $serverModel->makeConfigFile();
// Is the hash of current file different from the saved one? If so, warn the user
return ($serverModel->getConfigHash($actualContents) != $serverModel->getConfigHash($currentContents));
}
/**
* Check the strength of the Secret Word for front-end and remote scans. If it is insecure return the reason it
* is insecure as a string. If the Secret Word is secure return an empty string.
*
* @return string
*/
public function getFrontendSecretWordError(): string
{
$params = ComponentHelper::getParams($this->option);
// Is frontend backup enabled?
$febEnabled = $params->get('frontend_enable', 0) != 0;
if (!$febEnabled)
{
return '';
}
$secretWord = $params->get('frontend_secret_word', '');
try
{
Complexify::isStrongEnough($secretWord);
}
catch (RuntimeException $e)
{
// Ah, the current Secret Word is bad. Create a new one if necessary.
$newSecret = Factory::getApplication()->getSession()->get('admintools.cpanel.newSecretWord', null);
if (empty($newSecret))
{
$newSecret = UserHelper::genRandomPassword(32);
Factory::getApplication()->getSession()->set('admintools.cpanel.newSecretWord', $newSecret);
}
return $e->getMessage();
}
return '';
}
}