283 lines
6.3 KiB
PHP
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');
|
|
}
|
|
} |