From dd5ab7bdc213381ee552001dd80c41ca47afab00 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 22 Aug 2020 11:41:33 +0100 Subject: [PATCH] Remove webconsole, fixes RCE via json-rpc. Thanks @lb0x --- app/css/custom.php | 15 - app/css/lightsout.css | 10 - includes/adblock.php | 0 includes/internetRoute.php | 0 includes/webconsole.php | 938 ----------------------------------- templates/system.php | 6 - templates/system/console.php | 11 - 7 files changed, 980 deletions(-) mode change 100644 => 100755 includes/adblock.php mode change 100644 => 100755 includes/internetRoute.php delete mode 100755 includes/webconsole.php delete mode 100644 templates/system/console.php diff --git a/app/css/custom.php b/app/css/custom.php index 38415372..f6b18318 100644 --- a/app/css/custom.php +++ b/app/css/custom.php @@ -129,21 +129,6 @@ i.fa.fa-bars:hover{ float: left; } -.webconsole { - width:100%; - height:100%; - border:1px solid; -} - -#console { - height:500px; -} - -.systemtabcontent { - height:100%; - min-height:500px; -} - .service-status { border-width: 0; } diff --git a/app/css/lightsout.css b/app/css/lightsout.css index e70c4fd7..55eb4b54 100644 --- a/app/css/lightsout.css +++ b/app/css/lightsout.css @@ -357,16 +357,6 @@ color: #d2d2d2 !important border-color: #404040; } -.webconsole { - width: 100%; - height: 20rem; - border: 1px solid #404040; -} - -#console { - height: 500px; -} - tspan, rect { fill: #d2d2d2; } diff --git a/includes/adblock.php b/includes/adblock.php old mode 100644 new mode 100755 diff --git a/includes/internetRoute.php b/includes/internetRoute.php old mode 100644 new mode 100755 diff --git a/includes/webconsole.php b/includes/webconsole.php deleted file mode 100755 index 8ea74be2..00000000 --- a/includes/webconsole.php +++ /dev/null @@ -1,938 +0,0 @@ - 'password1', 'user2' => 'password2'); -$ACCOUNTS = array(); - -// Password hash algorithm (password must be hashed) -// Example: $PASSWORD_HASH_ALGORITHM = 'md5'; -// $PASSWORD_HASH_ALGORITHM = 'sha256'; -$PASSWORD_HASH_ALGORITHM = ''; - -// Home directory (multi-user mode supported) -// Example: $HOME_DIRECTORY = '/tmp'; -// $HOME_DIRECTORY = array('user1' => '/home/user1', 'user2' => '/home/user2'); -$HOME_DIRECTORY = ''; - -// Code below is automatically generated from different components -// For more information see: https://github.com/nickola/web-console -// -// Used components: -// - jQuery JavaScript Library: https://github.com/jquery/jquery -// - jQuery Terminal Emulator: https://github.com/jcubic/jquery.terminal -// - jQuery Mouse Wheel Plugin: https://github.com/brandonaaron/jquery-mousewheel -// - PHP JSON-RPC 2.0 Server/Client Implementation: https://github.com/sergeyfast/eazy-jsonrpc -// - Normalize.css: https://github.com/necolas/normalize.css -?> - method - */ - protected $instances = array(); - - /** - * Decoded Json Request - * - * @var object|array - */ - protected $request; - - /** - * Array of Received Calls - * - * @var array - */ - protected $calls = array(); - - /** - * Array of Responses for Calls - * - * @var array - */ - protected $response = array(); - - /** - * Has Calls Flag (not notifications) - * - * @var bool - */ - protected $hasCalls = false; - - /** - * Is Batch Call in using - * - * @var bool - */ - private $isBatchCall = false; - - /** - * Hidden Methods - * - * @var array - */ - protected $hiddenMethods = array( - 'execute', '__construct', 'registerinstance' - ); - - /** - * Content Type - * - * @var string - */ - public $ContentType = 'application/json'; - - /** - * Allow Cross-Domain Requests - * - * @var bool - */ - public $IsXDR = true; - - /** - * Max Batch Calls - * - * @var int - */ - public $MaxBatchCalls = 10; - - /** - * Error Messages - * - * @var array - */ - protected $errorMessages = array( - self::ParseError => 'Parse error', - self::InvalidRequest => 'Invalid Request', - self::MethodNotFound => 'Method not found', - self::InvalidParams => 'Invalid params', - self::InternalError => 'Internal error', - ); - - - /** - * Cached Reflection Methods - * - * @var ReflectionMethod[] - */ - private $reflectionMethods = array(); - - - /** - * Validate Request - * - * @return int error - */ - private function getRequest() - { - $error = null; - - do { - if (array_key_exists('REQUEST_METHOD', $_SERVER) && $_SERVER['REQUEST_METHOD'] != 'POST' ) { - $error = self::InvalidRequest; - break; - }; - - $request = !empty($_GET['rawRequest']) ? $_GET['rawRequest'] : file_get_contents('php://input'); - $this->request = json_decode($request, false); - if ($this->request === null ) { - $error = self::ParseError; - break; - } - - if ($this->request === array() ) { - $error = self::InvalidRequest; - break; - } - - // check for batch call - if (is_array($this->request) ) { - if(count($this->request) > $this->MaxBatchCalls ) { - $error = self::InvalidRequest; - break; - } - - $this->calls = $this->request; - $this->isBatchCall = true; - } else { - $this->calls[] = $this->request; - } - } while ( false ); - - return $error; - } - - - /** - * Get Error Response - * - * @param int $code - * @param mixed $id - * @param null $data - * @return array - */ - private function getError( $code, $id = null, $data = null ) - { - return array( - 'jsonrpc' => '2.0', - 'id' => $id, - 'error' => array( - 'code' => $code, - 'message' => isset($this->errorMessages[$code]) ? $this->errorMessages[$code] : $this->errorMessages[self::InternalError], - 'data' => $data, - ), - ); - } - - - /** - * Check for jsonrpc version and correct method - * - * @param object $call - * @return array|null - */ - private function validateCall( $call ) - { - $result = null; - $error = null; - $data = null; - $id = is_object($call) && property_exists($call, 'id') ? $call->id : null; - do { - if (!is_object($call) ) { - $error = self::InvalidRequest; - break; - } - - // hack for inputEx smd tester - if (property_exists($call, 'version') ) { - if ($call->version == 'json-rpc-2.0' ) { - $call->jsonrpc = '2.0'; - } - } - - if (!property_exists($call, 'jsonrpc') || $call->jsonrpc != '2.0' ) { - $error = self::InvalidRequest; - break; - } - - $fullMethod = property_exists($call, 'method') ? $call->method : ''; - $methodInfo = explode('.', $fullMethod, 2); - $namespace = array_key_exists(1, $methodInfo) ? $methodInfo[0] : ''; - $method = $namespace ? $methodInfo[1] : $fullMethod; - if (!$method || !array_key_exists($namespace, $this->instances) || !method_exists($this->instances[$namespace], $method) || in_array(strtolower($method), $this->hiddenMethods) ) { - $error = self::MethodNotFound; - break; - } - - if (!array_key_exists($fullMethod, $this->reflectionMethods) ) { - $this->reflectionMethods[$fullMethod] = new ReflectionMethod($this->instances[$namespace], $method); - } - - /** - * @var $params array -*/ - $params = property_exists($call, 'params') ? $call->params : null; - $paramsType = gettype($params); - if ($params !== null && $paramsType != 'array' && $paramsType != 'object' ) { - $error = self::InvalidParams; - break; - } - - // check parameters - switch ( $paramsType ) { - case 'array': - $totalRequired = 0; - // doesn't hold required, null, required sequence of params - foreach ( $this->reflectionMethods[$fullMethod]->getParameters() as $param ) { - if (!$param->isDefaultValueAvailable() ) { - $totalRequired++; - } - } - - if (count($params) < $totalRequired ) { - $error = self::InvalidParams; - $data = sprintf('Check numbers of required params (got %d, expected %d)', count($params), $totalRequired); - } - break; - case 'object': - foreach ( $this->reflectionMethods[$fullMethod]->getParameters() as $param ) { - if (!$param->isDefaultValueAvailable() && !array_key_exists($param->getName(), $params) ) { - $error = self::InvalidParams; - $data = $param->getName() . ' not found'; - - break 3; - } - } - break; - case 'NULL': - if ($this->reflectionMethods[$fullMethod]->getNumberOfRequiredParameters() > 0 ) { - $error = self::InvalidParams; - $data = 'Empty required params'; - break 2; - } - break; - } - - } while ( false ); - - if ($error ) { - $result = array( $error, $id, $data ); - } - - return $result; - } - - - /** - * Process Call - * - * @param $call - * @return array|null - */ - private function processCall( $call ) - { - $id = property_exists($call, 'id') ? $call->id : null; - $params = property_exists($call, 'params') ? $call->params : array(); - $result = null; - $namespace = substr($call->method, 0, strpos($call->method, '.')); - - try { - // set named parameters - if (is_object($params) ) { - $newParams = array(); - foreach ( $this->reflectionMethods[$call->method]->getParameters() as $param ) { - $paramName = $param->getName(); - $defaultValue = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; - $newParams[] = property_exists($params, $paramName) ? $params->$paramName : $defaultValue; - } - - $params = $newParams; - } - - // invoke - $result = $this->reflectionMethods[$call->method]->invokeArgs($this->instances[$namespace], $params); - } catch ( Exception $e ) { - return $this->getError($e->getCode(), $id, $e->getMessage()); - } - - if (!$id && $id !== 0 ) { - return null; - } - - return array( - 'jsonrpc' => '2.0', - 'result' => $result, - 'id' => $id, - ); - } - - - /** - * Create new Instance - * - * @param object $instance - */ - public function __construct( $instance = null ) - { - if (get_parent_class($this) ) { - $this->RegisterInstance($this, ''); - } else if ($instance ) { - $this->RegisterInstance($instance, ''); - } - } - - - /** - * Register Instance - * - * @param object $instance - * @param string $namespace default is empty string - * @return $this - */ - public function RegisterInstance( $instance, $namespace = '' ) - { - $this->instances[$namespace] = $instance; - $this->instances[$namespace]->errorMessages = $this->errorMessages; - - return $this; - } - - - /** - * Handle Requests - */ - public function Execute() - { - do { - // check for SMD Discovery request - if (array_key_exists('smd', $_GET) ) { - $this->response[] = $this->getServiceMap(); - $this->hasCalls = true; - break; - } - - $error = $this->getRequest(); - if ($error ) { - $this->response[] = $this->getError($error); - $this->hasCalls = true; - break; - } - - foreach ( $this->calls as $call ) { - $error = $this->validateCall($call); - if ($error ) { - $this->response[] = $this->getError($error[0], $error[1], $error[2]); - $this->hasCalls = true; - } else { - $result = $this->processCall($call); - if ($result ) { - $this->response[] = $result; - $this->hasCalls = true; - } - } - } - } while ( false ); - - // flush response - if ($this->hasCalls ) { - if (!$this->isBatchCall ) { - $this->response = reset($this->response); - } - - if (!headers_sent() ) { - // Set Content Type - if ($this->ContentType ) { - header('Content-Type: ' . $this->ContentType); - } - - // Allow Cross Domain Requests - if ($this->IsXDR ) { - header('Access-Control-Allow-Origin: *'); - header('Access-Control-Allow-Headers: x-requested-with, content-type'); - } - } - - echo json_encode($this->response); - $this->resetVars(); - } - } - - - /** - * Get Doc Comment - * - * @param $comment - * @return string|null - */ - private function getDocDescription( $comment ) - { - $result = null; - if (preg_match('/\*\s+([^@]*)\s+/s', $comment, $matches) ) { - $result = str_replace('*', "\n", trim(trim($matches[1], '*'))); - } - - return $result; - } - - - /** - * Get Service Map - * Maybe not so good realization of auto-discover via doc blocks - * - * @return array - */ - private function getServiceMap() - { - $result = array( - 'transport' => 'POST', - 'envelope' => 'JSON-RPC-2.0', - 'SMDVersion' => '2.0', - 'contentType' => 'application/json', - 'target' => !empty($_SERVER['REQUEST_URI']) ? substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '?')) : '', - 'services' => array(), - 'description' => '', - ); - - foreach( $this->instances as $namespace => $instance ) { - $rc = new ReflectionClass($instance); - - // Get Class Description - if ($rcDocComment = $this->getDocDescription($rc->getDocComment()) ) { - $result['description'] .= $rcDocComment . PHP_EOL; - } - - foreach ( $rc->getMethods() as $method ) { - /** - * @var ReflectionMethod $method -*/ - if (!$method->isPublic() || in_array(strtolower($method->getName()), $this->hiddenMethods) ) { - continue; - } - - $methodName = ( $namespace ? $namespace . '.' : '' ) . $method->getName(); - $docComment = $method->getDocComment(); - - $result['services'][$methodName] = array( 'parameters' => array() ); - - // set description - if ($rmDocComment = $this->getDocDescription($docComment) ) { - $result['services'][$methodName]['description'] = $rmDocComment; - } - - // @param\s+([^\s]*)\s+([^\s]*)\s*([^\s\*]*) - $parsedParams = array(); - if (preg_match_all('/@param\s+([^\s]*)\s+([^\s]*)\s*([^\n\*]*)/', $docComment, $matches) ) { - foreach ( $matches[2] as $number => $name ) { - $type = $matches[1][$number]; - $desc = $matches[3][$number]; - $name = trim($name, '$'); - - $param = array( 'type' => $type, 'description' => $desc ); - $parsedParams[$name] = array_filter($param); - } - }; - - // process params - foreach ( $method->getParameters() as $parameter ) { - $name = $parameter->getName(); - $param = array( 'name' => $name, 'optional' => $parameter->isDefaultValueAvailable() ); - if (array_key_exists($name, $parsedParams) ) { - $param += $parsedParams[$name]; - } - - if ($param['optional'] ) { - $param['default'] = $parameter->getDefaultValue(); - } - - $result['services'][$methodName]['parameters'][] = $param; - } - - // set return type - if (preg_match('/@return\s+([^\s]+)\s*([^\n\*]+)/', $docComment, $matches) ) { - $returns = array( 'type' => $matches[1], 'description' => trim($matches[2]) ); - $result['services'][$methodName]['returns'] = array_filter($returns); - } - } - } - - return $result; - } - - - /** - * Reset Local Class Vars after Execute - */ - private function resetVars() - { - $this->response = $this->calls = array(); - $this->hasCalls = $this->isBatchCall = false; - } - -} -?> -= 1) ? true : false; - -// Utilities -function is_empty_string($string) -{ - return strlen($string) <= 0; -} - -function is_equal_strings($string1, $string2) -{ - return strcmp($string1, $string2) == 0; -} - -function get_hash($algorithm, $string) -{ - return hash($algorithm, trim((string) $string)); -} - -// Command execution -function execute_command($command) -{ - $descriptors = array( - 0 => array('pipe', 'r'), // STDIN - 1 => array('pipe', 'w'), // STDOUT - 2 => array('pipe', 'w') // STDERR - ); - - $process = proc_open($command . ' 2>&1', $descriptors, $pipes); - if (!is_resource($process)) { die("Can't execute command."); - } - - // Nothing to push to STDIN - fclose($pipes[0]); - - $output = stream_get_contents($pipes[1]); - fclose($pipes[1]); - - $error = stream_get_contents($pipes[2]); - fclose($pipes[2]); - - // All pipes must be closed before "proc_close" - $code = proc_close($process); - - return $output; -} - -// Command parsing -function parse_command($command) -{ - $value = ltrim((string) $command); - - if (!is_empty_string($value)) { - $values = explode(' ', $value); - $values_total = count($values); - - if ($values_total > 1) { - $value = $values[$values_total - 1]; - - for ($index = $values_total - 2; $index >= 0; $index--) { - $value_item = $values[$index]; - - if (substr($value_item, -1) == '\\') { $value = $value_item . ' ' . $value; - } else { break; - } - } - } - } - - return $value; -} - -// RPC Server -class WebConsoleRPCServer extends BaseJsonRpcServer -{ - protected $home_directory = ''; - - private function error($message) - { - throw new Exception($message); - } - - // Authentication - private function authenticate_user($user, $password) - { - $user = trim((string) $user); - $password = trim((string) $password); - - if ($user && $password) { - global $ACCOUNTS, $PASSWORD_HASH_ALGORITHM; - - if (isset($ACCOUNTS[$user]) && !is_empty_string($ACCOUNTS[$user])) { - if ($PASSWORD_HASH_ALGORITHM) { $password = get_hash($PASSWORD_HASH_ALGORITHM, $password); - } - - if (is_equal_strings($password, $ACCOUNTS[$user])) { - return $user . ':' . get_hash('sha256', $password); - } - } - } - - throw new Exception("Incorrect user or password"); - } - - private function authenticate_token($token) - { - global $NO_LOGIN; - if ($NO_LOGIN) { return true; - } - - $token = trim((string) $token); - $token_parts = explode(':', $token, 2); - - if (count($token_parts) == 2) { - $user = trim((string) $token_parts[0]); - $password_hash = trim((string) $token_parts[1]); - - if ($user && $password_hash) { - global $ACCOUNTS; - - if (isset($ACCOUNTS[$user]) && !is_empty_string($ACCOUNTS[$user])) { - $real_password_hash = get_hash('sha256', $ACCOUNTS[$user]); - if (is_equal_strings($password_hash, $real_password_hash)) { return $user; - } - } - } - } - - throw new Exception("Incorrect user or password"); - } - - private function get_home_directory($user) - { - global $HOME_DIRECTORY; - - if (is_string($HOME_DIRECTORY)) { - if (!is_empty_string($HOME_DIRECTORY)) { return $HOME_DIRECTORY; - } - } - else if (is_string($user) && !is_empty_string($user) && isset($HOME_DIRECTORY[$user]) && !is_empty_string($HOME_DIRECTORY[$user])) { - return $HOME_DIRECTORY[$user]; - } - - return getcwd(); - } - - // Environment - private function get_environment() - { - $hostname = function_exists('gethostname') ? gethostname() : null; - return array('path' => getcwd(), 'hostname' => $hostname); - } - - private function set_environment($environment) - { - $environment = !empty($environment) ? (array) $environment : array(); - $path = (isset($environment['path']) && !is_empty_string($environment['path'])) ? $environment['path'] : $this->home_directory; - - if (!is_empty_string($path)) { - if (is_dir($path)) { - if (!@chdir($path)) { return array('output' => "Unable to change directory to current working directory, updating current directory", - 'environment' => $this->get_environment()); - } - } - else { return array('output' => "Current working directory not found, updating current directory", - 'environment' => $this->get_environment()); - } - } - } - - // Initialization - private function initialize($token, $environment) - { - $user = $this->authenticate_token($token); - $this->home_directory = $this->get_home_directory($user); - $result = $this->set_environment($environment); - - if ($result) { return $result; - } - } - - // Methods - public function login($user, $password) - { - $result = array('token' => $this->authenticate_user($user, $password), - 'environment' => $this->get_environment()); - - $home_directory = $this->get_home_directory($user); - if (!is_empty_string($home_directory)) { - if (is_dir($home_directory)) { $result['environment']['path'] = $home_directory; - } else { $result['output'] = "Home directory not found: ". $home_directory; - } - } - - return $result; - } - - public function cd($token, $environment, $path) - { - $result = $this->initialize($token, $environment); - if ($result) { return $result; - } - - $path = trim((string) $path); - if (is_empty_string($path)) { $path = $this->home_directory; - } - - if (!is_empty_string($path)) { - if (is_dir($path)) { - if (!@chdir($path)) { return array('output' => "cd: ". $path . ": Unable to change directory"); - } - } - else { return array('output' => "cd: ". $path . ": No such directory"); - } - } - - return array('environment' => $this->get_environment()); - } - - public function completion($token, $environment, $pattern, $command) - { - $result = $this->initialize($token, $environment); - if ($result) { return $result; - } - - $scan_path = ''; - $completion_prefix = ''; - $completion = array(); - - if (!empty($pattern)) { - if (!is_dir($pattern)) { - $pattern = dirname($pattern); - if ($pattern == '.') { $pattern = ''; - } - } - - if (!empty($pattern)) { - if (is_dir($pattern)) { - $scan_path = $completion_prefix = $pattern; - if (substr($completion_prefix, -1) != '/') { $completion_prefix .= '/'; - } - } - } - else { $scan_path = getcwd(); - } - } - else { $scan_path = getcwd(); - } - - if (!empty($scan_path)) { - // Loading directory listing - $completion = array_values(array_diff(scandir($scan_path), array('..', '.'))); - natsort($completion); - - // Prefix - if (!empty($completion_prefix) && !empty($completion)) { - foreach ($completion as &$value) { $value = $completion_prefix . $value; - } - } - - // Pattern - if (!empty($pattern) && !empty($completion)) { - // For PHP version that does not support anonymous functions (available since PHP 5.3.0) - function filter_pattern($value) - { - global $pattern; - return !strncmp($pattern, $value, strlen($pattern)); - } - - $completion = array_values(array_filter($completion, 'filter_pattern')); - } - } - - return array('completion' => $completion); - } - - public function run($token, $environment, $command) - { - $result = $this->initialize($token, $environment); - if ($result) { return $result; - } - - $output = ($command && !is_empty_string($command)) ? execute_command($command) : ''; - if ($output && substr($output, -1) == "\n") { $output = substr($output, 0, -1); - } - - return array('output' => $output); - } -} - -// Processing request -if (array_key_exists('REQUEST_METHOD', $_SERVER) && $_SERVER['REQUEST_METHOD'] == 'POST') { - $rpc_server = new WebConsoleRPCServer(); - $rpc_server->Execute(); -} -else if (!$IS_CONFIGURED) { - ?> - - - - - - Web Console - - - - - - - -
-

Web Console must be configured before use:

- -

For more information visit Web Console website: http://web-console.org

-
- - - - - - - - - Web Console - - - - - - - - - - diff --git a/templates/system.php b/templates/system.php index 902b6953..0e983186 100755 --- a/templates/system.php +++ b/templates/system.php @@ -16,19 +16,13 @@ - - -
-
-
- diff --git a/templates/system/console.php b/templates/system/console.php deleted file mode 100644 index 009af68d..00000000 --- a/templates/system/console.php +++ /dev/null @@ -1,11 +0,0 @@ - -
-
-
- - - -
-
-
-