Skip to content

Commit 60d66e6

Browse files
committed
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 <[email protected]> Reviewed-by: Alex Brainman <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: David Chase <[email protected]>
1 parent dea1262 commit 60d66e6

File tree

5 files changed

+206
-21
lines changed

5 files changed

+206
-21
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
On Windows, [Current], [Lookup] and [LookupId] now supports the
2+
following built-in service user accounts:
3+
- `NT AUTHORITY\SYSTEM`
4+
- `NT AUTHORITY\LOCAL SERVICE`
5+
- `NT AUTHORITY\NETWORK SERVICE`

src/internal/syscall/windows/security_windows.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,27 @@ func GetTokenGroups(t syscall.Token) (*TOKEN_GROUPS, error) {
210210
}
211211
return (*TOKEN_GROUPS)(i), nil
212212
}
213+
214+
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_identifier_authority
215+
type SID_IDENTIFIER_AUTHORITY struct {
216+
Value [6]byte
217+
}
218+
219+
const (
220+
SID_REVISION = 1
221+
// https://learn.microsoft.com/en-us/windows/win32/services/localsystem-account
222+
SECURITY_LOCAL_SYSTEM_RID = 18
223+
// https://learn.microsoft.com/en-us/windows/win32/services/localservice-account
224+
SECURITY_LOCAL_SERVICE_RID = 19
225+
// https://learn.microsoft.com/en-us/windows/win32/services/networkservice-account
226+
SECURITY_NETWORK_SERVICE_RID = 20
227+
)
228+
229+
var SECURITY_NT_AUTHORITY = SID_IDENTIFIER_AUTHORITY{
230+
Value: [6]byte{0, 0, 0, 0, 0, 5},
231+
}
232+
233+
//sys IsValidSid(sid *syscall.SID) (valid bool) = advapi32.IsValidSid
234+
//sys GetSidIdentifierAuthority(sid *syscall.SID) (idauth *SID_IDENTIFIER_AUTHORITY) = advapi32.GetSidIdentifierAuthority
235+
//sys GetSidSubAuthority(sid *syscall.SID, subAuthorityIdx uint32) (subAuth *uint32) = advapi32.GetSidSubAuthority
236+
//sys GetSidSubAuthorityCount(sid *syscall.SID) (count *uint8) = advapi32.GetSidSubAuthorityCount

src/internal/syscall/windows/zsyscall_windows.go

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/os/user/lookup_windows.go

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,73 @@ func getProfilesDirectory() (string, error) {
8686
}
8787
}
8888

