blob: 93f92e3fe9f5965642c807743d37e209710a5f79 [file] [log] [blame]
/*
* OpenVPN -- An application to securely tunnel IP networks
* over a single TCP/UDP port, with support for SSL/TLS-based
* session authentication and key exchange,
* packet encryption, packet authentication, and
* packet compression.
*
* Copyright (C) 2016-2021 Selva Nair <selva.nair@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "validate.h"
#include <lmaccess.h>
#include <shlwapi.h>
#include <lm.h>
static const WCHAR *white_list[] =
{
L"auth-retry",
L"config",
L"log",
L"log-append",
L"management",
L"management-forget-disconnect",
L"management-hold",
L"management-query-passwords",
L"management-query-proxy",
L"management-signal",
L"management-up-down",
L"mute",
L"setenv",
L"service",
L"verb",
L"pull-filter",
L"script-security",
NULL /* last value */
};
static BOOL IsUserInGroup(PSID sid, const PTOKEN_GROUPS groups, const WCHAR *group_name);
static PTOKEN_GROUPS GetTokenGroups(const HANDLE token);
/*
* Check workdir\fname is inside config_dir
* The logic here is simple: we may reject some valid paths if ..\ is in any of the strings
*/
static BOOL
CheckConfigPath(const WCHAR *workdir, const WCHAR *fname, const settings_t *s)
{
WCHAR tmp[MAX_PATH];
const WCHAR *config_file = NULL;
const WCHAR *config_dir = NULL;
#ifndef UNICODE
WCHAR widepath[MAX_PATH];
#endif
/* convert fname to full path */
if (PathIsRelativeW(fname) )
{
openvpn_swprintf(tmp, _countof(tmp), L"%s\\%s", workdir, fname);
config_file = tmp;
}
else
{
config_file = fname;
}
#ifdef UNICODE
config_dir = s->config_dir;
#else
if (MultiByteToWideChar(CP_UTF8, 0, s->config_dir, -1, widepath, MAX_PATH) == 0)
{
MsgToEventLog(M_SYSERR, TEXT("Failed to convert config_dir name to WideChar"));
return FALSE;
}
config_dir = widepath;
#endif
if (wcsncmp(config_dir, config_file, wcslen(config_dir)) == 0
&& wcsstr(config_file + wcslen(config_dir), L"..") == NULL)
{
return TRUE;
}
return FALSE;
}
/*
* A simple linear search meant for a small wchar_t *array.
* Returns index to the item if found, -1 otherwise.
*/
static int
OptionLookup(const WCHAR *name, const WCHAR *white_list[])
{
int i;
for (i = 0; white_list[i]; i++)
{
if (wcscmp(white_list[i], name) == 0)
{
return i;
}
}
return -1;
}
/*
* The Administrators group may be localized or renamed by admins.
* Get the local name of the group using the SID.
*/
static BOOL
GetBuiltinAdminGroupName(WCHAR *name, DWORD nlen)
{
BOOL b = FALSE;
PSID admin_sid = NULL;
DWORD sid_size = SECURITY_MAX_SID_SIZE;
SID_NAME_USE snu;
WCHAR domain[MAX_NAME];
DWORD dlen = _countof(domain);
admin_sid = malloc(sid_size);
if (!admin_sid)
{
return FALSE;
}
b = CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, admin_sid, &sid_size);
if (b)
{
b = LookupAccountSidW(NULL, admin_sid, name, &nlen, domain, &dlen, &snu);
}
free(admin_sid);
return b;
}
/*
* Check whether user is a member of Administrators group or
* the group specified in ovpn_admin_group
*/
BOOL
IsAuthorizedUser(PSID sid, const HANDLE token, const WCHAR *ovpn_admin_group)
{
const WCHAR *admin_group[2];
WCHAR username[MAX_NAME];
WCHAR domain[MAX_NAME];
WCHAR sysadmin_group[MAX_NAME];
DWORD len = MAX_NAME;
BOOL ret = FALSE;
SID_NAME_USE sid_type;
/* Get username */
if (!LookupAccountSidW(NULL, sid, username, &len, domain, &len, &sid_type))
{
MsgToEventLog(M_SYSERR, TEXT("LookupAccountSid"));
/* not fatal as this is now used only for logging */
username[0] = '\0';
domain[0] = '\0';
}
if (GetBuiltinAdminGroupName(sysadmin_group, _countof(sysadmin_group)))
{
admin_group[0] = sysadmin_group;
}
else
{
MsgToEventLog(M_SYSERR, TEXT("Failed to get the name of Administrators group. Using the default."));
/* use the default value */
admin_group[0] = SYSTEM_ADMIN_GROUP;
}
admin_group[1] = ovpn_admin_group;
PTOKEN_GROUPS token_groups = GetTokenGroups(token);
for (int i = 0; i < 2; ++i)
{
ret = IsUserInGroup(sid, token_groups, admin_group[i]);
if (ret)
{
MsgToEventLog(M_INFO, TEXT("Authorizing user '%s@%s' by virtue of membership in group '%s'"),
username, domain, admin_group[i]);
goto out;
}
}
out:
free(token_groups);
return ret;
}
/**
* Get a list of groups in token.
* Returns a pointer to TOKEN_GROUPS struct or NULL on error.
* The caller should free the returned pointer.
*/
static PTOKEN_GROUPS
GetTokenGroups(const HANDLE token)
{
PTOKEN_GROUPS groups = NULL;
DWORD buf_size = 0;
if (!GetTokenInformation(token, TokenGroups, groups, buf_size, &buf_size)
&& GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
groups = malloc(buf_size);
}
if (!groups)
{
MsgToEventLog(M_SYSERR, L"GetTokenGroups");
}
else if (!GetTokenInformation(token, TokenGroups, groups, buf_size, &buf_size))
{
MsgToEventLog(M_SYSERR, L"GetTokenInformation");
free(groups);
}
return groups;
}
/*
* Find SID from name
*
* On input sid buffer should have space for at least sid_size bytes.
* Returns true on success, false on failure.
* Suggest: in caller allocate sid to hold SECURITY_MAX_SID_SIZE bytes
*/
static BOOL
LookupSID(const WCHAR *name, PSID sid, DWORD sid_size)
{
SID_NAME_USE su;
WCHAR domain[MAX_NAME];
DWORD dlen = _countof(domain);
if (!LookupAccountName(NULL, name, sid, &sid_size, domain, &dlen, &su))
{
return FALSE; /* not fatal as the group may not exist */
}
return TRUE;
}
/**
* User is in group if the token groups contain the SID of the group
* of if the user is a direct member of the group. The latter check
* catches dynamic changes in group membership in the local user
* database not reflected in the token.
* If token_groups or sid is NULL the corresponding check is skipped.
*
* Using sid and list of groups in token avoids reference to domains so that
* this could be completed without access to a Domain Controller.
*
* Returns true if the user is in the group, false otherwise.
*/
static BOOL
IsUserInGroup(PSID sid, const PTOKEN_GROUPS token_groups, const WCHAR *group_name)
{
BOOL ret = FALSE;
DWORD_PTR resume = 0;
DWORD err;
BYTE grp_sid[SECURITY_MAX_SID_SIZE];
int nloop = 0; /* a counter used to not get stuck in the do .. while() */
/* first check in the token groups */
if (token_groups && LookupSID(group_name, (PSID) grp_sid, _countof(grp_sid)))
{
for (DWORD i = 0; i < token_groups->GroupCount; ++i)
{
if (EqualSid((PSID) grp_sid, token_groups->Groups[i].Sid))
{
return TRUE;
}
}
}
/* check user's SID is a member of the group */
if (!sid)
{
return FALSE;
}
do
{
DWORD nread, nmax;
LOCALGROUP_MEMBERS_INFO_0 *members = NULL;
err = NetLocalGroupGetMembers(NULL, group_name, 0, (LPBYTE *) &members,
MAX_PREFERRED_LENGTH, &nread, &nmax, &resume);
if ((err != NERR_Success && err != ERROR_MORE_DATA))
{
break;
}
/* If a match is already found, ret == TRUE and the loop is skipped */
for (DWORD i = 0; i < nread && !ret; ++i)
{
ret = EqualSid(members[i].lgrmi0_sid, sid);
}
NetApiBufferFree(members);
/* MSDN says the lookup should always iterate until err != ERROR_MORE_DATA */
} while (err == ERROR_MORE_DATA && nloop++ < 100);
if (err != NERR_Success && err != NERR_GroupNotFound)
{
SetLastError(err);
MsgToEventLog(M_SYSERR, TEXT("In NetLocalGroupGetMembers for group '%s'"), group_name);
}
return ret;
}
/*
* Check whether option argv[0] is white-listed. If argv[0] == "--config",
* also check that argv[1], if present, passes CheckConfigPath().
* The caller should set argc to the number of valid elements in argv[] array.
*/
BOOL
CheckOption(const WCHAR *workdir, int argc, WCHAR *argv[], const settings_t *s)
{
/* Do not modify argv or *argv -- ideally it should be const WCHAR *const *, but alas...*/
if (wcscmp(argv[0], L"--config") == 0
&& argc > 1
&& !CheckConfigPath(workdir, argv[1], s)
)
{
return FALSE;
}
/* option name starts at 2 characters from argv[i] */
if (OptionLookup(argv[0] + 2, white_list) == -1) /* not found */
{
return FALSE;
}
return TRUE;
}