]> Cypherpunks repositories - gostls13.git/commitdiff
os/user: support built-in service user accounts on Windows
authorqmuntal <quimmuntal@gmail.com>
Thu, 7 Nov 2024 11:22:20 +0000 (12:22 +0100)
committerQuim Muntal <quimmuntal@gmail.com>
Fri, 15 Nov 2024 16:10:50 +0000 (16:10 +0000)
Built-in service user accounts should be treated as special cases
of well-known groups and allowed in user.Lookup and user.LookupId.

Namely, these accounts are:
- NT AUTHORITY\SYSTEM (S-1-5-18)
- NT AUTHORITY\LOCAL SERVICE (S-1-5-19)
- NT AUTHORITY\NETWORK SERVICE (S-1-5-20)

See https://learn.microsoft.com/en-us/windows/win32/services/service-user-accounts.

Note that #49509 also mentions S-1-5-17 (NT AUTHORITY\IUSR) as
another well-known group that should be treated as a user. I haven't
found any documentation supporting this claim, and it is not an account
that is used usually, so I'm not adding it for now.

This CL is heavily based on CL 452497.

Fixes #49509

Change-Id: I6e204ddfb4ed0c01b4503001cf284602531e4a88
Reviewed-on: https://go-review.googlesource.com/c/go/+/626255
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
doc/next/6-stdlib/99-minor/os/user/49509.md [new file with mode: 0644]
src/internal/syscall/windows/security_windows.go
src/internal/syscall/windows/zsyscall_windows.go
src/os/user/lookup_windows.go
src/os/user/user_windows_test.go

diff --git a/doc/next/6-stdlib/99-minor/os/user/49509.md b/doc/next/6-stdlib/99-minor/os/user/49509.md
new file mode 100644 (file)
index 0000000..d853d90
--- /dev/null
@@ -0,0 +1,5 @@
+On Windows, [Current], [Lookup] and [LookupId] now supports the
+following built-in service user accounts:
+- `NT AUTHORITY\SYSTEM`
+- `NT AUTHORITY\LOCAL SERVICE`
+- `NT AUTHORITY\NETWORK SERVICE`
index aed04c61c49269e8cc5710a245c7bac27211a52c..547c30031a12f39d5c6e51e398b41226ca98d134 100644 (file)
@@ -210,3 +210,27 @@ func GetTokenGroups(t syscall.Token) (*TOKEN_GROUPS, error) {
        }
        return (*TOKEN_GROUPS)(i), nil
 }
