1
0
mirror of https://github.com/golang/go synced 2024-11-22 05:04:40 -07:00

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 <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>
This commit is contained in:
qmuntal 2024-11-07 12:22:20 +01:00 committed by Quim Muntal
parent dea12628e6
commit 60d66e6a86
5 changed files with 206 additions and 21 deletions

View File

@ -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`

View 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

View 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 {

View 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
}

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