]> git.ipfire.org Git - thirdparty/openvpn.git/commitdiff
Fix user's group membership check in interactive service to work with domains
authorSelva Nair <selva.nair@gmail.com>
Sat, 14 Jan 2017 21:16:29 +0000 (16:16 -0500)
committerGert Doering <gert@greenie.muc.de>
Mon, 20 Feb 2017 11:00:09 +0000 (12:00 +0100)
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.

Trac: #810

v2: include the token check as described above

Signed-off-by: Selva Nair <selva.nair@gmail.com>
Acked-by: Gert Doering <gert@greenie.muc.de>
Message-Id: <1484428589-7882-1-git-send-email-selva.nair@gmail.com>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg13877.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>
src/openvpnserv/interactive.c
src/openvpnserv/validate.c
src/openvpnserv/validate.h

index 11b475c4e5085795ce5f5bc082bf58d564e901d2..2bce598a77ba3d576628ba8921cbf12db3608839 100644 (file)
@@ -1477,7 +1477,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;
index c9c3855491bdd4a42ca8a97089c3f1ee393d5290..894c51b1b557ecc73deaa0541340a46d5ac523fa 100644 (file)
@@ -49,6 +49,9 @@ static const WCHAR *white_list[] =
     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
@@ -147,21 +150,16 @@ GetBuiltinAdminGroupName(WCHAR *name, DWORD nlen)
 
 /*
  * Check whether user is a member of Administrators group or
- * the group specified in s->ovpn_admin_group
+ * the group specified in ovpn_admin_group
  */
 BOOL
-IsAuthorizedUser(SID *sid, settings_t *s)
+IsAuthorizedUser(PSID sid, const HANDLE token, const WCHAR *ovpn_admin_group)
 {
-    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 +167,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)))
@@ -192,41 +182,136 @@ IsAuthorizedUser(SID *sid, settings_t *s)
         /* use the default value */
         admin_group[0] = SYSTEM_ADMIN_GROUP;
     }
+    admin_group[1] = ovpn_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)
+    PTOKEN_GROUPS token_groups = GetTokenGroups(token);
+    for (int i = 0; i < 2; ++i)
     {
-        MsgToEventLog(M_SYSERR, TEXT("Failed to convert admin group name to WideChar"));
-        goto out;
+        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;
+        }
     }
-    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++)
+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))
     {
-        if (wcscmp(groups[i].lgrui0_name, admin_group[0]) == 0
-            || wcscmp(groups[i].lgrui0_name, admin_group[1]) == 0
-            )
+        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)
         {
-            MsgToEventLog(M_INFO, TEXT("Authorizing user %s by virtue of membership in group %s"),
-                          username, groups[i].lgrui0_name);
-            ret = TRUE;
-            break;
+            if (EqualSid((PSID) grp_sid, token_groups->Groups[i].Sid))
+            {
+                return TRUE;
+            }
         }
     }
 
-out:
-    if (groups)
+    /* 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 (int 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)
     {
-        NetApiBufferFree(groups);
+        SetLastError(err);
+        MsgToEventLog(M_SYSERR, TEXT("In NetLocalGroupGetMembers for group '%s'"), group_name);
     }
-    free(tmp);
 
     return ret;
 }
index ece8704c789aa26e6db57a5d0c476b83bece4c60..033e0e4cfa4ad5f1a2ce6b1368122137e5fd2e27 100644 (file)
@@ -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);