core/src/etc/inc/auth.inc
2019-12-09 06:36:17 +01:00

798 lines
24 KiB
PHP

<?php
/*
* Copyright (C) 2014-2016 Deciso B.V.
* Copyright (C) 2010 Ermal Luçi
* Copyright (C) 2007-2008 Scott Ullrich <sullrich@gmail.com>
* Copyright (C) 2005-2006 Bill Marquette <bill.marquette@gmail.com>
* Copyright (C) 2006 Paul Taylor <paultaylor@winn-dixie.com>
* Copyright (C) 2003-2006 Manuel Kasper <mk@neon1.net>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
require_once("interfaces.inc");
require_once("util.inc");
// Will be changed to false if security checks fail
$security_passed = true;
/* If this function doesn't exist, we're being called from Captive Portal or
another internal subsystem which does not include authgui.inc */
if (function_exists("display_error_form") && !isset($config['system']['webgui']['nodnsrebindcheck'])) {
/* DNS ReBinding attack prevention */
$found_host = false;
/* either an IPv6 address with or without an alternate port */
if (strstr($_SERVER['HTTP_HOST'], "]")) {
$http_host_port = explode("]", $_SERVER['HTTP_HOST']);
/* v6 address has more parts, drop the last part */
if (count($http_host_port) > 1) {
array_pop($http_host_port);
$http_host = str_replace(array("[", "]"), "", implode(":", $http_host_port));
} else {
$http_host = str_replace(array("[", "]"), "", implode(":", $http_host_port));
}
} else {
$http_host = explode(":", $_SERVER['HTTP_HOST']);
$http_host = $http_host[0];
}
if (
is_ipaddr($http_host) || $_SERVER['SERVER_ADDR'] == "127.0.0.1" ||
strcasecmp($http_host, "localhost") == 0 or $_SERVER['SERVER_ADDR'] == "::1"
) {
$found_host = true;
}
if (
strcasecmp($http_host, $config['system']['hostname'] . "." . $config['system']['domain']) == 0 ||
strcasecmp($http_host, $config['system']['hostname']) == 0
) {
$found_host = true;
}
if (isset($config['dyndnses']['dyndns']) && !$found_host) {
foreach ($config['dyndnses']['dyndns'] as $dyndns) {
if (strcasecmp($dyndns['host'], $http_host) == 0) {
$found_host = true;
break;
}
}
}
if (isset($config['dnsupdates']['dnsupdate']) && !$found_host) {
foreach ($config['dnsupdates']['dnsupdate'] as $rfc2136) {
if (strcasecmp($rfc2136['host'], $http_host) == 0) {
$found_host = true;
break;
}
}
}
if (!empty($config['system']['webgui']['althostnames']) && !$found_host) {
$althosts = explode(" ", $config['system']['webgui']['althostnames']);
foreach ($althosts as $ah) {
if (strcasecmp($ah, $http_host) == 0 or strcasecmp($ah, $_SERVER['SERVER_ADDR']) == 0) {
$found_host = true;
break;
}
}
}
if ($found_host == false) {
if (!security_checks_disabled()) {
display_error_form("501", sprintf(gettext("A potential %sDNS Rebind attack%s has been detected.%sTry to access the router by IP address instead of by hostname."), '<a href="http://en.wikipedia.org/wiki/DNS_rebinding">', '</a>', '<br />'));
exit;
}
$security_passed = false;
}
}
// If the HTTP_REFERER is something other than ourselves then disallow.
if (function_exists("display_error_form") && !isset($config['system']['webgui']['nohttpreferercheck'])) {
if (isset($_SERVER['HTTP_REFERER'])) {
if (file_exists('/tmp/setupwizard_lastreferrer')) {
if ($_SERVER['HTTP_REFERER'] == file_get_contents('/tmp/setupwizard_lastreferrer')) {
unlink('/tmp/setupwizard_lastreferrer');
header("Refresh: 1; url=index.php");
echo "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
echo "<html><head><title>" . gettext("Redirecting...") . "</title></head><body>" . gettext("Redirecting to the dashboard...") . "</body></html>";
exit;
}
}
$found_host = false;
$referrer_host = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
$referrer_host = str_replace(array("[", "]"), "", $referrer_host);
if ($referrer_host) {
if (
strcasecmp($referrer_host, $config['system']['hostname'] . "." . $config['system']['domain']) == 0 ||
strcasecmp($referrer_host, $config['system']['hostname']) == 0
) {
$found_host = true;
}
if (!empty($config['system']['webgui']['althostnames']) && !$found_host) {
$althosts = explode(" ", $config['system']['webgui']['althostnames']);
foreach ($althosts as $ah) {
if (strcasecmp($referrer_host, $ah) == 0) {
$found_host = true;
break;
}
}
}
if (isset($config['dyndnses']['dyndns']) && !$found_host) {
foreach ($config['dyndnses']['dyndns'] as $dyndns) {
if (strcasecmp($dyndns['host'], $referrer_host) == 0) {
$found_host = true;
break;
}
}
}
if (isset($config['dnsupdates']['dnsupdate']) && !$found_host) {
foreach ($config['dnsupdates']['dnsupdate'] as $rfc2136) {
if (strcasecmp($rfc2136['host'], $referrer_host) == 0) {
$found_host = true;
break;
}
}
}
if (!$found_host) {
$found_host = isAuthLocalIP($referrer_host);
if ($referrer_host == "127.0.0.1" || $referrer_host == "localhost") {
// allow SSH port forwarded connections and links from localhost
$found_host = true;
}
}
}
if ($found_host == false) {
if (!security_checks_disabled()) {
display_error_form('501', sprintf(
gettext('The HTTP_REFERER "%s" does not match the predefined settings. You can disable this check if needed under System: Settings: Administration.'),
html_safe($_SERVER['HTTP_REFERER'])
));
exit;
}
$security_passed = false;
}
} else {
$security_passed = false;
}
}
if (function_exists("display_error_form") && $security_passed) {
/* Security checks passed, so it should be OK to turn them back on */
restore_security_checks();
}
unset($security_passed);
$groupindex = index_groups();
$userindex = index_users();
/**
* check if $http_host is a local configured ip address
*/
function isAuthLocalIP($http_host)
{
global $config;
if (isset($config['virtualip']['vip'])) {
foreach ($config['virtualip']['vip'] as $vip) {
if ($vip['subnet'] == $http_host) {
return true;
}
}
}
$address_in_list = function ($interface_list_ips, $http_host) {
foreach ($interface_list_ips as $ilips => $ifname) {
if (strcasecmp($http_host, $ilips) == 0) {
return true;
}
}
};
// try using cached addresses
$interface_list_ips = get_cached_json_content("/tmp/isAuthLocalIP.cache.json");
if (!empty($interface_list_ips) && $address_in_list($interface_list_ips, $http_host)) {
return true;
}
// fetch addresses and store in cache
$interface_list_ips = get_configured_ip_addresses();
file_put_contents("/tmp/isAuthLocalIP.cache.json", json_encode($interface_list_ips));
return $address_in_list($interface_list_ips, $http_host);
}
function index_groups()
{
global $config, $groupindex;
$groupindex = array();
if (isset($config['system']['group'])) {
$i = 0;
foreach ($config['system']['group'] as $groupent) {
if (isset($groupent['name'])) {
$groupindex[$groupent['name']] = $i;
$i++;
}
}
}
return ($groupindex);
}
function index_users()
{
global $config;
$userindex = array();
if (is_array($config['system']['user'])) {
$i = 0;
foreach ($config['system']['user'] as $userent) {
$userindex[$userent['name']] = $i;
$i++;
}
}
return $userindex;
}
function getUserGroups($username)
{
global $config;
$member_groups = array();
$user = getUserEntry($username);
if ($user !== false) {
$allowed_groups = local_user_get_groups($user);
if (isset($config['system']['group'])) {
foreach ($config['system']['group'] as $group) {
if (in_array($group['name'], $allowed_groups)) {
$member_groups[] = $group['name'];
}
}
}
}
return $member_groups;
}
function &getUserEntry($name)
{
global $config, $userindex;
$false = false;
if (isset($userindex[$name])) {
return $config['system']['user'][$userindex[$name]];
} else {
return $false;
}
}
function &getUserEntryByUID($uid)
{
global $config;
if (is_array($config['system']['user'])) {
foreach ($config['system']['user'] as & $user) {
if ($user['uid'] == $uid) {
return $user;
}
}
}
return false;
}
function &getGroupEntry($name)
{
global $config, $groupindex;
if (isset($groupindex[$name])) {
return $config['system']['group'][$groupindex[$name]];
}
return array();
}
function &getGroupEntryByGID($gid)
{
global $config;
if (isset($config['system']['group'])) {
foreach ($config['system']['group'] as & $group) {
if ($group['gid'] == $gid) {
return $group;
}
}
}
return false;
}
function get_user_privileges(&$user)
{
if (!isset($user['priv']) || !is_array($user['priv'])) {
$privs = array();
} else {
$privs = $user['priv'];
}
$names = local_user_get_groups($user);
foreach ($names as $name) {
$group = getGroupEntry($name);
if (isset($group['priv']) && is_array($group['priv'])) {
$privs = array_merge($privs, $group['priv']);
}
}
return $privs;
}
function userHasPrivilege($userent, $privid = false)
{
if (!$privid || !is_array($userent)) {
return false;
}
$privs = get_user_privileges($userent);
if (!is_array($privs)) {
return false;
}
if (!in_array($privid, $privs)) {
return false;
}
return true;
}
function userIsAdmin($username)
{
$user = getUserEntry($username);
return userHasPrivilege($user, 'page-all');
}
function auth_get_shells($uid = 0)
{
$shells = array('' => '/sbin/nologin');
if ($uid == 0) {
$shells = array('' => '/usr/local/sbin/opnsense-shell');
}
$etc_shells = @file_get_contents('/etc/shells');
if (!empty($etc_shells)) {
$etc_shells = explode("\n", $etc_shells);
foreach ($etc_shells as $shell) {
$shell = trim($shell);
if (
!empty($shell) && strpos($shell, '#') !== 0 &&
strpos($shell, '/usr/local/sbin/opnsense-') !== 0
) {
$shells[$shell] = $shell;
}
}
}
return $shells;
}
function local_sync_accounts()
{
global $config;
/* remove local users to avoid uid conflicts */
$fd = popen('/usr/sbin/pw usershow -a', 'r');
if ($fd) {
while (!feof($fd)) {
$line = explode(':', fgets($fd));
if (count($line) < 3 || !strncmp($line[0], '_', 1) || $line[2] < 2000 || $line[2] > 65000) {
continue;
}
/*
* If a crontab was created to user, pw userdel will be interactive and
* can cause issues. Just remove crontab before run it when necessary
*/
@unlink("/var/cron/tabs/{$line[0]}");
mwexecf('/usr/sbin/pw userdel -n %s', $line[0]);
}
pclose($fd);
}
/* remove local groups to avoid gid conflicts */
$fd = popen('/usr/sbin/pw groupshow -a', 'r');
if ($fd) {
while (!feof($fd)) {
$line = explode(':', fgets($fd));
if (count($line) < 3 || !strncmp($line[0], '_', 1) || $line[2] < 2000 || $line[2] > 65000) {
continue;
}
mwexecf('/usr/sbin/pw groupdel -g %s', $line[2]);
}
pclose($fd);
}
/* make sure the all group exists */
$allgrp = getGroupEntryByGID(1998);
local_group_set($allgrp, true);
/* sync all local users */
if (is_array($config['system']['user'])) {
foreach ($config['system']['user'] as $user) {
local_user_set($user);
}
}
/* sync all local groups */
if (is_array($config['system']['group'])) {
foreach ($config['system']['group'] as $group) {
local_group_set($group);
}
}
}
function local_user_set(&$user, $force_password = false)
{
global $config;
if (empty($user['password'])) {
log_error(sprintf(
gettext('There is something wrong in your config because user %s password is missing!'),
$user['name']
));
return;
}
@mkdir('/home', 0755);
$user_pass = $user['password'];
$user_name = $user['name'];
$user_uid = $user['uid'];
$lock_account = 'lock';
$is_expired = !empty($user['expires']) &&
strtotime('-1 day') > strtotime(date('m/d/Y', strtotime($user['expires'])));
$is_disabled = isset($user['disabled']);
$is_unlocked = !$is_disabled && !$is_expired;
if ($is_unlocked) {
$lock_account = 'unlock';
} elseif ($user_uid == 0) {
/*
* The root account should not be locked as this will have
* side-effects such as cron not working correctly. Set
* password to unreachable "*" instead. Our auth framework
* already checks for disabled elsewhere so we only need to
* prevent root login in the console when integrated
* authentication is off.
*/
$lock_account = 'unlock';
$user_pass = '*';
}
if ($user_uid == 0) {
$user_shell = isset($user['shell']) ? $user['shell'] : '/usr/local/sbin/opnsense-shell';
$user_group = 'wheel';
$user_home = '/root';
} else {
$user_shell = isset($user['shell']) ? $user['shell'] : '/sbin/nologin';
$user_home = "/home/{$user_name}";
$user_group = 'nobody';
}
/* admins access gives wheely rights */
if (userIsAdmin($user['name'])) {
$user_group = 'wheel';
}
/* passwords only when integrated auth is disabled or forced */
if (!$force_password && empty($config['system']['disableintegratedauth'])) {
$user_pass = '*';
}
/* read from pw db */
$fd = popen("/usr/sbin/pw usershow -n {$user_name} 2>&1", "r");
$pwread = fgets($fd);
pclose($fd);
$userattrs = explode(":", trim($pwread));
/* determine add or mod */
if (($userattrs[0] != $user['name']) || (!strncmp($pwread, 'pw:', 3))) {
$user_op = 'useradd -m -k /usr/share/skel -o';
} else {
$user_op = 'usermod';
}
$comment = str_replace(array(':', '!', '@'), ' ', $user['descr']);
/* add or mod pw db */
$cmd = "/usr/sbin/pw {$user_op} -q -u {$user_uid} -n {$user_name}" .
" -g {$user_group} -s {$user_shell} -d {$user_home}" .
" -c " . escapeshellarg($comment) . " -H 0 2>&1";
$fd = popen($cmd, 'w');
fwrite($fd, $user_pass);
pclose($fd);
/* create user directory if required */
@mkdir($user_home, 0700);
@chown($user_home, $user_name);
@chgrp($user_home, $user_group);
/* write out ssh authorized key file */
if ($is_unlocked && isset($user['authorizedkeys'])) {
@mkdir("{$user_home}/.ssh", 0700);
@chown("{$user_home}/.ssh", $user_name);
$keys = base64_decode($user['authorizedkeys']);
$keys = preg_split('/[\n\r]+/', $keys);
$keys[] = '';
$keys = implode($keys, "\n");
@file_put_contents("{$user_home}/.ssh/authorized_keys", $keys);
@chown("{$user_home}/.ssh/authorized_keys", $user_name);
} else {
@unlink("{$user_home}/.ssh/authorized_keys");
}
mwexecf('/usr/sbin/pw %s %s', array($lock_account, $user_name), true);
}
function local_user_del($user)
{
/* remove all memberships */
local_user_set_groups($user);
/* delete from pw db */
mwexecf('/usr/sbin/pw userdel -n %s -r', $user['name']);
/* Delete user from groups needs a call to write_config() */
local_group_del_user($user);
}
function local_user_set_password(&$user, $password = null)
{
$cost = 10;
if ($password == null) {
/* generate a random password */
$bytes = openssl_random_pseudo_bytes(50);
$password = pack('H*', bin2hex($bytes));
}
$hash = password_hash($password, PASSWORD_BCRYPT, [ 'cost' => $cost ]);
if ($hash !== false) {
$user['password'] = $hash;
}
}
function local_user_get_groups($user)
{
global $config;
$groups = array();
if (!isset($config['system']['group'])) {
return $groups;
}
foreach ($config['system']['group'] as $group) {
if (isset($group['member'])) {
if (in_array($user['uid'], $group['member'])) {
$groups[] = $group['name'];
}
}
}
sort($groups);
return $groups;
}
function local_user_set_groups($user, $new_groups = null)
{
global $config, $groupindex;
if (!isset($config['system']['group'])) {
return;
}
$cur_groups = local_user_get_groups($user);
$mod_groups = array();
if (!is_array($new_groups)) {
$new_groups = array();
}
if (!is_array($cur_groups)) {
$cur_groups = array();
}
/* determine which memberships to add */
foreach ($new_groups as $groupname) {
if (in_array($groupname, $cur_groups) || !isset($groupindex[$groupname])) {
// continue if group is already in current list or the groupname is invalid
continue;
}
$group = &config_read_array('system', 'group', $groupindex[$groupname]);
$group['member'][] = $user['uid'];
$mod_groups[] = $group;
}
/* determine which memberships to remove */
foreach ($cur_groups as $groupname) {
if (in_array($groupname, $new_groups)) {
continue;
}
if (!isset($config['system']['group'][$groupindex[$groupname]])) {
continue;
}
$group = &config_read_array('system', 'group', $groupindex[$groupname]);
if (is_array($group['member'])) {
$index = array_search($user['uid'], $group['member']);
array_splice($group['member'], $index, 1);
$mod_groups[] = $group;
}
}
/* sync all modified groups */
foreach ($mod_groups as $group) {
local_group_set($group);
}
}
function local_group_del_user($user)
{
global $config;
if (!isset($config['system']['group'])) {
return;
}
foreach ($config['system']['group'] as $group) {
if (isset($group['member'])) {
foreach ($group['member'] as $idx => $uid) {
if ($user['uid'] == $uid) {
unset($config['system']['group']['member'][$idx]);
}
}
}
}
}
function local_group_set($group, $reset = false)
{
if (!isset($group['name']) || !isset($group['gid'])) {
return;
}
$group_name = $group['name'];
$group_gid = $group['gid'];
$group_members = '';
if (!$reset && !empty($group['member']) && count($group['member']) > 0) {
$group_members = implode(',', $group['member']);
}
$ret = mwexecf('/usr/sbin/pw groupshow %s', $group_name, true);
if ($ret) {
$group_op = 'groupadd';
} else {
$group_op = 'groupmod';
}
mwexecf('/usr/sbin/pw %s %s -g %s -M %s', array($group_op, $group_name, $group_gid, $group_members));
}
function local_group_del($group)
{
/* delete from group db */
mwexecf('/usr/sbin/pw groupdel %s', $group['name']);
}
/**
* @param $name string name of the authentication system configured on the authentication server page or 'Local Database' for local authentication
* @return array|bool false if the authentication server was not found, otherwise the configuration of the authentication server
*/
function auth_get_authserver($name)
{
global $config;
if ($name == "Local Database") {
return array(
"name" => gettext("Local Database"),
"type" => "local",
"host" => $config['system']['hostname']
);
}
if (isset($config['system']['authserver']) && is_array($config['system']['authserver'])) {
foreach ($config['system']['authserver'] as $authcfg) {
if ($authcfg['name'] == $name) {
if ($authcfg['type'] == 'ldap' || $authcfg['type'] == 'ldap-totp') {
// make sure a user and password entry exists and are null for anonymous usage
if (empty($authcfg['ldap_binddn'])) {
$authcfg['ldap_binddn'] = null;
}
if (empty($authcfg['ldap_bindpw'])) {
$authcfg['ldap_bindpw'] = null;
}
}
return $authcfg;
}
}
}
return false;
}
function auth_get_authserver_list()
{
global $config;
$list = array();
if (isset($config['system']['authserver']) && is_array($config['system']['authserver'])) {
foreach ($config['system']['authserver'] as $authcfg) {
/* Add support for disabled entries? */
$list[$authcfg['name']] = $authcfg;
}
}
$list["Local Database"] = array( "name" => gettext("Local Database"), "type" => "local", "host" => $config['system']['hostname']);
return $list;
}
/**
* return authenticator object
* @param array|null $authcfg configuration
* @return Auth\Base type object
*/
function get_authenticator($authcfg = null)
{
if (empty($authcfg)) {
$authName = 'Local Database';
} else {
$authName = $authcfg['name'];
if ($authcfg['type'] == 'local') {
// avoid gettext type issues on Local Database, authenticator should always be named "Local Database"
$authName = 'Local Database';
}
}
$authFactory = new OPNsense\Auth\AuthenticationFactory();
return $authFactory->get($authName);
}
function authenticate_user($username, $password, $authcfg = null)
{
$authenticator = get_authenticator($authcfg);
if ($authenticator != null) {
return $authenticator->authenticate($username, $password);
} else {
log_error('Unable to retrieve authenticator for ' . $username);
return false;
}
}