Files
conservatorio-tomadini/plugins/system/nrframework/NRFramework/Executer.php
2024-12-31 11:07:09 +01:00

283 lines
6.3 KiB
PHP

<?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die;
use Joomla\Registry\Registry;
use Joomla\Filesystem\File;
use Joomla\CMS\Factory;
/**
* Cleverly evaluate php code using a temporary file and without using the evil eval() PHP method
*/
class Executer
{
/**
* The php code is going to be executed
*
* @var string
*/
private $php_code;
/**
* The data object passed as argument to function
*
* @var mixed
*/
private $payload;
/**
* Executer configuration
*
* @var object
*/
private $options;
/**
* Class constructor
*
* @param string $php_code The php code is going to be executed
*/
public function __construct($php_code = null, &$payload = null, $options = array())
{
$this->setPhpCode($php_code);
$this->setPayload($payload);
// Default options
$defaults = [
'forbidden_php_functions' => [
'fopen',
'popen',
'unlink',
'rmdir',
'dl',
'escapeshellarg',
'escapeshellcmd',
'exec',
'passthru',
'proc_close',
'proc_open',
'shell_exec',
'symlink',
'system',
'pcntl_exec',
'eval',
'create_function'
]
];
$options = array_merge($defaults, $options);
$this->options = new Registry($options);
}
/**
* Payload contains the variables passed as argumentse into the PHP code
*
* @param mixed $data
*
* @return void
*/
public function setPayload(&$data)
{
$this->payload = &$data;
return $this;
}
/**
* Set forbidden PHP functions. If any found, the whole PHP block won't run.
*
* @param array $functions
*
* @return void
*/
public function setForbiddenPHPFunctions($functions)
{
if (empty($functions))
{
return $this;
}
if (is_string($functions))
{
$functions = explode(',', $functions);
}
$this->options->set('forbidden_php_functions', $functions);
return $this;
}
/**
* Helper method to set the php code is about to be executed
*
* @param string $php_code
*
* @return void
*/
public function setPhpCode($php_code)
{
$this->php_code = $php_code;
return $this;
}
/**
* Checks if given PHP code is valid and it's allowed to run.
*
* @return bool
*/
private function allowedToRun()
{
// Get the forbidden PHP functions, but we want to make sure we whitelist 'curl_exec'
$forbidden_functions = array_diff($this->options->get('forbidden_php_functions'), ['curl_exec']);
// Build the regex, making sure 'exec' does not match within 'curl_exec'
$re = '/\b(' . implode('|', $forbidden_functions) . ')\b(\s*\(|\s+[\'"])/mi';
preg_match_all($re, $this->php_code ?? '', $matches);
if (!empty($matches[0]))
{
return false;
}
// Check for backticks ``
if ($has_back_ticks = preg_match('/`(.*?)`/s', $this->php_code ?? ''))
{
return false;
}
return true;
}
/**
* Run function
*
* @return function
*/
public function run()
{
if (!$this->allowedToRun())
{
return;
}
$function_name = $this->getFunctionName();
// Function doesn't exist. Let's create it.
if (!function_exists($function_name))
{
if (!$this->createFunction())
{
return;
}
}
return $function_name($this->payload);
}
/**
* Creates a temporary function in memory
*
* @return void
*/
private function createFunction()
{
$function_name = $this->getFunctionName();
$function_content = $this->getFunctionContent();
$temp_file = $this->getTempPath() . '/' . $function_name;
// Write function's content to a temporary file
File::write($temp_file, $function_content);
// Include file
include_once $temp_file;
// Delete file
if (!defined('JDEBUG') || !JDEBUG)
{
@chmod($temp_file, 0777);
@unlink($temp_file);
}
return function_exists($function_name);
}
/**
* Get temporary file content
*
* @return string
*/
private function getFunctionContent()
{
$function_name = $this->getFunctionName();
$variables = $this->getFunctionVariables();
$contents = [
'<?php',
'defined(\'_JEXEC\') or die;',
'function ' . $function_name . '(&$displayData = null) {
if ($displayData) {
extract($displayData, EXTR_REFS);
}
',
implode("\n", $variables),
$this->php_code,
';return true;}'
];
$contents = implode("\n", $contents);
// Remove Zero Width spaces / (non-)joiners
$contents = str_replace(
[
"\xE2\x80\x8B",
"\xE2\x80\x8C",
"\xE2\x80\x8D",
],
'',
$contents
);
return $contents;
}
/**
* Make user's life easier by initializing some Joomla helpful variables
*
* @return array
*/
protected function getFunctionVariables()
{
return [
'$app = $mainframe = \Joomla\CMS\Factory::getApplication();',
'$document = $doc = \Joomla\CMS\Factory::getDocument();',
'$database = $db = \Joomla\CMS\Factory::getDbo();',
'$user = \Joomla\CMS\Factory::getUser();',
'$Itemid = $app->input->getInt(\'Itemid\');'
];
}
/**
* Construct a temporary function name
*
* @return string
*/
private function getFunctionName()
{
return 'tassos_php_' . md5($this->php_code ?? '');
}
/**
* Return Joomla temporary path
*
* @return void
*/
private function getTempPath()
{
return Factory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp');
}
}