89+
func isServiceAccount(sid *syscall.SID) bool {
90+
if !windows.IsValidSid(sid) {
91+
// We don't accept SIDs from the public API, so this should never happen.
92+
// Better be on the safe side and validate anyway.
93+
return false
94+
}
95+
// The following RIDs are considered service user accounts as per
96+
// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids and
97+
// https://learn.microsoft.com/en-us/windows/win32/services/service-user-accounts:
98+
// - "S-1-5-18": LocalSystem
99+
// - "S-1-5-19": LocalService
100+
// - "S-1-5-20": NetworkService
101+
if *windows.GetSidSubAuthorityCount(sid) != windows.SID_REVISION ||
102+
*windows.GetSidIdentifierAuthority(sid) != windows.SECURITY_NT_AUTHORITY {
103+
return false
104+
}
105+
switch *windows.GetSidSubAuthority(sid, 0) {
106+
case windows.SECURITY_LOCAL_SYSTEM_RID,
107+
windows.SECURITY_LOCAL_SERVICE_RID,
108+
windows.SECURITY_NETWORK_SERVICE_RID:
109+
return true
110+
}
111+
return false
112+
}
113+
114+
func isValidUserAccountType(sid *syscall.SID, sidType uint32) bool {
115+
switch sidType {
116+
case syscall.SidTypeUser:
117+
return true
118+
case syscall.SidTypeWellKnownGroup:
119+
return isServiceAccount(sid)
120+
}
121+
return false
122+
}
123+
124+
func isValidGroupAccountType(sidType uint32) bool {
125+
switch sidType {
126+
case syscall.SidTypeGroup:
127+
return true
128+
case syscall.SidTypeWellKnownGroup:
129+
// Some well-known groups are also considered service accounts,
130+
// so isValidUserAccountType would return true for them.
131+
// We have historically allowed them in LookupGroup and LookupGroupId,
132+
// so don't treat them as invalid here.
133+
return true
134+
case syscall.SidTypeAlias:
135+
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0
136+
// SidTypeAlias should also be treated as a group type next to SidTypeGroup
137+
// and SidTypeWellKnownGroup:
138+
// "alias object -> resource group: A group object..."
139+
//
140+
// Tests show that "Administrators" can be considered of type SidTypeAlias.
141+
return true
142+
}
143+
return false
144+
}
145+
89146
// lookupUsernameAndDomain obtains the username and domain for usid.
90-
func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, e error) {
91-
username, domain, t, e := usid.LookupAccount("")
147+
func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, sidType uint32, e error) {
148+
username, domain, sidType, e = usid.LookupAccount("")
92149
if e != nil {
93-
return "", "", e
150+
return "", "", 0, e
94151
}
95-
if t != syscall.SidTypeUser {
96-
return "", "", fmt.Errorf("user: should be user account type, not %d", t)
152+
if !isValidUserAccountType(usid, sidType) {
153+
return "", "", 0, fmt.Errorf("user: should be user account type, not %d", sidType)
97154
}
98-
return username, domain, nil
155+
return username, domain, sidType, nil
99156
}
100157

101158
// findHomeDirInRegistry finds the user home path based on the uid.
@@ -118,13 +175,7 @@ func lookupGroupName(groupname string) (string, error) {
118175
if e != nil {
119176
return "", e
120177
}
121-
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0
122-
// SidTypeAlias should also be treated as a group type next to SidTypeGroup
123-
// and SidTypeWellKnownGroup:
124-
// "alias object -> resource group: A group object..."
125-
//
126-
// Tests show that "Administrators" can be considered of type SidTypeAlias.
127-
if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
178+
if !isValidGroupAccountType(t) {
128179
return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)
129180
}
130181
return sid.String()
@@ -355,18 +406,27 @@ func lookupUserPrimaryGroup(username, domain string) (string, error) {
355406
}
356407

357408
func newUserFromSid(usid *syscall.SID) (*User, error) {
358-
username, domain, e := lookupUsernameAndDomain(usid)
359-
if e != nil {
360-
return nil, e
361-
}
362-
gid, e := lookupUserPrimaryGroup(username, domain)
409+
username, domain, sidType, e := lookupUsernameAndDomain(usid)
363410
if e != nil {
364411
return nil, e
365412
}
366413
uid, e := usid.String()
367414
if e != nil {
368415
return nil, e
369416
}
417+
var gid string
418+
if sidType == syscall.SidTypeWellKnownGroup {
419+
// The SID does not contain a domain; this function's domain variable has
420+
// been populated with the SID's identifier authority. This happens with
421+
// special service user accounts such as "NT AUTHORITY\LocalSystem".
422+
// In this case, gid is the same as the user SID.
423+
gid = uid
424+
} else {
425+
gid, e = lookupUserPrimaryGroup(username, domain)
426+
if e != nil {
427+
return nil, e
428+
}
429+
}
370430
// If this user has logged in at least once their home path should be stored
371431
// in the registry under the specified SID. References:
372432
// 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) {
396456
if e != nil {
397457
return nil, e
398458
}
399-
if t != syscall.SidTypeUser {
459+
if !isValidUserAccountType(sid, t) {
400460
return nil, fmt.Errorf("user: should be user account type, not %d", t)
401461
}
402462
return newUserFromSid(sid)
@@ -427,7 +487,7 @@ func lookupGroupId(gid string) (*Group, error) {
427487
if err != nil {
428488
return nil, err
429489
}
430-
if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
490+
if !isValidGroupAccountType(t) {
431491
return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
432492
}
433493
return &Group{Name: groupname, Gid: gid}, nil
@@ -465,7 +525,7 @@ func listGroups(user *User) ([]string, error) {
465525
if err != nil {
466526
return nil, err
467527
}
468-
username, domain, err := lookupUsernameAndDomain(sid)
528+
username, domain, _, err := lookupUsernameAndDomain(sid)
469529
if err != nil {
470530
return nil, err
471531
}

src/os/user/user_windows_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,71 @@ func TestGroupIdsTestUser(t *testing.T) {
202202
t.Errorf("%+v.GroupIds() = %v; does not contain user GID %s", user, gids, user.Gid)
203203
}
204204
}
205+
206+
var serviceAccounts = []struct {
207+
sid string
208+
name string
209+
}{
210+
{"S-1-5-18", "NT AUTHORITY\\SYSTEM"},
211+
{"S-1-5-19", "NT AUTHORITY\\LOCAL SERVICE"},
212+
{"S-1-5-20", "NT AUTHORITY\\NETWORK SERVICE"},
213+
}
214+
215+
func TestLookupServiceAccount(t *testing.T) {
216+
t.Parallel()
217+
for _, tt := range serviceAccounts {
218+
u, err := Lookup(tt.name)
219+
if err != nil {
220+
t.Errorf("Lookup(%q): %v", tt.name, err)
221+
continue
222+
}
223+
if u.Uid != tt.sid {
224+
t.Errorf("unexpected uid for %q; got %q, want %q", u.Name, u.Uid, tt.sid)
225+
}
226+
}
227+
}
228+
229+
func TestLookupIdServiceAccount(t *testing.T) {
230+
t.Parallel()
231+
for _, tt := range serviceAccounts {
232+
u, err := LookupId(tt.sid)
233+
if err != nil {
234+
t.Errorf("LookupId(%q): %v", tt.sid, err)
235+
continue
236+
}
237+
if u.Gid != tt.sid {
238+
t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
239+
}
240+
if u.Username != tt.name {
241+
t.Errorf("unexpected user name for %q; got %q, want %q", u.Gid, u.Username, tt.name)
242+
}
243+
}
244+
}
245+
246+
func TestLookupGroupServiceAccount(t *testing.T) {
247+
t.Parallel()
248+
for _, tt := range serviceAccounts {
249+
u, err := LookupGroup(tt.name)
250+
if err != nil {
251+
t.Errorf("LookupGroup(%q): %v", tt.name, err)
252+
continue
253+
}
254+
if u.Gid != tt.sid {
255+
t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
256+
}
257+
}
258+
}
259+
260+
func TestLookupGroupIdServiceAccount(t *testing.T) {
261+
t.Parallel()
262+
for _, tt := range serviceAccounts {
263+
u, err := LookupGroupId(tt.sid)
264+
if err != nil {
265+
t.Errorf("LookupGroupId(%q): %v", tt.sid, err)
266+
continue
267+
}
268+
if u.Gid != tt.sid {
269+
t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
270+
}
271+
}
272+
}

0 commit comments

Comments
 (0)