Skip to contentSkip to conversation

Fix user's group membership check in interactive service to work with domains

PATCH Superseded [email protected]... 2 participants 10 messages Jan 8, 2017 – Feb 20, 2017

lore
Share & tools
lore
#0 lore raw

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
index c9c3855..98d868f 100644
@@ -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
author
#1 lore raw
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

author
#2 lore raw
In reply to [email protected]...

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

#3 lore raw

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]...
#4 lore raw

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
index dbe2b9b..9436fba 100644
@@ -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
index ece8704..033e0e4 100644
@@ -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
author
#5 lore raw

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_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.

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

#6 lore raw
In reply to [email protected]...

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_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.

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]...
author
#7 lore raw
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_admin group is in the process

token

Quoted 3 lines

(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

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

#8 lore raw

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 or ovpn_admin groups, 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]...
#9 lore raw
In reply to [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