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) + } + } +}