From 29c8d7e90ec2d49c77efb298a9d8e2964bb34d1c Mon Sep 17 00:00:00 2001 From: Marius Burkard Date: Tue, 17 Nov 2020 12:26:27 +0100 Subject: [PATCH 1/5] - fixed potential shell user vulnerability --- interface/lib/classes/functions.inc.php | 78 +++++++++++++------ interface/lib/classes/tform_base.inc.php | 5 +- interface/web/sites/form/shell_user.tform.php | 6 ++ server/lib/classes/functions.inc.php | 32 +++++++- server/lib/classes/system.inc.php | 30 +++++++ .../shelluser_base_plugin.inc.php | 16 ++++ 6 files changed, 139 insertions(+), 28 deletions(-) diff --git a/interface/lib/classes/functions.inc.php b/interface/lib/classes/functions.inc.php index 03e331f0f1..4d4c011fb5 100644 --- a/interface/lib/classes/functions.inc.php +++ b/interface/lib/classes/functions.inc.php @@ -61,7 +61,7 @@ class functions { if(is_string($to) && strpos($to, ',') !== false) { $to = preg_split('/\s*,\s*/', $to); } - + $app->ispcmail->send($to); $app->ispcmail->finish(); @@ -234,7 +234,7 @@ class functions { if(preg_match($regex, $result['ip'])) $ips[] = $result['ip']; } } - + $results = $app->db->queryAllRecords("SELECT remote_ips FROM web_database WHERE remote_ips != ''"); if(!empty($results) && is_array($results)){ foreach($results as $result){ @@ -290,6 +290,34 @@ class functions { return round(pow(1024, $base-floor($base)), $precision).$suffixes[floor($base)]; } + + /** + * Normalize a path and strip duplicate slashes from it + * + * This will also remove all /../ from the path, reducing the preceding path elements + * + * @param string $path + * @return string + */ + public function normalize_path($path) { + $path = preg_replace('~[/]{2,}~', '/', $path); + $parts = explode('/', $path); + $return_parts = array(); + + foreach($parts as $current_part) { + if($current_part === '..') { + if(!empty($return_parts) && end($return_parts) !== '') { + array_pop($return_parts); + } + } else { + $return_parts[] = $current_part; + } + } + + return implode('/', $return_parts); + } + + /** IDN converter wrapper. * all converter classes should be placed in ISPC_CLASS_PATH.'/idn/' */ @@ -370,42 +398,42 @@ class functions { public function is_allowed_user($username, $restrict_names = false) { global $app; - + $name_blacklist = array('root','ispconfig','vmail','getmail'); if(in_array($username,$name_blacklist)) return false; - + if(preg_match('/^[a-zA-Z0-9\.\-_]{1,32}$/', $username) == false) return false; - + if($restrict_names == true && preg_match('/^web\d+$/', $username) == false) return false; - + return true; } - + public function is_allowed_group($groupname, $restrict_names = false) { global $app; - + $name_blacklist = array('root','ispconfig','vmail','getmail'); if(in_array($groupname,$name_blacklist)) return false; - + if(preg_match('/^[a-zA-Z0-9\.\-_]{1,32}$/', $groupname) == false) return false; - + if($restrict_names == true && preg_match('/^client\d+$/', $groupname) == false) return false; - + return true; } - + public function getimagesizefromstring($string){ if (!function_exists('getimagesizefromstring')) { $uri = 'data://application/octet-stream;base64,' . base64_encode($string); return getimagesize($uri); } else { return getimagesizefromstring($string); - } + } } - + public function password($minLength = 10, $special = false){ global $app; - + $iteration = 0; $password = ""; $maxLength = $minLength + 5; @@ -430,7 +458,7 @@ class functions { public function getRandomInt($min, $max){ return floor((mt_rand() / mt_getrandmax()) * ($max - $min + 1)) + $min; } - + public function generate_customer_no(){ global $app; // generate customer no. @@ -438,13 +466,13 @@ class functions { while($app->db->queryOneRecord("SELECT client_id FROM client WHERE customer_no = ?", $customer_no)) { $customer_no = mt_rand(100000, 999999); } - + return $customer_no; } - + public function generate_ssh_key($client_id, $username = ''){ global $app; - + // generate the SSH key pair for the client $id_rsa_file = '/tmp/'.uniqid('',true); $id_rsa_pub_file = $id_rsa_file.'.pub'; @@ -458,7 +486,7 @@ class functions { $app->log("Failed to create SSH keypair for ".$username, LOGLEVEL_WARN); } } - + public function htmlentities($value) { global $conf; @@ -474,10 +502,10 @@ class functions { } else { $out = htmlentities($value, ENT_QUOTES, $conf["html_content_encoding"]); } - + return $out; } - + // Function to check paths before we use it as include. Use with absolute paths only. public function check_include_path($path) { if(strpos($path,'//') !== false) die('Include path seems to be an URL: '.$this->htmlentities($path)); @@ -488,7 +516,7 @@ class functions { if(substr($path,0,strlen(ISPC_ROOT_PATH)) != ISPC_ROOT_PATH) die('Path '.$this->htmlentities($path).' is outside of ISPConfig installation directory.'); return $path; } - + // Function to check language strings public function check_language($language) { global $app; @@ -496,10 +524,10 @@ class functions { return $language; } else { $app->log('Wrong language string: '.$this->htmlentities($language),1); - return 'en'; + return 'en'; } } - + } ?> diff --git a/interface/lib/classes/tform_base.inc.php b/interface/lib/classes/tform_base.inc.php index 91a855872c..72ddb4b6ae 100644 --- a/interface/lib/classes/tform_base.inc.php +++ b/interface/lib/classes/tform_base.inc.php @@ -399,7 +399,7 @@ class tform_base { $tmp_key = $limit_parts[2]; $allowed = $allowed = explode(',',$tmp_conf[$tmp_key]); } - + if($formtype == 'CHECKBOX') { if(strstr($limit,'force_')) { // Force the checkbox field to be ticked and enabled @@ -958,6 +958,9 @@ class tform_base { case 'STRIPNL': $returnval = str_replace(array("\n","\r"),'', $returnval); break; + case 'NORMALIZEPATH': + $returnval = $app->functions->normalize_path($returnval); + break; default: $this->errorMessage .= "Unknown Filter: ".$filter['type']; break; diff --git a/interface/web/sites/form/shell_user.tform.php b/interface/web/sites/form/shell_user.tform.php index f4e83a1b57..523a03687a 100644 --- a/interface/web/sites/form/shell_user.tform.php +++ b/interface/web/sites/form/shell_user.tform.php @@ -232,6 +232,12 @@ if($_SESSION["s"]["user"]["typ"] == 'admin') { 'dir' => array ( 'datatype' => 'VARCHAR', 'formtype' => 'TEXT', + 'filters' => array( + 0 => array ( + 'event' => 'SAVE', + 'type' => 'NORMALIZEPATH' + ) + ), 'validators' => array ( 0 => array ( 'type' => 'NOTEMPTY', 'errmsg'=> 'directory_error_empty'), 1 => array ( 'type' => 'REGEX', diff --git a/server/lib/classes/functions.inc.php b/server/lib/classes/functions.inc.php index 5da1f3d713..5296c3012b 100644 --- a/server/lib/classes/functions.inc.php +++ b/server/lib/classes/functions.inc.php @@ -356,6 +356,34 @@ class functions { } } + + /** + * Normalize a path and strip duplicate slashes from it + * + * This will also remove all /../ from the path, reducing the preceding path elements + * + * @param string $path + * @return string + */ + public function normalize_path($path) { + $path = preg_replace('~[/]{2,}~', '/', $path); + $parts = explode('/', $path); + $return_parts = array(); + + foreach($parts as $current_part) { + if($current_part === '..') { + if(!empty($return_parts) && end($return_parts) !== '') { + array_pop($return_parts); + } + } else { + $return_parts[] = $current_part; + } + } + + return implode('/', $return_parts); + } + + /** IDN converter wrapper. * all converter classes should be placed in ISPC_CLASS_PATH.'/idn/' */ @@ -435,10 +463,10 @@ class functions { } return implode("\n", $domains); } - + public function generate_ssh_key($client_id, $username = ''){ global $app; - + // generate the SSH key pair for the client $id_rsa_file = '/tmp/'.uniqid('',true); $id_rsa_pub_file = $id_rsa_file.'.pub'; diff --git a/server/lib/classes/system.inc.php b/server/lib/classes/system.inc.php index 131d10f244..8a8a2f4fb0 100644 --- a/server/lib/classes/system.inc.php +++ b/server/lib/classes/system.inc.php @@ -2300,6 +2300,36 @@ class system{ return true; } + public function is_allowed_path($path) { + global $app; + + $path = $app->functions->normalize_path($path); + if(file_exists($path)) { + $path = realpath($path); + } + + $blacklisted_paths_regex = array( + '@^/$@', + '@^/proc(/.*)?$@', + '@^/sys(/.*)?$@', + '@^/etc(/.*)$@', + '@^/dev(/.*)$@', + '@^/tmp(/.*)$@', + '@^/run(/.*)$@', + '@^/boot(/.*)$@', + '@^/root(/.*)$@', + '@^/var(/?|/backups?(/.*)?)?$@', + ); + + foreach($blacklisted_paths_regex as $regex) { + if(preg_match($regex, $path)) { + return false; + } + } + + return true; + } + public function last_exec_out() { return $this->_last_exec_out; } diff --git a/server/plugins-available/shelluser_base_plugin.inc.php b/server/plugins-available/shelluser_base_plugin.inc.php index 71653cf5c2..39ab2298ad 100755 --- a/server/plugins-available/shelluser_base_plugin.inc.php +++ b/server/plugins-available/shelluser_base_plugin.inc.php @@ -96,6 +96,14 @@ class shelluser_base_plugin { return false; } + if(is_file($data['new']['dir']) || is_link($data['new']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOVLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['new']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOVLEVEL_WARN); + return false; + } + if($data['new']['active'] != 'y' || $data['new']['chroot'] == "jailkit") $data['new']['shell'] = '/bin/false'; if($app->system->is_user($data['new']['puser'])) { @@ -207,6 +215,14 @@ class shelluser_base_plugin { return false; } + if(is_file($data['new']['dir']) || is_link($data['new']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOVLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['new']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOVLEVEL_WARN); + return false; + } + if($data['new']['active'] != 'y') $data['new']['shell'] = '/bin/false'; if($app->system->is_user($data['new']['puser'])) { -- GitLab From f45e5fae5ac5672b7d51ea6bb6c096093458928b Mon Sep 17 00:00:00 2001 From: Marius Burkard Date: Tue, 17 Nov 2020 13:51:52 +0100 Subject: [PATCH 2/5] - fixed problems from implementation of vulnerability fix --- server/lib/classes/system.inc.php | 18 ++++++++---- .../shelluser_base_plugin.inc.php | 16 ++++++++--- .../shelluser_jailkit_plugin.inc.php | 28 +++++++++++++++++++ 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/server/lib/classes/system.inc.php b/server/lib/classes/system.inc.php index 8a8a2f4fb0..a26707b0ae 100644 --- a/server/lib/classes/system.inc.php +++ b/server/lib/classes/system.inc.php @@ -2312,12 +2312,12 @@ class system{ '@^/$@', '@^/proc(/.*)?$@', '@^/sys(/.*)?$@', - '@^/etc(/.*)$@', - '@^/dev(/.*)$@', - '@^/tmp(/.*)$@', - '@^/run(/.*)$@', - '@^/boot(/.*)$@', - '@^/root(/.*)$@', + '@^/etc(/.*)?$@', + '@^/dev(/.*)?$@', + '@^/tmp(/.*)?$@', + '@^/run(/.*)?$@', + '@^/boot(/.*)?$@', + '@^/root(/.*)?$@', '@^/var(/?|/backups?(/.*)?)?$@', ); @@ -2380,6 +2380,8 @@ class system{ } public function create_jailkit_user($username, $home_dir, $user_home_dir, $shell = '/bin/bash', $p_user = null, $p_user_home_dir = null) { + global $app; + // Disallow operating on root directory if(realpath($home_dir) == '/') { $app->log("create_jailkit_user: invalid home_dir: $home_dir", LOGLEVEL_WARN); @@ -2409,6 +2411,8 @@ class system{ } public function create_jailkit_chroot($home_dir, $app_sections = array(), $options = array()) { + global $app; + // Disallow operating on root directory if(realpath($home_dir) == '/') { $app->log("create_jailkit_chroot: invalid home_dir: $home_dir", LOGLEVEL_WARN); @@ -2480,6 +2484,8 @@ class system{ } public function create_jailkit_programs($home_dir, $programs = array(), $options = array()) { + global $app; + // Disallow operating on root directory if(realpath($home_dir) == '/') { $app->log("create_jailkit_programs: invalid home_dir: $home_dir", LOGLEVEL_WARN); diff --git a/server/plugins-available/shelluser_base_plugin.inc.php b/server/plugins-available/shelluser_base_plugin.inc.php index 39ab2298ad..f9a316d90e 100755 --- a/server/plugins-available/shelluser_base_plugin.inc.php +++ b/server/plugins-available/shelluser_base_plugin.inc.php @@ -97,10 +97,10 @@ class shelluser_base_plugin { } if(is_file($data['new']['dir']) || is_link($data['new']['dir'])) { - $app->log('Shell user dir must not be existing file or symlink.', LOVLEVEL_WARN); + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); return false; } elseif(!$app->system->is_allowed_path($data['new']['dir'])) { - $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOVLEVEL_WARN); + $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOGLEVEL_WARN); return false; } @@ -216,10 +216,10 @@ class shelluser_base_plugin { } if(is_file($data['new']['dir']) || is_link($data['new']['dir'])) { - $app->log('Shell user dir must not be existing file or symlink.', LOVLEVEL_WARN); + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); return false; } elseif(!$app->system->is_allowed_path($data['new']['dir'])) { - $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOVLEVEL_WARN); + $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOGLEVEL_WARN); return false; } @@ -320,6 +320,14 @@ class shelluser_base_plugin { return false; } + if(is_file($data['old']['dir']) || is_link($data['old']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['old']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['old']['dir'], LOGLEVEL_WARN); + return false; + } + if($app->system->is_user($data['old']['username'])) { // Get the UID of the user $userid = intval($app->system->getuid($data['old']['username'])); diff --git a/server/plugins-available/shelluser_jailkit_plugin.inc.php b/server/plugins-available/shelluser_jailkit_plugin.inc.php index 3f8d94d2a7..dbc3d8041b 100755 --- a/server/plugins-available/shelluser_jailkit_plugin.inc.php +++ b/server/plugins-available/shelluser_jailkit_plugin.inc.php @@ -89,6 +89,15 @@ class shelluser_jailkit_plugin { return false; } + if(is_file($data['new']['dir']) || is_link($data['new']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['new']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOGLEVEL_WARN); + return false; + } + + if($app->system->is_user($data['new']['puser'])) { // Get the UID of the parent user $uid = intval($app->system->getuid($data['new']['puser'])); @@ -170,6 +179,14 @@ class shelluser_jailkit_plugin { return false; } + if(is_file($data['new']['dir']) || is_link($data['new']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['new']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['new']['dir'], LOGLEVEL_WARN); + return false; + } + if($app->system->is_user($data['new']['puser'])) { $web = $app->db->queryOneRecord("SELECT * FROM web_domain WHERE domain_id = ?", $data['new']['parent_domain_id']); @@ -241,6 +258,14 @@ class shelluser_jailkit_plugin { return false; } + if(is_file($data['old']['dir']) || is_link($data['old']['dir'])) { + $app->log('Shell user dir must not be existing file or symlink.', LOGLEVEL_WARN); + return false; + } elseif(!$app->system->is_allowed_path($data['old']['dir'])) { + $app->log('Shell user dir is not an allowed path: ' . $data['old']['dir'], LOGLEVEL_WARN); + return false; + } + if ($data['old']['chroot'] == "jailkit") { $web = $app->db->queryOneRecord("SELECT * FROM web_domain WHERE domain_id = ?", $data['old']['parent_domain_id']); @@ -518,6 +543,9 @@ class shelluser_jailkit_plugin { } //* Get the keys $existing_keys = file($sshkeys, FILE_IGNORE_NEW_LINES); + if(!$existing_keys) { + $existing_keys = array(); + } $new_keys = explode("\n", $sshrsa); $old_keys = explode("\n", $this->data['old']['ssh_rsa']); -- GitLab From 5fe7013ed572ab1f64955478f34a1664e856e591 Mon Sep 17 00:00:00 2001 From: Marius Burkard Date: Sun, 3 Jan 2021 14:52:20 +0100 Subject: [PATCH 3/5] - do not allow raw SQL through array[SQL] in db lib - don't make sql request on invalid arguments in password reset form --- interface/lib/classes/db_mysql.inc.php | 36 +++++++++---------- interface/web/login/password_reset.php | 14 ++++---- .../classes/cron.d/300-quota_notify.inc.php | 6 ++-- server/lib/classes/db_mysql.inc.php | 36 +++++++++---------- 4 files changed, 43 insertions(+), 49 deletions(-) diff --git a/interface/lib/classes/db_mysql.inc.php b/interface/lib/classes/db_mysql.inc.php index feab66cd93..cd9c333b22 100644 --- a/interface/lib/classes/db_mysql.inc.php +++ b/interface/lib/classes/db_mysql.inc.php @@ -171,14 +171,10 @@ class db } elseif(is_null($sValue) || (is_string($sValue) && (strcmp($sValue, '#NULL#') == 0))) { $sTxt = 'NULL'; } elseif(is_array($sValue)) { - if(isset($sValue['SQL'])) { - $sTxt = $sValue['SQL']; - } else { - $sTxt = ''; - foreach($sValue as $sVal) $sTxt .= ',\'' . $this->escape($sVal) . '\''; - $sTxt = '(' . substr($sTxt, 1) . ')'; - if($sTxt == '()') $sTxt = '(0)'; - } + $sTxt = ''; + foreach($sValue as $sVal) $sTxt .= ',\'' . $this->escape($sVal) . '\''; + $sTxt = '(' . substr($sTxt, 1) . ')'; + if($sTxt == '()') $sTxt = '(0)'; } else { $sTxt = '\'' . $this->escape($sValue) . '\''; } @@ -258,7 +254,7 @@ class db private function _query($sQuery = '') { global $app; - + $aArgs = func_get_args(); if ($sQuery == '') { @@ -354,7 +350,7 @@ class db * @return array result row or NULL if none found */ public function queryOneRecord($sQuery = '') { - + $aArgs = func_get_args(); if(!empty($aArgs)) { $sQuery = array_shift($aArgs); @@ -363,7 +359,7 @@ class db } array_unshift($aArgs, $sQuery); } - + $oResult = call_user_func_array([&$this, 'query'], $aArgs); if(!$oResult) return null; @@ -750,7 +746,7 @@ class db foreach($insert_data as $key => $val) { $key_str .= '??,'; $params[] = $key; - + $val_str .= '?,'; $v_params[] = $val; } @@ -764,7 +760,7 @@ class db $this->query("INSERT INTO ?? $insert_data_str", $tablename); $app->log("deprecated use of passing values to datalogInsert() - table " . $tablename, 1); } - + $old_rec = array(); $index_value = $this->insertID(); if(!$index_value && isset($insert_data[$index_field])) { @@ -1112,7 +1108,7 @@ class db * @access public * @return string 'mariadb' or string 'mysql' */ - + public function getDatabaseType() { $tmp = $this->queryOneRecord('SELECT VERSION() as version'); if(stristr($tmp['version'],'mariadb')) { @@ -1140,7 +1136,7 @@ class db return $version[0]; } } - + /** * Get a mysql password hash * @@ -1150,9 +1146,9 @@ class db */ public function getPasswordHash($password) { - + $password_type = 'password'; - + /* Disabled until caching_sha2_password is implemented if($this->getDatabaseType() == 'mysql' && $this->getDatabaseVersion(true) >= 8) { // we are in MySQL 8 mode @@ -1162,16 +1158,16 @@ class db } } */ - + if($password_type == 'caching_sha2_password') { /* - caching_sha2_password hashing needs to be implemented, have not + caching_sha2_password hashing needs to be implemented, have not found valid PHP implementation for the new password hash type. */ } else { $password_hash = '*'.strtoupper(sha1(sha1($password, true))); } - + return $password_hash; } diff --git a/interface/web/login/password_reset.php b/interface/web/login/password_reset.php index 9a2541bba0..2e1d5e6aad 100644 --- a/interface/web/login/password_reset.php +++ b/interface/web/login/password_reset.php @@ -47,7 +47,7 @@ include ISPC_ROOT_PATH.'/web/login/lib/lang/'.$app->functions->check_language($c $app->tpl->setVar($wb); $continue = true; -if(isset($_POST['username']) && $_POST['username'] != '' && $_POST['email'] != '' && $_POST['username'] != 'admin') { +if(isset($_POST['username']) && is_string($_POST['username']) && $_POST['username'] != '' && isset($_POST['email']) && is_string($_POST['email']) && $_POST['email'] != '' && $_POST['username'] != 'admin') { if(!preg_match("/^[\w\.\-\_]{1,64}$/", $_POST['username'])) { $app->tpl->setVar("error", $wb['user_regex_error']); $continue = false; @@ -60,11 +60,13 @@ if(isset($_POST['username']) && $_POST['username'] != '' && $_POST['email'] != ' $username = $_POST['username']; $email = $_POST['email']; - $client = $app->db->queryOneRecord("SELECT client.*, sys_user.lost_password_function, sys_user.lost_password_hash, IF(sys_user.lost_password_reqtime IS NOT NULL AND DATE_SUB(NOW(), INTERVAL 15 MINUTE) < sys_user.lost_password_reqtime, 1, 0) as `lost_password_wait` FROM client,sys_user WHERE client.username = ? AND client.email = ? AND client.client_id = sys_user.client_id", $username, $email); + if($continue) { + $client = $app->db->queryOneRecord("SELECT client.*, sys_user.lost_password_function, sys_user.lost_password_hash, IF(sys_user.lost_password_reqtime IS NOT NULL AND DATE_SUB(NOW(), INTERVAL 15 MINUTE) < sys_user.lost_password_reqtime, 1, 0) as `lost_password_wait` FROM client,sys_user WHERE client.username = ? AND client.email = ? AND client.client_id = sys_user.client_id", $username, $email); + } - if($client['lost_password_function'] == 0) { + if($client && $client['lost_password_function'] == 0) { $app->tpl->setVar("error", $wb['lost_password_function_disabled_txt']); - } elseif($client['lost_password_wait'] == 1) { + } elseif($client && $client['lost_password_wait'] == 1) { $app->tpl->setVar("error", $wb['lost_password_function_wait_txt']); } elseif ($continue) { if($client['client_id'] > 0) { @@ -111,7 +113,7 @@ if(isset($_POST['username']) && $_POST['username'] != '' && $_POST['email'] != ' $app->tpl->setVar("error", $wb['user_regex_error']); $continue = false; } - + $username = $_GET['username']; $hash = $_GET['hash']; @@ -127,7 +129,7 @@ if(isset($_POST['username']) && $_POST['username'] != '' && $_POST['email'] != ' if($client['client_id'] > 0) { $server_config_array = $app->getconf->get_global_config(); $min_password_length = $app->auth->get_min_password_length(); - + $new_password = $app->auth->get_random_password($min_password_length, true); $new_password_encrypted = $app->auth->crypt_password($new_password); diff --git a/server/lib/classes/cron.d/300-quota_notify.inc.php b/server/lib/classes/cron.d/300-quota_notify.inc.php index bd6a410309..5e1bb92276 100644 --- a/server/lib/classes/cron.d/300-quota_notify.inc.php +++ b/server/lib/classes/cron.d/300-quota_notify.inc.php @@ -250,7 +250,7 @@ class cronjob_quota_notify extends cronjob { //* Send quota notifications if(($web_config['overquota_notify_admin'] == 'y' || $web_config['overquota_notify_client'] == 'y') && $send_notification == true) { - $app->dbmaster->datalogUpdate('web_domain', array("last_quota_notification" => array("SQL" => "CURDATE()")), 'domain_id', $rec['domain_id']); + $app->dbmaster->datalogUpdate('web_domain', array("last_quota_notification" => date('Y-m-d')), 'domain_id', $rec['domain_id']); $placeholders = array('{domain}' => $rec['domain'], '{admin_mail}' => ($global_config['admin_mail'] != ''? $global_config['admin_mail'] : 'root'), @@ -379,7 +379,7 @@ class cronjob_quota_notify extends cronjob { elseif($mail_config['overquota_notify_freq'] > 0 && $rec['notified_before'] >= $mail_config['overquota_notify_freq']) $send_notification = true; if(($mail_config['overquota_notify_admin'] == 'y' || $mail_config['overquota_notify_client'] == 'y') && $send_notification == true) { - $app->dbmaster->datalogUpdate('mail_user', array("last_quota_notification" => array("SQL" => "CURDATE()")), 'mailuser_id', $rec['mailuser_id']); + $app->dbmaster->datalogUpdate('mail_user', array("last_quota_notification" => date('Y-m-d')), 'mailuser_id', $rec['mailuser_id']); $placeholders = array('{email}' => $rec['email'], '{admin_mail}' => ($global_config['admin_mail'] != ''? $global_config['admin_mail'] : 'root'), @@ -466,7 +466,7 @@ class cronjob_quota_notify extends cronjob { //* Send quota notifications if(($web_config['overquota_db_notify_admin'] == 'y' || $web_config['overquota_db_notify_client'] == 'y') && $send_notification == true) { - $app->dbmaster->datalogUpdate('web_database', array("last_quota_notification" => array("SQL" => "CURDATE()")), 'database_id', $rec['database_id']); + $app->dbmaster->datalogUpdate('web_database', array("last_quota_notification" => date('Y-m-d')), 'database_id', $rec['database_id']); $placeholders = array( '{database_name}' => $rec['database_name'], '{admin_mail}' => ($global_config['admin_mail'] != ''? $global_config['admin_mail'] : 'root'), diff --git a/server/lib/classes/db_mysql.inc.php b/server/lib/classes/db_mysql.inc.php index df38086ebe..9b9d43b442 100644 --- a/server/lib/classes/db_mysql.inc.php +++ b/server/lib/classes/db_mysql.inc.php @@ -171,14 +171,10 @@ class db } elseif(is_null($sValue) || (is_string($sValue) && (strcmp($sValue, '#NULL#') == 0))) { $sTxt = 'NULL'; } elseif(is_array($sValue)) { - if(isset($sValue['SQL'])) { - $sTxt = $sValue['SQL']; - } else { - $sTxt = ''; - foreach($sValue as $sVal) $sTxt .= ',\'' . $this->escape($sVal) . '\''; - $sTxt = '(' . substr($sTxt, 1) . ')'; - if($sTxt == '()') $sTxt = '(0)'; - } + $sTxt = ''; + foreach($sValue as $sVal) $sTxt .= ',\'' . $this->escape($sVal) . '\''; + $sTxt = '(' . substr($sTxt, 1) . ')'; + if($sTxt == '()') $sTxt = '(0)'; } else { $sTxt = '\'' . $this->escape($sValue) . '\''; } @@ -258,7 +254,7 @@ class db private function _query($sQuery = '') { global $app; - + $aArgs = func_get_args(); if ($sQuery == '') { @@ -354,7 +350,7 @@ class db * @return array result row or NULL if none found */ public function queryOneRecord($sQuery = '') { - + $aArgs = func_get_args(); if(!empty($aArgs)) { $sQuery = array_shift($aArgs); @@ -363,7 +359,7 @@ class db } array_unshift($aArgs, $sQuery); } - + $oResult = call_user_func_array([&$this, 'query'], $aArgs); if(!$oResult) return null; @@ -750,7 +746,7 @@ class db foreach($insert_data as $key => $val) { $key_str .= '??,'; $params[] = $key; - + $val_str .= '?,'; $v_params[] = $val; } @@ -764,7 +760,7 @@ class db $this->query("INSERT INTO ?? $insert_data_str", $tablename); $app->log("deprecated use of passing values to datalogInsert() - table " . $tablename, 1); } - + $old_rec = array(); $index_value = $this->insertID(); if(!$index_value && isset($insert_data[$index_field])) { @@ -1140,7 +1136,7 @@ class db return $version[0]; } } - + /** * Get a mysql password hash * @@ -1148,11 +1144,11 @@ class db * @param string cleartext password * @return string Password hash */ - + public function getPasswordHash($password) { - + $password_type = 'password'; - + /* Disabled until caching_sha2_password is implemented if($this->getDatabaseType() == 'mysql' && $this->getDatabaseVersion(true) >= 8) { // we are in MySQL 8 mode @@ -1162,16 +1158,16 @@ class db } } */ - + if($password_type == 'caching_sha2_password') { /* - caching_sha2_password hashing needs to be implemented, have not + caching_sha2_password hashing needs to be implemented, have not found valid PHP implementation for the new password hash type. */ } else { $password_hash = '*'.strtoupper(sha1(sha1($password, true))); } - + return $password_hash; } -- GitLab From 5b98587f5f3842b1ecaab60af87e88f654a26ca7 Mon Sep 17 00:00:00 2001 From: Marius Burkard Date: Sun, 3 Jan 2021 19:30:39 +0100 Subject: [PATCH 4/5] - do not use md5 on sys_user password - update user password with new hashing algo on login - delete initial password from ispconfig sql file --- install/dist/lib/fedora.lib.php | 4 +-- install/dist/lib/gentoo.lib.php | 4 +-- install/dist/lib/opensuse.lib.php | 4 +-- install/lib/installer_base.lib.php | 32 ++++++++++++++++++-- install/sql/ispconfig3.sql | 2 +- interface/web/login/index.php | 48 ++++++++++++++++-------------- 6 files changed, 63 insertions(+), 31 deletions(-) diff --git a/install/dist/lib/fedora.lib.php b/install/dist/lib/fedora.lib.php index 506659f6e7..9e3f07d102 100644 --- a/install/dist/lib/fedora.lib.php +++ b/install/dist/lib/fedora.lib.php @@ -1227,8 +1227,8 @@ class installer_dist extends installer_base { caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); if ($this->install_ispconfig_interface == true && isset($conf['interface_password']) && $conf['interface_password']!='admin') { - $sql = "UPDATE sys_user SET passwort = md5(?) WHERE username = 'admin';"; - $this->db->query($sql, $conf['interface_password']); + $sql = "UPDATE sys_user SET passwort = ? WHERE username = 'admin';"; + $this->db->query($sql, $this->crypt_password($conf['interface_password'])); } if($conf['apache']['installed'] == true && $this->install_ispconfig_interface == true){ diff --git a/install/dist/lib/gentoo.lib.php b/install/dist/lib/gentoo.lib.php index acd4dbcf61..a7d62cda0b 100644 --- a/install/dist/lib/gentoo.lib.php +++ b/install/dist/lib/gentoo.lib.php @@ -1115,8 +1115,8 @@ class installer extends installer_base caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); if ($this->install_ispconfig_interface == true && isset($conf['interface_password']) && $conf['interface_password']!='admin') { - $sql = "UPDATE sys_user SET passwort = md5(?) WHERE username = 'admin';"; - $this->db->query($sql, $conf['interface_password']); + $sql = "UPDATE sys_user SET passwort = ? WHERE username = 'admin';"; + $this->db->query($sql, $this->crypt_password($conf['interface_password'])); } if($conf['apache']['installed'] == true && $this->install_ispconfig_interface == true){ diff --git a/install/dist/lib/opensuse.lib.php b/install/dist/lib/opensuse.lib.php index cb145ea7df..8a4152d9b5 100644 --- a/install/dist/lib/opensuse.lib.php +++ b/install/dist/lib/opensuse.lib.php @@ -1215,8 +1215,8 @@ class installer_dist extends installer_base { caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); if ($this->install_ispconfig_interface == true && isset($conf['interface_password']) && $conf['interface_password']!='admin') { - $sql = "UPDATE sys_user SET passwort = md5(?) WHERE username = 'admin';"; - $this->db->query($sql, $conf['interface_password']); + $sql = "UPDATE sys_user SET passwort = ? WHERE username = 'admin';"; + $this->db->query($sql, $this->crypt_password($conf['interface_password'])); } if($conf['apache']['installed'] == true && $this->install_ispconfig_interface == true){ diff --git a/install/lib/installer_base.lib.php b/install/lib/installer_base.lib.php index 7bb75d8c37..338a3dfc7e 100644 --- a/install/lib/installer_base.lib.php +++ b/install/lib/installer_base.lib.php @@ -157,6 +157,34 @@ class installer_base { else return true; } + public function crypt_password($cleartext_password, $charset = 'UTF-8') { + if($charset != 'UTF-8') { + $cleartext_password = mb_convert_encoding($cleartext_password, $charset, 'UTF-8'); + } + + if(defined('CRYPT_SHA512') && CRYPT_SHA512 == 1) { + $salt = '$6$rounds=5000$'; + $salt_length = 16; + } elseif(defined('CRYPT_SHA256') && CRYPT_SHA256 == 1) { + $salt = '$5$rounds=5000$'; + $salt_length = 16; + } else { + $salt = '$1$'; + $salt_length = 12; + } + + if(function_exists('openssl_random_pseudo_bytes')) { + $salt .= substr(bin2hex(openssl_random_pseudo_bytes($salt_length)), 0, $salt_length); + } else { + $base64_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./'; + for($n = 0; $n < $salt_length; $n++) { + $salt .= $base64_alphabet[mt_rand(0, 63)]; + } + } + $salt .= "$"; + return crypt($cleartext_password, $salt); + } + //** Detect installed applications public function find_installed_apps() { global $conf; @@ -3415,8 +3443,8 @@ class installer_base { caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); if ($this->install_ispconfig_interface == true && isset($conf['interface_password']) && $conf['interface_password']!='admin') { - $sql = "UPDATE sys_user SET passwort = md5(?) WHERE username = 'admin';"; - $this->db->query($sql, $conf['interface_password']); + $sql = "UPDATE sys_user SET passwort = ? WHERE username = 'admin';"; + $this->db->query($sql, $this->crypt_password($conf['interface_password'])); } if($conf['apache']['installed'] == true && $this->install_ispconfig_interface == true){ diff --git a/install/sql/ispconfig3.sql b/install/sql/ispconfig3.sql index 8fb5cdfb74..0f10d59ac6 100644 --- a/install/sql/ispconfig3.sql +++ b/install/sql/ispconfig3.sql @@ -2580,7 +2580,7 @@ INSERT INTO `sys_theme` (`var_id`, `tpl_name`, `username`, `logo_url`) VALUES (N -- Dumping data for table `sys_user` -- -INSERT INTO `sys_user` (`userid`, `sys_userid`, `sys_groupid`, `sys_perm_user`, `sys_perm_group`, `sys_perm_other`, `username`, `passwort`, `modules`, `startmodule`, `app_theme`, `typ`, `active`, `language`, `groups`, `default_group`, `client_id`) VALUES (1, 1, 0, 'riud', 'riud', '', 'admin', '21232f297a57a5a743894a0e4a801fc3', 'dashboard,admin,client,mail,monitor,sites,dns,vm,tools,help', 'dashboard', 'default', 'admin', 1, 'en', '1,2', 1, 0); +INSERT INTO `sys_user` (`userid`, `sys_userid`, `sys_groupid`, `sys_perm_user`, `sys_perm_group`, `sys_perm_other`, `username`, `passwort`, `modules`, `startmodule`, `app_theme`, `typ`, `active`, `language`, `groups`, `default_group`, `client_id`) VALUES (1, 1, 0, 'riud', 'riud', '', 'admin', 'xxx', 'dashboard,admin,client,mail,monitor,sites,dns,vm,tools,help', 'dashboard', 'default', 'admin', 1, 'en', '1,2', 1, 0); -- -------------------------------------------------------- diff --git a/interface/web/login/index.php b/interface/web/login/index.php index b5d5abc27b..d820e917c9 100644 --- a/interface/web/login/index.php +++ b/interface/web/login/index.php @@ -83,23 +83,23 @@ if(count($_POST) > 0) { * The actual user is NOT a admin or reseller, but maybe he * has logged in as "normal" user before... */ - + if (isset($_SESSION['s_old'])&& ($_SESSION['s_old']['user']['typ'] == 'admin' || $app->auth->has_clients($_SESSION['s_old']['user']['userid']))){ /* The "old" user is admin or reseller, so everything is ok * if he is reseller, we need to check if he logs in to one of his clients */ if($_SESSION['s_old']['user']['typ'] != 'admin') { - + /* this is the one currently logged in (normal user) */ $old_client_group_id = $app->functions->intval($_SESSION["s"]["user"]["default_group"]); $old_client = $app->db->queryOneRecord("SELECT client.client_id, client.parent_client_id FROM sys_group, client WHERE sys_group.client_id = client.client_id and sys_group.groupid = ?", $old_client_group_id); - + /* this is the reseller, that shall be re-logged in */ $sql = "SELECT * FROM sys_user WHERE USERNAME = ? and PASSWORT = ?"; $tmp = $app->db->queryOneRecord($sql, $username, $password); $client_group_id = $app->functions->intval($tmp['default_group']); $tmp_client = $app->db->queryOneRecord("SELECT client.client_id FROM sys_group, client WHERE sys_group.client_id = client.client_id and sys_group.groupid = ?", $client_group_id); - + if(!$tmp_client || $old_client["parent_client_id"] != $tmp_client["client_id"] || $tmp["default_group"] != $_SESSION["s_old"]["user"]["default_group"] ) { die("You don't have the right to 'login as' this user!"); } @@ -115,12 +115,12 @@ if(count($_POST) > 0) { /* a reseller wants to 'login as', we need to check if he is allowed to */ $res_client_group_id = $app->functions->intval($_SESSION["s"]["user"]["default_group"]); $res_client = $app->db->queryOneRecord("SELECT client.client_id FROM sys_group, client WHERE sys_group.client_id = client.client_id and sys_group.groupid = ?", $res_client_group_id); - + /* this is the user the reseller wants to 'login as' */ $sql = "SELECT * FROM sys_user WHERE USERNAME = ? and PASSWORT = ?"; $tmp = $app->db->queryOneRecord($sql, $username, $password); $tmp_client = $app->db->queryOneRecord("SELECT client.client_id, client.parent_client_id FROM sys_group, client WHERE sys_group.client_id = client.client_id and sys_group.groupid = ?", $tmp["default_group"]); - + if(!$tmp || $tmp_client["parent_client_id"] != $res_client["client_id"]) { die("You don't have the right to login as this user!"); } @@ -129,16 +129,16 @@ if(count($_POST) > 0) { unset($tmp_client); } $loginAs = true; - + } else { /* normal login */ $loginAs = false; } - + //* Check if there are already wrong logins $sql = "SELECT * FROM `attempts_login` WHERE `ip`= ? AND `login_time` > (NOW() - INTERVAL 1 MINUTE) LIMIT 1"; $alreadyfailed = $app->db->queryOneRecord($sql, $ip); - + //* too many failedlogins if($alreadyfailed['times'] > 5) { $error = $app->lng('error_user_too_many_logins'); @@ -148,7 +148,7 @@ if(count($_POST) > 0) { $sql = "SELECT * FROM sys_user WHERE USERNAME = ? and PASSWORT = ?"; $user = $app->db->queryOneRecord($sql, $username, $password); } else { - + if(stristr($username, '@')) { //* mailuser login $sql = "SELECT * FROM mail_user WHERE login = ? or email = ?"; @@ -160,7 +160,7 @@ if(count($_POST) > 0) { if(crypt(stripslashes($password), $saved_password) == $saved_password) { //* Get the sys_user language of the client of the mailuser $sys_user_lang = $app->db->queryOneRecord("SELECT language FROM sys_user WHERE default_group = ?", $mailuser['sys_groupid'] ); - + //* we build a fake user here which has access to the mailuser module only and userid 0 $user = array(); $user['userid'] = 0; @@ -196,6 +196,10 @@ if(count($_POST) > 0) { //* The password is md5 encrypted if(md5($password) != $saved_password) { $user = false; + } else { + // update password with secure algo + $sql = 'UPDATE `sys_user` SET `passwort` = ? WHERE `username` = ?'; + $app->db->query($sql, $app->auth->crypt_password($password), $username); } } } else { @@ -203,19 +207,19 @@ if(count($_POST) > 0) { } } } - + if($user) { if($user['active'] == 1) { // Maintenance mode - allow logins only when maintenance mode is off or if the user is admin if(!$app->is_under_maintenance() || $user['typ'] == 'admin'){ - + // User login right, so attempts can be deleted $sql = "DELETE FROM `attempts_login` WHERE `ip`=?"; $app->db->query($sql, $ip); $user = $app->db->toLower($user); - + if ($loginAs) $oldSession = $_SESSION['s']; - + // Session regenerate causes login problems on some systems, see Issue #3827 // Set session_regenerate_id to no in security settings, it you encounter // this problem. @@ -231,7 +235,7 @@ if(count($_POST) > 0) { $_SESSION['s']['language'] = $app->functions->check_language($user['language']); $_SESSION["s"]['theme'] = $_SESSION['s']['user']['theme']; if ($loginAs) $_SESSION['s']['plugin_cache'] = $_SESSION['s_old']['plugin_cache']; - + if(is_file(ISPC_WEB_PATH . '/' . $_SESSION['s']['user']['startmodule'].'/lib/module.conf.php')) { include_once $app->functions->check_include_path(ISPC_WEB_PATH . '/' . $_SESSION['s']['user']['startmodule'].'/lib/module.conf.php'); $menu_dir = ISPC_WEB_PATH.'/' . $_SESSION['s']['user']['startmodule'] . '/lib/menu.d'; @@ -257,20 +261,20 @@ if(count($_POST) > 0) { $_SESSION['show_error_msg'] = $app->lng('theme_not_compatible'); } } - + $app->plugin->raiseEvent('login', $username); - + //* Save successfull login message to var - $authlog = 'Successful login for user \''. $username .'\' from '. $_SERVER['REMOTE_ADDR'] .' at '. date('Y-m-d H:i:s') . ' with session ID ' .session_id(); + $authlog = 'Successful login for user \''. $username .'\' from '. $_SERVER['REMOTE_ADDR'] .' at '. date('Y-m-d H:i:s') . ' with session ID ' .session_id(); $authlog_handle = fopen($conf['ispconfig_log_dir'].'/auth.log', 'a'); fwrite($authlog_handle, $authlog ."\n"); fclose($authlog_handle); - + /* * We need LOGIN_REDIRECT instead of HEADER_REDIRECT to load the * new theme, if the logged-in user has another */ - + if ($loginAs){ echo 'LOGIN_REDIRECT:'.$_SESSION['s']['module']['startpage']; exit; @@ -327,7 +331,7 @@ if($security_config['password_reset_allowed'] == 'yes') { } else { $app->tpl->setVar('pw_lost_show', 0); } - + $app->tpl->setVar('error', $error); $app->tpl->setVar('error_txt', $app->lng('error_txt')); $app->tpl->setVar('login_txt', $app->lng('login_txt')); -- GitLab From d6933c0e36ed09826d639658740e58971a4c84c6 Mon Sep 17 00:00:00 2001 From: Marius Burkard Date: Mon, 4 Jan 2021 14:44:48 +0100 Subject: [PATCH 5/5] - adjust ispconfig log file permissions --- install/dist/lib/fedora.lib.php | 1 + install/dist/lib/gentoo.lib.php | 1 + install/dist/lib/opensuse.lib.php | 1 + install/lib/installer_base.lib.php | 1 + 4 files changed, 4 insertions(+) diff --git a/install/dist/lib/fedora.lib.php b/install/dist/lib/fedora.lib.php index 9e3f07d102..9620bf3561 100644 --- a/install/dist/lib/fedora.lib.php +++ b/install/dist/lib/fedora.lib.php @@ -1372,6 +1372,7 @@ class installer_dist extends installer_base { //* Create the ispconfig log directory if(!is_dir($conf['ispconfig_log_dir'])) mkdir($conf['ispconfig_log_dir']); if(!is_file($conf['ispconfig_log_dir'].'/ispconfig.log')) exec('touch '.$conf['ispconfig_log_dir'].'/ispconfig.log'); + chmod($conf['ispconfig_log_dir'].'/ispconfig.log', 0600); if(is_user('getmail')) { exec('mv /usr/local/ispconfig/server/scripts/run-getmail.sh /usr/local/bin/run-getmail.sh'); diff --git a/install/dist/lib/gentoo.lib.php b/install/dist/lib/gentoo.lib.php index a7d62cda0b..f719fbee38 100644 --- a/install/dist/lib/gentoo.lib.php +++ b/install/dist/lib/gentoo.lib.php @@ -1252,6 +1252,7 @@ class installer extends installer_base if (!is_file($conf['ispconfig_log_dir'].'/ispconfig.log')) { touch($conf['ispconfig_log_dir'].'/ispconfig.log'); } + chmod($conf['ispconfig_log_dir'].'/ispconfig.log', 0600); //* Create the ispconfig auth log file and set uid/gid if(!is_file($conf['ispconfig_log_dir'].'/auth.log')) { diff --git a/install/dist/lib/opensuse.lib.php b/install/dist/lib/opensuse.lib.php index 8a4152d9b5..b9e3a1c575 100644 --- a/install/dist/lib/opensuse.lib.php +++ b/install/dist/lib/opensuse.lib.php @@ -1369,6 +1369,7 @@ class installer_dist extends installer_base { //* Create the ispconfig log directory if(!is_dir($conf['ispconfig_log_dir'])) mkdir($conf['ispconfig_log_dir']); if(!is_file($conf['ispconfig_log_dir'].'/ispconfig.log')) exec('touch '.$conf['ispconfig_log_dir'].'/ispconfig.log'); + chmod($conf['ispconfig_log_dir'].'/ispconfig.log', 0600); if(is_user('getmail')) { exec('mv /usr/local/ispconfig/server/scripts/run-getmail.sh /usr/local/bin/run-getmail.sh'); diff --git a/install/lib/installer_base.lib.php b/install/lib/installer_base.lib.php index 338a3dfc7e..cec9c55b42 100644 --- a/install/lib/installer_base.lib.php +++ b/install/lib/installer_base.lib.php @@ -3588,6 +3588,7 @@ class installer_base { if(!is_dir($conf['ispconfig_log_dir'])) mkdir($conf['ispconfig_log_dir'], 0755); touch($conf['ispconfig_log_dir'].'/ispconfig.log'); } + chmod($conf['ispconfig_log_dir'].'/ispconfig.log', 0600); //* Create the ispconfig auth log file and set uid/gid if(!is_file($conf['ispconfig_log_dir'].'/auth.log')) { -- GitLab