shell bypass 403
<?php /** * @package admintools * @copyright Copyright (c)2010-2023 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ use Joomla\CMS\Access\Access; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Factory; defined('_JEXEC') || die; /** * Keep track of Super Users on the site and send an email when users are added. Optionally automatically block these * new Super Users. */ class AtsystemFeatureSuperuserslist extends AtsystemFeatureAbstract { protected $loadOrder = 998; /** * Cache of user group IDs with Super User privileges * * @var array * @since 5.6.1 */ protected $superUserGroups = []; /** * Is this feature enabled? * * @return bool */ public function isEnabled() { /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * A Short History Of How This Feature Ended Up Disabled By Default * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * Despite this feature working just fine, we found out that it's a constant source of support request for * reasons unrelated to its performance or reliability. It boils down to: * * - Badly written third party software, some of it running outside Joomla!, will create from scratch or afresh * Super User accounts silently. This is EXACTLY the problem this feature is supposed to catch and it does. * However users don't perceive that ugly and dangerous third party hack as a problem and instead believe that * it's Admin Tools fault for warning them when they have not been subjectively hacked (in fact, the machine * has no way to determine that what just happened is not malicious BECAUSE THAT'S EXACTLY WHAT AN EVIL HACKER * WOULD DO TO PWN YOUR SITE). * * - People forget that they have disabled Admin Tools when they are creating a Super User, therefore making it * impossible for AT to know if the new Super User is legit or an evil implant. AT warns them, as they should, * but they again think it's a bug - despite the feature doing EXACTLY what it is asked to do, i.e. warn for * any user account created outside the Users editor and / or outside its watch. * * - People use third party extensions either by themselves (obvious) or one which override the backend Users * page of Joomla! (absolutely not obvious). In this case the created Super User is indeed created outside the * backend Users page of Joomla! so Admin Tools correctly warns them. Once more, people perceive it as a bug * in Admin Tools. */ return ($this->cparams->getValue('superuserslist', 0) == 1); } /** * Checks if a backend Super User is saving another Super User account. We have to run this check onAfterRoute since * com_users will perform an immediate redirect upon saving, without hitting onAfterRender. For the same reason the * detected ID of the Super User being saved has to be saved in the session to persist the successive page loads. */ public function onAfterRoute() { if (!$this->isBackendSuperUser()) { return; } // Do I already have session data? $safeIDs = $this->container->platform->getSessionVar('superuserslist.safeids', [], 'com_admintools'); $isUserSaveOrApply = $this->container->platform->getSessionVar('superuserslist.createnew', null, 'com_admintools'); if (!is_null($isUserSaveOrApply)) { // Yeah. Let's not overwrite the session data. We shall do that onAfterRender. return; } $safeIDs = $this->getSafeIDs(); // Get the option and task parameters $app = Factory::getApplication(); $option = $app->input->getCmd('option', 'com_foobar'); $task = $app->input->getCmd('task'); $isUserSaveOrApply = false; // Are we using com_user to Save or Save & Close a user? if ($option == 'com_users') { if (in_array($task, ['user.apply', 'user.save'])) { $isUserSaveOrApply = true; } } $this->container->platform->setSessionVar('superuserslist.safeids', $safeIDs, 'com_admintools'); $this->container->platform->setSessionVar('superuserslist.createnew', $isUserSaveOrApply, 'com_admintools'); } public function onAfterRender() { // Only run if the current user is a Super User AND we haven't already set a flag $currentUser = $this->container->platform->getUser(); if ($currentUser->guest) { return; } if (!$currentUser->authorise('core.admin')) { return; } $flag = $this->container->platform->getSessionVar('allowedsuperuser', null, 'com_admintools'); if ($flag === true) { return; } // Get temporary session variables $safeIDs = $this->container->platform->getSessionVar('superuserslist.safeids', [], 'com_admintools'); $isUserSaveOrApply = $this->container->platform->getSessionVar('superuserslist.createnew', null, 'com_admintools'); $this->container->platform->unsetSessionVar('superuserslist.safeids', 'com_admintools'); $this->container->platform->unsetSessionVar('superuserslist.createnew', 'com_admintools'); // Normalize if (empty($safeIDs)) { $safeIDs = []; } if (empty($isUserSaveOrApply)) { $isUserSaveOrApply = false; } // If it's not a backend Super User we are going to ignore session variables (they are forged!) if (!$this->isBackendSuperUser()) { $safeIDs = []; $isUserSaveOrApply = false; } // Get the Super User IDs $savedSuperUserIDs = $this->load(); $superUserGroups = $this->getSuperUserGroups(); $currentSuperUserIDs = $this->getUsersInGroups($superUserGroups); // Oh, we never had a list of Super Users. Let's fix that. if (empty($savedSuperUserIDs)) { $this->save($currentSuperUserIDs); return; } // Do we have new Super Users? $newSuperUsers = array_diff($currentSuperUserIDs, $savedSuperUserIDs); // Do NOT remove this variable! It catches the case were Super Users are added BUT THEN REMOVED FROM $newSuperUsers WITH array_diff. WE MUST SAVE IN THIS CASE! $hasNewSuperUsers = !empty($newSuperUsers); $newSuperUsers = array_diff($newSuperUsers, $safeIDs); $removedSuperUsers = array_diff($savedSuperUserIDs, $currentSuperUserIDs); // Detect the case where we have to simply save the list of Super Users and quit (no new or removed SUs) $saveListAndQuit = empty($newSuperUsers) && empty($removedSuperUsers); /** * Special case: Super User logged in backend creates a new user account that is also a Super User. * * In this case we do not have any safeIDs because the JForm is being submitted with user ID 0. This is normal * since we are creating a new user record, therefore we do not have a user ID yet. We can distinguish this * case from the generic "third party backend extension creates a new user account" by checking the option and * task parameters. If the option is com_users (the Joomla! user management core component) and the task * indicates applying or saving a user we have the special case we need to avoid blocking. */ if ($this->isBackendSuperUser() && empty($safeIDs) && $isUserSaveOrApply) { $saveListAndQuit = true; } if ($saveListAndQuit) { // In case Super Users ARE added BUT are in the safe IDs list THEN we MUST save the new list! if ($hasNewSuperUsers) { $this->save($currentSuperUserIDs); } return; } // If we're here a new Super User was added through means unknown. Notify the admins and block the user. $this->sendEmail($newSuperUsers); $flag = true; foreach ($newSuperUsers as $id) { $user = $this->container->platform->getUser($id); $user->block = 1; $user->save(); if ($currentUser->id == $id) { $flag = false; } } $this->container->platform->setSessionVar('allowedsuperuser', $flag, 'com_admintools'); $currentSuperUserIDs = array_diff($currentSuperUserIDs, $newSuperUsers); $newSuperUsers = []; if (!empty($newSuperUsers) || !empty($removedSuperUsers)) { $this->save($currentSuperUserIDs); } // Is the current user one of the new, bad admins? If so, try to log the out if ($flag === false) { $app = Factory::getApplication(); // Try being nice about it if (!$app->logout()) { // If being nice about logging you out doesn't work I'm gonna terminate you, with extreme prejudice. $app->getSession()->set('user', null); $app->getSession()->destroy(); } } } /** * Returns a list of safe Super User IDs. These are the IDs of the Super Users being saved by another Super User in * the backend of the site through com_users. * * @return array */ public function getSafeIDs() { $app = Factory::getApplication(); if (!$this->isBackendSuperUser()) { return []; } // Get the option and task parameters $option = $app->input->getCmd('option', 'com_foobar'); $task = $app->input->getCmd('task'); // Not com_users? if ($option != 'com_users') { return []; } // Special case: unblock with one click. There's no jform here, the ID is passed in the 'cid' query string parameter if ($task == 'users.unblock') { $cid = $app->input->get('cid', [], 'array'); if (empty($cid)) { return []; } if (!is_array($cid)) { $cid = [$cid]; } return $cid; } // Note Save or Save & Close? if (!in_array($task, ['user.apply', 'user.save'])) { return []; } // Get the user IDs from the form $jForm = $app->input->get('jform', [], 'array'); if (!is_array($jForm) || empty($jForm)) { return []; } // No user ID or group information? if (!isset($jForm['groups']) || !isset($jForm['id'])) { return []; } // Is it a Super User? $superUserGroups = $this->getSuperUserGroups(); $groups = $jForm['groups']; $isSuperUser = false; if (empty($groups)) { return []; } foreach ($groups as $group) { if (in_array($group, $superUserGroups)) { $isSuperUser = true; break; } } if (!$isSuperUser) { return []; } // Get the user ID being saved and return it $id = $jForm['id']; if (empty($id)) { return []; } return [$id]; } /** * Returns all Joomla! user groups * * @return array * * @since 5.3.0 */ protected function getSuperUserGroups() { if (empty($this->superUserGroups)) { // Get all groups $db = $this->container->db; $query = $db->getQuery(true) ->select([$db->qn('id')]) ->from($db->qn('#__usergroups')); $this->superUserGroups = $db->setQuery($query)->loadColumn(0); // This should never happen (unless your site is very dead, in which case I feel terribly sorry for you...) if (empty($this->superUserGroups)) { $this->superUserGroups = []; } $this->superUserGroups = array_filter($this->superUserGroups, function ($group) { return Access::checkGroup($group, 'core.admin'); }); } return $this->superUserGroups; } /** * Save the list of users to the database * * @param array $userList The list of User IDs * * @return void */ private function save(array $userList) { $db = $this->container->db; $data = json_encode($userList); $query = $db->getQuery(true) ->delete($db->quoteName('#__admintools_storage')) ->where($db->quoteName('key') . ' = ' . $db->quote('superuserslist')); $db->setQuery($query); $db->execute(); $object = (object) [ 'key' => 'superuserslist', 'value' => $data, ]; $db->insertObject('#__admintools_storage', $object); } /** * Load the saved list of Super User IDs from the database * * @return array */ private function load() { $db = $this->container->db; $query = $db->getQuery(true) ->select($db->quoteName('value')) ->from($db->quoteName('#__admintools_storage')) ->where($db->quoteName('key') . ' = ' . $db->quote('superuserslist')); $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); } /** * Sends a warning email to the addresses set up to receive security exception emails * * @param array $superUsers The IDs of Super Users added * * @return void */ private function sendEmail(array $superUsers) { if (empty($superUsers)) { // What are you doing here? return; } // Load the component's administrator translation files $jlang = Factory::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 added Super Users $htmlUsersList = <<< HTML <ul> HTML; foreach ($superUsers as $id) { $user = $this->container->platform->getUser($id); $htmlUsersList .= <<< HTML <li> #$id – <b>{$user->username}</b> – {$user->name} <{$user->email}> </li> HTML; } $htmlUsersList .= <<< HTML </ul> HTML; // Construct the replacement table $substitutions = $this->exceptionsHandler->getEmailVariables('', [ '[INFO]' => $htmlUsersList, ]); // Let's get the most suitable email template $template = $this->exceptionsHandler->getEmailTemplate('superuserslist', true); // Got no template, the user didn't published any email template, or the template doesn't want us to // send a notification email. Anyway, let's stop here. if (!$template) { return; } $subject = $template[0]; $body = $template[1]; foreach ($substitutions as $k => $v) { $subject = str_replace($k, $v, $subject); $body = str_replace($k, $v, $body); } try { $config = $this->container->platform->getConfig(); $mailer = Factory::getMailer(); $mailfrom = $config->get('mailfrom'); $fromname = $config->get('fromname'); $recipients = explode(',', $this->cparams->getValue('emailbreaches', '')); $recipients = array_map('trim', $recipients); foreach ($recipients as $recipient) { if (empty($recipient)) { continue; } // This line is required because SpamAssassin is BROKEN $mailer->Priority = 3; $mailer->isHtml(true); $mailer->setSender([$mailfrom, $fromname]); // Resets the recipients, otherwise they will pile up $mailer->clearAllRecipients(); if ($mailer->addRecipient($recipient) === false) { // Failed to add a recipient? continue; } $mailer->setSubject($subject); $mailer->setBody($body); $mailer->Send(); } } catch (Exception $e) { // Joomla! 3.5 and later throw an exception when crap happens instead of suppressing it and returning false } } /** * Get the IDs of users who are members of one or more groups in the $groups list * * @param array $groups The users must be a member of at least one of these groups * * @return array */ private function getUsersInGroups(array $groups) { $db = $this->container->db; $ret = []; $groups = array_map([$db, 'q'], $groups); try { $query = $db->getQuery(true) ->select($db->qn('user_id')) ->from($db->qn('#__user_usergroup_map') . ' AS ' . $db->qn('m')) ->innerJoin($db->qn('#__users') . ' AS ' . $db->qn('u') . 'ON(' . $db->qn('u.id') . ' = ' . $db->qn('m.user_id') . ')') ->where($db->qn('group_id') . ' IN(' . implode(',', $groups) . ')') ->where($db->qn('block') . ' = ' . $db->q('0')) // Don't look only for empty string. Joomla! considers '' and '0' identical and will let you log in! ->where('(' . '(' . $db->qn('activation') . ' = ' . $db->q('0') . ') OR ' . '(' . $db->qn('activation') . ' = ' . $db->q('') . ')' . ')'); $db->setQuery($query); $rawUserIDs = $db->loadColumn(0); } catch (Exception $exc) { return $ret; } if (empty($rawUserIDs)) { return $ret; } return array_unique($rawUserIDs); } /** * Are we currently in the backend, with a logged in Super User? * * @return bool */ private function isBackendSuperUser() { $app = Factory::getApplication(); // Not a valid application object? if (!is_object($app)) { return false; } $isCMSApp = version_compare(JVERSION, '3.99999.99999', 'gt') ? ($app instanceof CMSApplication) : $app instanceof CMSApplication; if (!$isCMSApp) { return false; } // Are we in the backend? $isAdmin = method_exists($app, 'isAdmin') ? $app->isAdmin() : $app->isClient('administrator'); if (!$isAdmin) { return false; } // Not a Super User? if (!$this->container->platform->getUser()->authorise('core.admin')) { return false; } return true; } }