+
+// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_identifier_authority
+type SID_IDENTIFIER_AUTHORITY struct {
+       Value [6]byte
+}
+
+const (
+       SID_REVISION = 1
+       // https://learn.microsoft.com/en-us/windows/win32/services/localsystem-account
+       SECURITY_LOCAL_SYSTEM_RID = 18
+       // https://learn.microsoft.com/en-us/windows/win32/services/localservice-account
+       SECURITY_LOCAL_SERVICE_RID = 19
+       // https://learn.microsoft.com/en-us/windows/win32/services/networkservice-account
+       SECURITY_NETWORK_SERVICE_RID = 20
+)
+
+var SECURITY_NT_AUTHORITY = SID_IDENTIFIER_AUTHORITY{
+       Value: [6]byte{0, 0, 0, 0, 0, 5},
+}
+
+//sys  IsValidSid(sid *syscall.SID) (valid bool) = advapi32.IsValidSid
+//sys  GetSidIdentifierAuthority(sid *syscall.SID) (idauth *SID_IDENTIFIER_AUTHORITY) = advapi32.GetSidIdentifierAuthority
+//sys  GetSidSubAuthority(sid *syscall.SID, subAuthorityIdx uint32) (subAuth *uint32) = advapi32.GetSidSubAuthority
+//sys  GetSidSubAuthorityCount(sid *syscall.SID) (count *uint8) = advapi32.GetSidSubAuthorityCount
index 711ebd679a27e7e91dedad118411a118f0e921c3..f4048c440e32eaeef67a18fafe658eb2b11ae61d 100644 (file)
@@ -49,8 +49,12 @@ var (
 
        procAdjustTokenPrivileges             = modadvapi32.NewProc("AdjustTokenPrivileges")
        procDuplicateTokenEx                  = modadvapi32.NewProc("DuplicateTokenEx")
+       procGetSidIdentifierAuthority         = modadvapi32.NewProc("GetSidIdentifierAuthority")
+       procGetSidSubAuthority                = modadvapi32.NewProc("GetSidSubAuthority")
+       procGetSidSubAuthorityCount           = modadvapi32.NewProc("GetSidSubAuthorityCount")
        procImpersonateLoggedOnUser           = modadvapi32.NewProc("ImpersonateLoggedOnUser")
        procImpersonateSelf                   = modadvapi32.NewProc("ImpersonateSelf")
+       procIsValidSid                        = modadvapi32.NewProc("IsValidSid")
        procLogonUserW                        = modadvapi32.NewProc("LogonUserW")
        procLookupPrivilegeValueW             = modadvapi32.NewProc("LookupPrivilegeValueW")
        procOpenSCManagerW                    = modadvapi32.NewProc("OpenSCManagerW")
@@ -120,6 +124,24 @@ func DuplicateTokenEx(hExistingToken syscall.Token, dwDesiredAccess uint32, lpTo
        return
 }
 
+func GetSidIdentifierAuthority(sid *syscall.SID) (idauth *SID_IDENTIFIER_AUTHORITY) {
+       r0, _, _ := syscall.Syscall(procGetSidIdentifierAuthority.Addr(), 1, uintptr(unsafe.Pointer(sid)), 0, 0)
+       idauth = (*SID_IDENTIFIER_AUTHORITY)(unsafe.Pointer(r0))
+       return
+}
+
+func GetSidSubAuthority(sid *syscall.SID, subAuthorityIdx uint32) (subAuth *uint32) {
+       r0, _, _ := syscall.Syscall(procGetSidSubAuthority.Addr(), 2, uintptr(unsafe.Pointer(sid)), uintptr(subAuthorityIdx), 0)
+       subAuth = (*uint32)(unsafe.Pointer(r0))
+       return
+}
+
+func GetSidSubAuthorityCount(sid *syscall.SID) (count *uint8) {
+       r0, _, _ := syscall.Syscall(procGetSidSubAuthorityCount.Addr(), 1, uintptr(unsafe.Pointer(sid)), 0, 0)
+       count = (*uint8)(unsafe.Pointer(r0))
+       return
+}
+
 func ImpersonateLoggedOnUser(token syscall.Token) (err error) {
        r1, _, e1 := syscall.Syscall(procImpersonateLoggedOnUser.Addr(), 1, uintptr(token), 0, 0)
        if r1 == 0 {
@@ -136,6 +158,12 @@ func ImpersonateSelf(impersonationlevel uint32) (err error) {
        return
 }
 
+func IsValidSid(sid *syscall.SID) (valid bool) {
+       r0, _, _ := syscall.Syscall(procIsValidSid.Addr(), 1, uintptr(unsafe.Pointer(sid)), 0, 0)
+       valid = r0 != 0
+       return
+}
+
 func LogonUser(username *uint16, domain *uint16, password *uint16, logonType uint32, logonProvider uint32, token *syscall.Token) (err error) {
        r1, _, e1 := syscall.Syscall6(procLogonUserW.Addr(), 6, uintptr(unsafe.Pointer(username)), uintptr(unsafe.Pointer(domain)), uintptr(unsafe.Pointer(password)), uintptr(logonType), uintptr(logonProvider), uintptr(unsafe.Pointer(token)))
        if r1 == 0 {
index 5d990600657f43201d638daff46d08b50522233a..11bb58e87bde54a65cbf76bf49c95dbc7191409a 100644 (file)
@@ -86,16 +86,73 @@ func getProfilesDirectory() (string, error) {
        }
 }
 
+func isServiceAccount(sid *syscall.SID) bool {
+       if !windows.IsValidSid(sid) {
+               // We don't accept SIDs from the public API, so this should never happen.
+               // Better be on the safe side and validate anyway.
+               return false
+       }
+       // The following RIDs are considered service user accounts as per
+       // https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids and
+       // https://learn.microsoft.com/en-us/windows/win32/services/service-user-accounts:
+       // - "S-1-5-18": LocalSystem
+       // - "S-1-5-19": LocalService
+       // - "S-1-5-20": NetworkService
+       if *windows.GetSidSubAuthorityCount(sid) != windows.SID_REVISION ||
+               *windows.GetSidIdentifierAuthority(sid) != windows.SECURITY_NT_AUTHORITY {
+               return false
+       }
+       switch *windows.GetSidSubAuthority(sid, 0) {
+       case windows.SECURITY_LOCAL_SYSTEM_RID,
+               windows.SECURITY_LOCAL_SERVICE_RID,
+               windows.SECURITY_NETWORK_SERVICE_RID:
+               return true
+       }
+       return false
+}
+
+func isValidUserAccountType(sid *syscall.SID, sidType uint32) bool {
+       switch sidType {
+       case syscall.SidTypeUser:
+               return true
+       case syscall.SidTypeWellKnownGroup:
+               return isServiceAccount(sid)
+       }
+       return false
+}
+
+func isValidGroupAccountType(sidType uint32) bool {
+       switch sidType {
+       case syscall.SidTypeGroup:
+               return true
+       case syscall.SidTypeWellKnownGroup:
+               // Some well-known groups are also considered service accounts,
+               // so isValidUserAccountType would return true for them.
+               // We have historically allowed them in LookupGroup and LookupGroupId,
+               // so don't treat them as invalid here.
+               return true
+       case syscall.SidTypeAlias:
+               // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0
+               // SidTypeAlias should also be treated as a group type next to SidTypeGroup
+               // and SidTypeWellKnownGroup:
+               // "alias object -> resource group: A group object..."
+               //
+               // Tests show that "Administrators" can be considered of type SidTypeAlias.
+               return true
+       }
+       return false
+}
+
 // lookupUsernameAndDomain obtains the username and domain for usid.
-func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, e error) {
-       username, domain, t, e := usid.LookupAccount("")
+func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, sidType uint32, e error) {
+       username, domain, sidType, e = usid.LookupAccount("")
        if e != nil {
-               return "", "", e
+               return "", "", 0, e
        }
-       if t != syscall.SidTypeUser {
-               return "", "", fmt.Errorf("user: should be user account type, not %d", t)
+       if !isValidUserAccountType(usid, sidType) {
+               return "", "", 0, fmt.Errorf("user: should be user account type, not %d", sidType)
        }
-       return username, domain, nil
+       return username, domain, sidType, nil
 }
 
 // findHomeDirInRegistry finds the user home path based on the uid.
@@ -118,13 +175,7 @@ func lookupGroupName(groupname string) (string, error) {
        if e != nil {
                return "", e
        }
-       // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0
-       // SidTypeAlias should also be treated as a group type next to SidTypeGroup
-       // and SidTypeWellKnownGroup:
-       // "alias object -> resource group: A group object..."
-       //
-       // Tests show that "Administrators" can be considered of type SidTypeAlias.
-       if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
+       if !isValidGroupAccountType(t) {
                return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)
        }
        return sid.String()
@@ -355,11 +406,7 @@ func lookupUserPrimaryGroup(username, domain string) (string, error) {
 }
 
 func newUserFromSid(usid *syscall.SID) (*User, error) {
-       username, domain, e := lookupUsernameAndDomain(usid)
-       if e != nil {
-               return nil, e
-       }
-       gid, e := lookupUserPrimaryGroup(username, domain)
+       username, domain, sidType, e := lookupUsernameAndDomain(usid)
        if e != nil {
                return nil, e
        }
@@ -367,6 +414,19 @@ func newUserFromSid(usid *syscall.SID) (*User, error) {
        if e != nil {
                return nil, e
        }
+       var gid string
+       if sidType == syscall.SidTypeWellKnownGroup {
+               // The SID does not contain a domain; this function's domain variable has
+               // been populated with the SID's identifier authority. This happens with
+               // special service user accounts such as "NT AUTHORITY\LocalSystem".
+               // In this case, gid is the same as the user SID.
+               gid = uid
+       } else {
+               gid, e = lookupUserPrimaryGroup(username, domain)
+               if e != nil {
+                       return nil, e
+               }
+       }
        // If this user has logged in at least once their home path should be stored
        // in the registry under the specified SID. References:
        // https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx
@@ -396,7 +456,7 @@ func lookupUser(username string) (*User, error) {
        if e != nil {
                return nil, e
        }
-       if t != syscall.SidTypeUser {
+       if !isValidUserAccountType(sid, t) {
                return nil, fmt.Errorf("user: should be user account type, not %d", t)
        }
        return newUserFromSid(sid)
@@ -427,7 +487,7 @@ func lookupGroupId(gid string) (*Group, error) {
        if err != nil {
                return nil, err
        }
-       if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
+       if !isValidGroupAccountType(t) {
                return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
        }
        return &Group{Name: groupname, Gid: gid}, nil
@@ -465,7 +525,7 @@ func listGroups(user *User) ([]string, error) {
                if err != nil {
                        return nil, err
                }
-               username, domain, err := lookupUsernameAndDomain(sid)
+               username, domain, _, err := lookupUsernameAndDomain(sid)
                if err != nil {
                        return nil, err
                }
index ff5bc5e8a0fa6c64acc8b417a47e8447bd43f69b..ff5155e1f8fc006c34de7ab1d0c65e6cf2949802 100644 (file)
@@ -202,3 +202,71 @@ func TestGroupIdsTestUser(t *testing.T) {
                t.Errorf("%+v.GroupIds() = %v; does not contain user GID %s", user, gids, user.Gid)
        }
 }
+
+var serviceAccounts = []struct {
+       sid  string
+       name string
+}{
+       {"S-1-5-18", "NT AUTHORITY\\SYSTEM"},
+       {"S-1-5-19", "NT AUTHORITY\\LOCAL SERVICE"},
+       {"S-1-5-20", "NT AUTHORITY\\NETWORK SERVICE"},
+}
+
+func TestLookupServiceAccount(t *testing.T) {
+       t.Parallel()
+       for _, tt := range serviceAccounts {
+               u, err := Lookup(tt.name)
+               if err != nil {
+                       t.Errorf("Lookup(%q): %v", tt.name, err)
+                       continue
+               }
+               if u.Uid != tt.sid {
+                       t.Errorf("unexpected uid for %q; got %q, want %q", u.Name, u.Uid, tt.sid)
+               }
+       }
+}
+
+func TestLookupIdServiceAccount(t *testing.T) {
+       t.Parallel()
+       for _, tt := range serviceAccounts {
+               u, err := LookupId(tt.sid)
+               if err != nil {
+                       t.Errorf("LookupId(%q): %v", tt.sid, err)
+                       continue
+               }
+               if u.Gid != tt.sid {
+                       t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
+               }
+               if u.Username != tt.name {
+                       t.Errorf("unexpected user name for %q; got %q, want %q", u.Gid, u.Username, tt.name)
+               }
+       }
+}
+
+func TestLookupGroupServiceAccount(t *testing.T) {
+       t.Parallel()
+       for _, tt := range serviceAccounts {
+               u, err := LookupGroup(tt.name)
+               if err != nil {
+                       t.Errorf("LookupGroup(%q): %v", tt.name, err)
+                       continue
+               }
+               if u.Gid != tt.sid {
+                       t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
+               }
+       }
+}
+
+func TestLookupGroupIdServiceAccount(t *testing.T) {
+       t.Parallel()
+       for _, tt := range serviceAccounts {
+               u, err := LookupGroupId(tt.sid)
+               if err != nil {
+                       t.Errorf("LookupGroupId(%q): %v", tt.sid, err)
+                       continue
+               }
+               if u.Gid != tt.sid {
+                       t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
+               }
+       }
+}