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\Mixin\RunPluginsTrait;
use Akeeba\Component\AdminTools\Administrator\Mixin\TableNoSuperUsersCheckFlagsTrait;
use Akeeba\Component\AdminTools\Administrator\Mixin\TempSuperUserChecksTrait;
use Exception;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormFactoryInterface;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\CMS\User\UserHelper;
use RuntimeException;
#[\AllowDynamicProperties]
class TempsuperuserModel extends AdminModel
{
use TableNoSuperUsersCheckFlagsTrait;
use TempSuperUserChecksTrait;
use RunPluginsTrait;
/**
* Cache of user group IDs with Super User privileges
*
* @var array
* @since 5.3.0
*/
protected $superUserGroups = [];
public function __construct($config = [], MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null)
{
parent::__construct($config, $factory, $formFactory);
$this->_parent_table = '';
}
/**
* @inheritDoc
*/
public function getForm($data = [], $loadData = true)
{
$pk = (int) $this->getState($this->getName() . '.id');
$id = $data['user_id'] ?? $pk;
$user = empty($id) ? new User() : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($id);
$formName = (!empty($id) && ($user->id == $id)) ? 'tempsuperuser' : 'tempsuperuser_wizard';
$form = $this->loadForm(
'com_admintools.' . $formName,
$formName,
[
'control' => 'jform',
'load_data' => $loadData,
]
) ?: false;
if (empty($form))
{
return false;
}
return $form;
}
public function save($data)
{
try
{
$this->setNoCheckFlags(true);
$isNew = empty($data['user_id'] ?? null);
$data['user_id'] = ($data['user_id'] ?? null) ?: $this->getUserFromData($data);
}
catch (Exception $e)
{
$this->setError($e->getMessage());
return false;
}
finally
{
$this->setNoCheckFlags(false);
}
try
{
$table = $this->getTable();
$context = $this->option . '.' . $this->name;
$app = Factory::getApplication();
$key = $table->getKeyName();
$pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id');
if (!$isNew)
{
$table->load($pk);
}
// Bind the data.
if (!$table->bind($data))
{
$this->setError($table->getError());
return false;
}
// Prepare the row for saving
$this->prepareTable($table);
// Check the data.
if (!$table->check())
{
$this->setError($table->getError());
return false;
}
// Trigger the before save event.
$result = $this->triggerPluginEvent($this->event_before_save, [$context, $table, $isNew, $data], null, $app);
if (\in_array(false, $result, true))
{
$this->setError($table->getError());
return false;
}
// Store the data.
try
{
if ($isNew)
{
$this->getDatabase()->insertObject($table->getTableName(), $table, $table->getKeyName());
}
else
{
$this->getDatabase()->updateObject($table->getTableName(), $table, $table->getKeyName(), true);
}
}
catch (Exception $e)
{
$this->setError($e->getMessage());
return false;
}
if (!$table->store())
{
$this->setError($table->getError());
return false;
}
// Clean the cache.
$this->cleanCache();
// Trigger the after save event.
$this->triggerPluginEvent($this->event_after_save, [$context, $table, $isNew, $data], null, $app);
}
catch (Exception $e)
{
$this->setError($e->getMessage());
return false;
}
if (isset($table->$key))
{
$this->setState($this->getName() . '.id', $table->$key);
}
$this->setState($this->getName() . '.new', $isNew);
return true;
}
protected function canDelete($record)
{
$this->assertNotMyself($record->id);
return parent::canDelete($record);
}
/**
* Loads the form data.
*
* This method has three modes of operation:
*
* - If there is saved form data in the session and the user_id (PK) matches we'll use that.
* - If we are editing an existing record we load the record.
* - Otherwise it's the wizard layout and we pre-fill it with some sane defaults.
*
* @return array|bool|\Joomla\CMS\Object\CMSObject|mixed
* @throws Exception
*/
protected function loadFormData()
{
/** @var CMSApplication $app */
$app = Factory::getApplication();
$data = $app->getUserState('com_admintools.edit.tempsuperuser.data', []);
$pk = (int) $this->getState($this->getName() . '.id');
$item = ($pk ? (object) $this->getItem()->getProperties() : false) ?: [];
// There's data saved in the session and the user_id in it matches what we're editing
if (!empty($data) && ($item->user_id ?? null) == ($data['user_id'] ?? null))
{
$this->preprocessData('com_admintools.tempsuperuser', $data);
return $data;
}
// First, let's try loading an existing item.
$data = $item;
$pk = (int) $this->getState($this->getName() . '.id');
if ($pk > 0)
{
$this->preprocessData('com_admintools.tempsuperuser', $data);
return $data;
}
// No existing item. I am in the Wizard view. Let's preload it with some randomized, default values.
$jDate = clone Factory::getDate();
$interval = new \DateInterval('P15D');
$superUserGroups = $this->getSuperUserGroups() ?: [8];
// Get a random password respecting Joomla's password restrictions
$uParams = ComponentHelper::getParams('com_users');
$length = max($uParams->get('minimum_length', 8) ?: 8, 32);
$nInt = $uParams->get('minimum_integers', 0) ?: 0;
$nSymbols = $uParams->get('minimum_symbols', 0) ?: 0;
$nUpper = $uParams->get('minimum_uppercase', 0) ?: 0;
$nLower = $uParams->get('minimum_lowercase', 0) ?: 0;
$password = $this->generatePassword($length, $nInt, $nSymbols, $nUpper, $nLower);
return [
'expiration' => $jDate->add($interval)->toRFC822(),
'username' => 'temp' . UserHelper::genRandomPassword(12),
'password' => $password,
'password2' => $password,
'email' => UserHelper::genRandomPassword(12) . '@example.com',
'name' => Text::_('COM_ADMINTOOLS_TEMPSUPERUSERS_LBL_DEFAULTNAME'),
'groups' => array_shift($superUserGroups),
];
}
/**
* Generate a random password with specific restrictions
*
* @param int $length Password length
* @param int|null $minInt Minimum number of integers to include
* @param int|null $minSymbol Minimum number of symbols to include
* @param int|null $minUpper Minimum number of uppercase English letters to include
* @param int|null $minLower Minimum number of lowercase English letters to include
*
* @return string A secure, random password
* @throws Exception
*/
private function generatePassword(int $length = 64, ?int $minInt = 0, ?int $minSymbol = 0, ?int $minUpper = 0, ?int $minLower = 0): string
{
$sNumbers = '1234567890';
$sSymbols = '~!@#$%^&*()_+[]{};:\'"\|,<.>/?';
$sUpper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$sLower = 'abcdefghijklmnopqrstuvwxyz';
$sEverything = $sLower . $sUpper . $sNumbers . $sSymbols;
$getRandomCharacters = function (int $length, string $salt = '') use ($sEverything) {
$salt = $salt ?: $sEverything;
$base = \strlen($salt);
$makepass = '';
$random = random_bytes($length + 1);
$shift = \ord($random[0]);
for ($i = 1; $i <= $length; ++$i)
{
$makepass .= $salt[($shift + \ord($random[$i])) % $base];
$shift += \ord($random[$i]);
}
return $makepass;
};
// The password length is at least the sum of the minimum occurrences set up
$minLength = ($minInt ?? 0) + ($minSymbol ?? 0) + ($minUpper ?? 0) + ($minLower ?? 0);
$length = max($length, $minLength);
$pass = '';
// If there are no requirements on minimum number of characters return a truly random password and be done with it
if ($minLength === 0)
{
return $getRandomCharacters($length, $sUpper . $sLower . $sNumbers);
}
// Create a minimum number of integers
if (($minInt ?? 0) > 0)
{
$pass .= $getRandomCharacters($minInt, $sNumbers);
}
// Create a minimum number of symbols
if (($minSymbol ?? 0) > 0)
{
$pass .= $getRandomCharacters($minSymbol, $sSymbols);
}
// Create a minimum number of uppercase characters
if (($minUpper ?? 0) > 0)
{
$pass .= $getRandomCharacters($minUpper, $sUpper);
}
// Create a minimum number of lowercase characters
if (($minLower ?? 0) > 0)
{
$pass .= $getRandomCharacters($minUpper, $sLower);
}
// Add random characters for the remaining length of the password
$remainingLength = $length - $minLength + 1;
if ($remainingLength > 0)
{
$pass .= $getRandomCharacters($remainingLength, $sEverything);
}
// Shuffle the characters
for ($i = 0; $i < strlen($sEverything) * strlen($pass); $i++)
{
$from = random_int(0, $length - 1);
$to = random_int(0, $length - 1);
$temp = $pass[$to];
$pass[$to] = $pass[$from];
$pass[$from] = $temp;
}
return $pass;
}
/**
* Returns all Joomla! user groups
*
* @return array
*
* @since 5.3.0
*/
private function getSuperUserGroups()
{
if (!empty($this->superUserGroups))
{
return $this->superUserGroups;
}
// Get all groups
$db = $this->getDatabase();
$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;
}
private function getUserFromData($info)
{
$info['block'] = 0;
$info['sendEmail'] = 0;
$info['lastvisitDate'] = (clone Factory::getDate())->toSql();
$info['activation'] = '';
$info['otpKey'] = '';
$info['otep'] = '';
$info['requireReset'] = 0;
$userId = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserByUsername($info['username'])->id;
if (empty($userId))
{
return $this->createNewUser($info);
}
// Make sure I am not trying to edit myself
if ($userId == Factory::getApplication()->getIdentity()->id)
{
throw new RuntimeException(Text::_('COM_ADMINTOOLS_TEMPSUPERUSERS_ERR_CANTEDITSELF'), 403);
}
// Get the existing user
$user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
// Make sure the user is a Super User
if (!$user->authorise('core.admin'))
{
throw new RuntimeException(Text::_('COM_ADMINTOOLS_TEMPSUPERUSERS_ERR_NOTSUPER'), 500);
}
// Make sure the user was already blocked
if (!$user->block)
{
throw new RuntimeException(Text::_('COM_ADMINTOOLS_TEMPSUPERUSERS_ERR_NOTBLOCKED'), 500);
}
// Apply changes to the existing user
$user->bind($info);
$saved = $user->save();
if (!$saved)
{
throw new RuntimeException($user->getError());
}
return $userId;
}
private function createNewUser(&$info)
{
// Make sure $info['groups'] is defined and defines at least one Super User group
$superUserGroups = $this->getSuperUserGroups();
$usedSUGroups = array_intersect($info['groups'], $superUserGroups);
if (empty($usedSUGroups))
{
$this->setNoCheckFlags(false);
throw new RuntimeException(Text::_('COM_ADMINTOOLS_TEMPSUPERUSERS_ERR_NOTASUPERUSER'), 500);
}
// Create a new user
$user = new User();
// Set the user's default language to whatever the site's current language is
$info['params'] = [
'language' => Factory::getApplication()->get('language'),
];
$user->bind($info);
$saved = $user->save();
if (!$saved)
{
$this->setNoCheckFlags(false);
throw new RuntimeException($user->getError());
}
$this->addUserToSafeId($user->id);
return $user->id;
}
/**
* Adds a new user into the list of "safe ids", otherwise at the next session load it will be disabled by the
* feature "Monitor Super User accounts"
*
* @param int $userid ID of the new user that should be injected into the list
*/
private function addUserToSafeId($userid)
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('value'))
->from($db->quoteName('#__admintools_storage'))
->where($db->quoteName('key') . ' = ' . $db->quote('superuserslist'));
$db->setQuery($query);
try
{
$jsonData = $db->loadResult();
}
catch (Exception $e)
{
return;
}
$userList = [];
if (!empty($jsonData))
{
$userList = json_decode($jsonData, true);
}
$userList[] = $userid;
$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);
}
}