From 60d66e6a86085478bc86fc924d1cd0221565262f Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 7 Nov 2024 12:22:20 +0100 Subject: [PATCH] os/user: support built-in service user accounts on Windows 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 Reviewed-by: Alex Brainman LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase --- doc/next/6-stdlib/99-minor/os/user/49509.md | 5 + .../syscall/windows/security_windows.go | 24 +++++ .../syscall/windows/zsyscall_windows.go | 28 +++++ src/os/user/lookup_windows.go | 102 ++++++++++++++---- src/os/user/user_windows_test.go | 68 ++++++++++++ 5 files changed, 206 insertions(+), 21 deletions(-) create mode 100644 doc/next/6-stdlib/99-minor/os/user/49509.md 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 index 0000000000..d853d9055a --- /dev/null +++ b/doc/next/6-stdlib/99-minor/os/user/49509.md @@ -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` diff --git a/src/internal/syscall/windows/security_windows.go b/src/internal/syscall/windows/security_windows.go index aed04c61c4..547c30031a 100644 --- a/src/internal/syscall/windows/security_windows.go +++ b/src/internal/syscall/windows/security_windows.go @@ -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 diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go index 711ebd679a..f4048c440e 100644 --- a/src/internal/syscall/windows/zsyscall_windows.go +++ b/src/internal/syscall/windows/zsyscall_windows.go @@ -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 { diff --git a/src/os/user/lookup_windows.go b/src/os/user/lookup_windows.go index 5d99060065..11bb58e87b 100644 --- a/src/os/user/lookup_windows.go +++ b/src/os/user/lookup_windows.go @@ -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 } diff --git a/src/os/user/user_windows_test.go b/src/os/user/user_windows_test.go index ff5bc5e8a0..ff5155e1f8 100644 --- a/src/os/user/user_windows_test.go +++ b/src/os/user/user_windows_test.go @@ -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) + } + } +} -- 2.48.1