Fix user's group membership check in interactive service to work with domains
PATCH 1/1 Fix user's group membership check in interactive service to work with domains
From: Selva Nair <[email protected]...>
Currently the username unqualified by the domain is used to validate a user which fails for domain users. Instead compare the user's SID with SIDs in the Administrators group and ovpn_admin_group.
This has the advantage that connection to a domain controller is not required and will work even when user is logged in with cached credentials.
Limitations:
(i) Group membership is not checked recursively
(ii) Domain administrators will not be recognized as members of local Administrators group.
Resolves Trac: #810
--- src/openvpnserv/validate.c | 95 +++++++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 40 deletions(-)
src/openvpnserv/validate.c +55 -40
@@ -49,6 +49,8 @@ static const WCHAR *white_list[] = NULL /* last value */ }; +static BOOL IsUserInGroup (PSID sid,const WCHAR *group_name); +/* * 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
@@ -152,16 +154,11 @@ GetBuiltinAdminGroupName(WCHAR *name, DWORD nlen)BOOL IsAuthorizedUser (SID *sid,settings_t *s) { -LOCALGROUP_USERS_INFO_0 *groups =NULL ; -DWORD nread; -DWORD nmax; -WCHAR *tmp =NULL ;const WCHAR *admin_group[2 ];WCHAR username[MAX_NAME ];WCHAR domain[MAX_NAME ];WCHAR sysadmin_group[MAX_NAME ]; -DWORD err, len =MAX_NAME ; -int i; +DWORD len =MAX_NAME ;BOOL ret = FALSE;SID_NAME_USE sid_type;
@@ -169,17 +166,9 @@ IsAuthorizedUser(SID *sid, settings_t *s)if (!LookupAccountSidW (NULL , sid, username, &len, domain, &len, &sid_type)) {MsgToEventLog (M_SYSERR ,TEXT ("LookupAccountSid" )); - goto out; - } - -/* Get an array of groups the user is member of */ - err =NetUserGetLocalGroups (NULL , username,0 ,LG_INCLUDE_INDIRECT , (LPBYTE *) &groups, -MAX_PREFERRED_LENGTH , &nread, &nmax); -if (err && err !=ERROR_MORE_DATA ) - { -SetLastError (err); -MsgToEventLog (M_SYSERR ,TEXT ("NetUserGetLocalGroups" )); - goto out; +/* not fatal as this is now used only for logging */ + username[0 ] ='\0' ; + domain[0 ] ='\0' ; }if (GetBuiltinAdminGroupName (sysadmin_group,_countof (sysadmin_group)))
@@ -193,40 +182,66 @@ IsAuthorizedUser(SID *sid, settings_t *s) admin_group[0 ] =SYSTEM_ADMIN_GROUP ; } -#ifdef UNICODE admin_group[1 ] = s->ovpn_admin_group; -#else - tmp =NULL ; - len =MultiByteToWideChar (CP_UTF8 ,0 , s->ovpn_admin_group,-1 ,NULL ,0 ); -if (len ==0 || (tmp =malloc (len*sizeof (WCHAR ))) ==NULL ) - { -MsgToEventLog (M_SYSERR ,TEXT ("Failed to convert admin group name to WideChar" )); - goto out; - } -MultiByteToWideChar (CP_UTF8 ,0 , s->ovpn_admin_group,-1 , tmp, len); - admin_group[1 ] = tmp; -#endif -/* Check if user's groups include any of the admin groups */ -for (i =0 ; i < nread; i++) +for (int i =0 ; i <2 ; ++i) { -if (wcscmp (groups[i].lgrui0_name, admin_group[0 ]) ==0 - ||wcscmp (groups[i].lgrui0_name, admin_group[1 ]) ==0 - ) + ret =IsUserInGroup (sid, admin_group[i]); +if (ret) { -MsgToEventLog (M_INFO ,TEXT ("Authorizing user %s by virtue of membership in group %s" ), - username, groups[i].lgrui0_name); - ret = TRUE; -break ; +MsgToEventLog (M_INFO ,TEXT ("Authorizing user '%s@%s' by virtue of membership in group '%s'" ), + username, domain, admin_group[i]); + goto out; } } out: -if (groups) +return ret; +} + +/** +* Check a user with specified sid is in a local group named group_name +* +* Using sid instead of username avoids reference to domains so that +* this could be completed without access to the Domain Controller. +* +* Returns true if the user is in the group, false otherwise. +*/ +static BOOL +IsUserInGroup (PSID sid,const WCHAR *group_name) +{ +BOOL ret = FALSE; +LOCALGROUP_MEMBERS_INFO_0 *groups; +DWORD_PTR resume =0 ; +DWORD err, nread, nmax; +int nloop =0 ;/* a counter used to not get stuck in the do .. while() */ + +do { + groups =NULL ; + err =NetLocalGroupGetMembers (NULL , group_name,0 , (LPBYTE *) &groups, +MAX_PREFERRED_LENGTH , &nread, &nmax, &resume); +if (err && err !=ERROR_MORE_DATA ) + { +break ; + } + +/* If a match is already found, ret = TRUE, the loop is skipped */ +for (int i =0 ; i < nread && !ret; ++i) + { + ret =EqualSid (groups[i].lgrmi0_sid, sid); + } +NetApiBufferFree (groups); + +/* 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); } -free (tmp);return ret; }
Signature · 1 line
2.1.4
Quoted 9 lines · On Sun, Jan 8, 2017 at 3:00 PM, <[email protected]...> wrote:
From: Selva Nair <[email protected]...>
Currently the username unqualified by the domain is used to validate a user which fails for domain users. Instead compare the user's SID with SIDs in the Administrators group and
ovpn_admin_group.This has the advantage that connection to a domain controller is not required and will work even when user is logged in with cached credentials.
Quoted 7 lines
Limitations:
(i) Group membership is not checked recursively
(ii) Domain administrators will not be recognized as members of local Administrators group.Resolves Trac: #810
Tested on Windows 7 as local user and domain user. Also tested by jiquera as described in Trac: #810
Hi,
This is like talking to myself, but here goes:
Quoted 7 lines · On Sun, Jan 8, 2017 at 3:00 PM, <[email protected]...> wrote:
Currently the username unqualified by the domain is used to validate a user which fails for domain users. Instead compare the user's SID with SIDs in the Administrators group and
ovpn_admin_group.This has the advantage that connection to a domain controller is not required and will work even when user is logged in with cached credentials.
Quoted 7 lines
Limitations:
(i) Group membership is not checked recursively
(ii) Domain administrators will not be recognized as members of local Administrators group.Resolves Trac: #810
Based on further discussions on Trac: #810, domain admins are understandably not pleased with those limitations. I'll submit a v2.
Selva
Hi,
Quoted 1 line · Selva Nair
This is like talking to myself, but here goes:
I do read this (and #810) with lots of interest :-)
Have not had time to do actual testing, though, but have learned a lot about windows and shortcuts...
gert
Signature · 4 lines
USENET is *not* the non-clickable part of WWW!
//www.muc.de/~gert/
Gert Doering - Munich, Germany [email protected]...
fax: +49-89-35655025 [email protected]...PATCH v2 1/1 Fix user's group membership check in interactive service to work with domains
In reply to [email protected]...From: Selva Nair <[email protected]...>
Currently the username unqualified by the domain is used to validate a user which fails for domain users. Instead authorize the user
(i) if the built-in admin group or ovpn_admin group is in the process token (ii) else if the user's SID is in the built-in admin or ovpn_admin groups
The second check is needed to recognize dynamic updates to group membership on the local machine that will not be reflected in the token.
These checks do not require connection to a domain controller and will work even when user is logged in with cached credentials.
Resolves Trac: #810
v2: include the token check as described above
--- src/openvpnserv/interactive.c | 2 +- src/openvpnserv/validate.c | 171 +++++++++++++++++++++++++++++++----------- src/openvpnserv/validate.h | 2 +- 3 files changed, 130 insertions(+), 45 deletions(-)
src/openvpnserv/interactive.c +1 -1
@@ -1475,7 +1475,7 @@ RunOpenvpn(LPVOID p) }/* Check user is authorized or options are white-listed */ -if (!IsAuthorizedUser (ovpn_user->User.Sid, &settings) +if (!IsAuthorizedUser (ovpn_user->User.Sid, imp_token, settings.ovpn_admin_group) && !ValidateOptions (pipe, sud.directory, sud.options)) { goto out;
src/openvpnserv/validate.c +128 -43
213 lines not on this page · raw message
src/openvpnserv/validate.h +1 -1
@@ -34,7 +34,7 @@/* The last one may be reset in registry: HKLM\Software\OpenVPN\ovpn_admin_group */ BOOL -IsAuthorizedUser (SID *sid,settings_t *s); +IsAuthorizedUser (PSID sid,const HANDLE token,const WCHAR *ovpn_admin_group);BOOL CheckOption (const WCHAR *workdir,int narg,WCHAR *argv[],const settings_t *s);
Signature · 1 line
2.1.4
Hi,
Quoted 11 lines · On Sat, Jan 14, 2017 at 4:16 PM, <[email protected]...> wrote:
From: Selva Nair <[email protected]...>
Currently the username unqualified by the domain is used to validate a user which fails for domain users. Instead authorize the user
(i) if the built-in admin group or
ovpn_admingroup is in the process token (ii) else if the user's SID is in the built-in admin orovpn_admingroupsThe second check is needed to recognize dynamic updates to group membership on the local machine that will not be reflected in the token.
Quoted 6 lines
These checks do not require connection to a domain controller and will work even when user is logged in with cached credentials.
Resolves Trac: #810
v2: include the token check as described above
Bump :) This addresses a critical issue that I would like to see fixed in 2.4.1..
Thanks,
Selva
HI,
Quoted 11 lines · On Sat, Jan 14, 2017 at 04:16:29PM -0500, [email protected]... wrote:
From: Selva Nair <[email protected]...>
Currently the username unqualified by the domain is used to validate a user which fails for domain users. Instead authorize the user
(i) if the built-in admin group or
ovpn_admingroup is in the process token (ii) else if the user's SID is in the built-in admin orovpn_admingroupsThe second check is needed to recognize dynamic updates to group membership on the local machine that will not be reflected in the token.
Quoted 6 lines
These checks do not require connection to a domain controller and will work even when user is logged in with cached credentials.
Resolves Trac: #810
v2: include the token check as described above
Took me way too long... the code change looks reasonable ("does what it says on the tin, and safely so").
One questions occured to me, though...
MS documentation for GetTokenInformation() suggests that group membership tests should be done with "CheckTokenMembership()", which sounds more convenient than "extract them all and walk the list" - so maybe this is done to avoid domain controller contact?
OTOH, the example given somewhere else does the same thing with EqualSid(), so MS doesn't know what to recommend either, it seems...
https://msdn.microsoft.com/de-de/library/windows/desktop/aa379554(v=vs.85).aspx
A few more tests, then merge...
gert
Signature · 4 lines
USENET is *not* the non-clickable part of WWW!
//www.muc.de/~gert/
Gert Doering - Munich, Germany [email protected]...
fax: +49-89-35655025 [email protected]...Quoted 24 lines · Gert Doering
Quoted 6 lines · On Sat, Jan 14, 2017 at 04:16:29PM -0500, [email protected]... wrote:
From: Selva Nair <[email protected]...>
Currently the username unqualified by the domain is used to validate a user which fails for domain users. Instead authorize the user
(i) if the built-in admin group or
ovpn_admingroup is in the processtoken
Quoted 3 lines
(ii) else if the user's SID is in the built-in admin or
ovpn_admingroupsThe second check is needed to recognize dynamic updates to group
membership
Quoted 8 lines
on the local machine that will not be reflected in the token.
These checks do not require connection to a domain controller and will work even when user is logged in with cached credentials.
Resolves Trac: #810
v2: include the token check as described above
Took me way too long... the code change looks reasonable ("does what it says on the tin, and safely so").
Quoted 6 lines
One questions occured to me, though...
MS documentation for
GetTokenInformation()suggests that group membership tests should be done with "CheckTokenMembership()", which sounds more convenient than "extract them all and walk the list" - so maybe this is done to avoid domain controller contact?
Thanks for the review :)
CheckTokenMembership() returns true only if the SID is present and enabled. That means when UAC is active it will not detect that the user is a member of administrators group as the SID will not be enabled. In other words, our usage of group membership is somewhat special -- we only care user is a member of admin or ovpn_admin groups, not that the corresponding rights be enabled in the token.
Selva
Hi,
Quoted 5 lines · Selva Nair
Quoted 4 lines
MS documentation for
GetTokenInformation()suggests that group membership tests should be done with "CheckTokenMembership()", which sounds more convenient than "extract them all and walk the list" - so maybe this is done to avoid domain controller contact?
Quoted 8 lines
Thanks for the review :)
CheckTokenMembership()returns true only if the SID is present and enabled. That means when UAC is active it will not detect that the user is a member of administrators group as the SID will not be enabled. In other words, our usage of group membership is somewhat special -- we only care user is a member of admin orovpn_admingroups, not that the corresponding rights be enabled in the token.
Oh, wow.
Thanks for the explanation - indeed, that makes sense :-)
gert
Signature · 4 lines
USENET is *not* the non-clickable part of WWW!
//www.muc.de/~gert/
Gert Doering - Munich, Germany [email protected]...
fax: +49-89-35655025 [email protected]...ACK, based on "according to MSDN documentation these are the correct functions and are called properly", and on the test results in #810.
I have only compile tested this (which succeeds).
Your patch has been applied to the master and release/2.4 branch.
commit e82733a1ab78062feca28578fe505b275a2356a6 (master) commit a9743bf25e661d66ca7537adfe457e75afc947c4 (release/2.4)
Author: Selva Nair Date: Sat Jan 14 16:16:29 2017 -0500
Fix user's group membership check in interactive service to work with domains
Signed-off-by: Selva Nair <[email protected]...>
Acked-by: Gert Doering <[email protected]...>
Message-Id: <[email protected]...>
URL: https://www.mail-archive.com/[email protected]/msg13877.html
Signed-off-by: Gert Doering <[email protected]...>--
kind regards,
Gert Doering