mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-07 10:38:42 -04:00
294 lines
10 KiB
Go
294 lines
10 KiB
Go
//go:build windows
|
|
|
|
package server
|
|
|
|
import (
|
|
"os/user"
|
|
"testing"
|
|
"unsafe"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// filterNormalAccount limits NetUserEnum to normal user accounts.
|
|
const filterNormalAccount = 0x2
|
|
|
|
// TOKEN_ELEVATION_TYPE values.
|
|
const (
|
|
tokenElevationTypeDefault = 1
|
|
tokenElevationTypeFull = 2
|
|
tokenElevationTypeLimited = 3
|
|
)
|
|
|
|
// tokenElevationType reads TokenElevationType from a token.
|
|
func tokenElevationType(token windows.Token) (uint32, error) {
|
|
var elevationType, returnedLen uint32
|
|
err := windows.GetTokenInformation(token, windows.TokenElevationType,
|
|
(*byte)(unsafe.Pointer(&elevationType)), uint32(unsafe.Sizeof(elevationType)), &returnedLen)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return elevationType, nil
|
|
}
|
|
|
|
// userInfo0 mirrors USER_INFO_0.
|
|
type userInfo0 struct {
|
|
name *uint16
|
|
}
|
|
|
|
func mustParseSID(t *testing.T, s string) *windows.SID {
|
|
t.Helper()
|
|
sid, err := windows.StringToSid(s)
|
|
require.NoError(t, err, "parse SID %s", s)
|
|
return sid
|
|
}
|
|
|
|
// localAccountNames returns the names of the local user accounts.
|
|
func localAccountNames(t *testing.T) []string {
|
|
t.Helper()
|
|
|
|
var buf *byte
|
|
var entriesRead, totalEntries, resume uint32
|
|
err := windows.NetUserEnum(nil, 0, filterNormalAccount, &buf, maxPreferredLength,
|
|
&entriesRead, &totalEntries, &resume)
|
|
require.NoError(t, err, "enumerate local users")
|
|
t.Cleanup(func() {
|
|
require.NoError(t, windows.NetApiBufferFree(buf), "free NetApi buffer")
|
|
})
|
|
|
|
entries := unsafe.Slice((*userInfo0)(unsafe.Pointer(buf)), entriesRead)
|
|
names := make([]string, 0, entriesRead)
|
|
for _, entry := range entries {
|
|
names = append(names, windows.UTF16PtrToString(entry.name))
|
|
}
|
|
return names
|
|
}
|
|
|
|
// localAccountNameByRID returns the name of the local account carrying the
|
|
// given RID. Accounts such as Administrator and Guest can be renamed and are
|
|
// localized, so tests must not name them literally.
|
|
func localAccountNameByRID(t *testing.T, rid uint32) string {
|
|
t.Helper()
|
|
|
|
for _, name := range localAccountNames(t) {
|
|
sid, _, _, err := windows.LookupSID("", name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if sid.IdentifierAuthority() != windows.SECURITY_NT_AUTHORITY {
|
|
continue
|
|
}
|
|
count := sid.SubAuthorityCount()
|
|
if count < 2 || sid.SubAuthority(0) != 21 {
|
|
continue
|
|
}
|
|
if sid.SubAuthority(uint32(count-1)) == rid {
|
|
return name
|
|
}
|
|
}
|
|
|
|
t.Fatalf("no local account with RID %d", rid)
|
|
return ""
|
|
}
|
|
|
|
// wellKnownAccountName resolves a well-known SID to the qualified account name
|
|
// the local system uses for it, which is localized.
|
|
func wellKnownAccountName(t *testing.T, sidType windows.WELL_KNOWN_SID_TYPE) string {
|
|
t.Helper()
|
|
|
|
sid, err := windows.CreateWellKnownSid(sidType)
|
|
require.NoError(t, err, "create well-known SID")
|
|
name, domain, _, err := sid.LookupAccount("")
|
|
require.NoError(t, err, "resolve %s to an account name", sid)
|
|
if domain == "" {
|
|
return name
|
|
}
|
|
return domain + `\` + name
|
|
}
|
|
|
|
func TestIsBuiltinAdministratorSID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sid string
|
|
want bool
|
|
}{
|
|
{"machine_administrator", "S-1-5-21-1111111111-2222222222-3333333333-500", true},
|
|
{"domain_administrator", "S-1-5-21-3390233681-4087452608-412898826-500", true},
|
|
{"regular_user", "S-1-5-21-1111111111-2222222222-3333333333-1001", false},
|
|
{"guest_account", "S-1-5-21-1111111111-2222222222-3333333333-501", false},
|
|
{"domain_admins_group", "S-1-5-21-1111111111-2222222222-3333333333-512", false},
|
|
{"system", "S-1-5-18", false},
|
|
{"administrators_group", "S-1-5-32-544", false},
|
|
{"non_nt_authority", "S-1-1-0", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isBuiltinAdministratorSID(mustParseSID(t, tt.sid))
|
|
assert.Equal(t, tt.want, result, "RID 500 detection for %s", tt.sid)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsPrivilegedUserSID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sid string
|
|
want bool
|
|
}{
|
|
{"local_system", "S-1-5-18", true},
|
|
{"local_service", "S-1-5-19", true},
|
|
{"network_service", "S-1-5-20", true},
|
|
{"administrators_group", "S-1-5-32-544", true},
|
|
{"builtin_administrator", "S-1-5-21-1111111111-2222222222-3333333333-500", true},
|
|
{"regular_user", "S-1-5-21-1111111111-2222222222-3333333333-1001", false},
|
|
{"users_group", "S-1-5-32-545", false},
|
|
{"everyone", "S-1-1-0", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isPrivilegedUserSID(mustParseSID(t, tt.sid))
|
|
assert.Equal(t, tt.want, result, "SID privilege classification for %s", tt.sid)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsWindowsAccountPrivileged(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
username string
|
|
want bool
|
|
}{
|
|
{"system", wellKnownAccountName(t, windows.WinLocalSystemSid), true},
|
|
{"local_service", wellKnownAccountName(t, windows.WinLocalServiceSid), true},
|
|
{"network_service", wellKnownAccountName(t, windows.WinNetworkServiceSid), true},
|
|
{"administrators_group", wellKnownAccountName(t, windows.WinBuiltinAdministratorsSid), true},
|
|
// The built-in Administrator (RID 500) and Guest (RID 501) accounts
|
|
// exist on every Windows installation, though they may be disabled.
|
|
{"builtin_administrator", localAccountNameByRID(t, 500), true},
|
|
{"guest", localAccountNameByRID(t, 501), false},
|
|
// Unresolvable accounts fail closed.
|
|
{"nonexistent_user", "netbird-no-such-user", true},
|
|
{"empty_username", "", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isWindowsAccountPrivileged(tt.username)
|
|
assert.Equal(t, tt.want, result, "account privilege classification for %q", tt.username)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsProcessElevated(t *testing.T) {
|
|
elevated := isProcessElevated()
|
|
|
|
// TokenElevationType is a second, independent view of the same token:
|
|
// Full means elevated and Limited means a filtered administrator, while
|
|
// Default covers both a standard user and an administrator with no linked
|
|
// token (UAC off, the built-in Administrator, SYSTEM), so it implies nothing.
|
|
elevationType, err := tokenElevationType(windows.GetCurrentProcessToken())
|
|
require.NoError(t, err, "read token elevation type")
|
|
|
|
adminSid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
|
require.NoError(t, err, "create Administrators SID")
|
|
|
|
// Token(0) makes CheckTokenMembership evaluate the caller's own token. It
|
|
// counts only enabled SIDs, so a filtered administrator reports false here.
|
|
member, err := windows.Token(0).IsMember(adminSid)
|
|
require.NoError(t, err, "check own Administrators membership")
|
|
|
|
t.Logf("elevated=%v elevationType=%d memberOfAdministrators=%v", elevated, elevationType, member)
|
|
|
|
switch elevationType {
|
|
case tokenElevationTypeFull:
|
|
assert.True(t, elevated, "a token of elevation type Full must report elevated")
|
|
case tokenElevationTypeLimited:
|
|
assert.False(t, elevated, "a filtered administrator token must not report elevated")
|
|
}
|
|
|
|
// Administrators enabled in the token means the token wields administrative
|
|
// rights, which is what elevation reports.
|
|
if member {
|
|
assert.True(t, elevated, "token with enabled Administrators membership must report elevated")
|
|
}
|
|
}
|
|
|
|
// TestS4UMembershipAgreesWithLocalGroups exercises the S4U token path used
|
|
// for domain accounts. S4U logons need the TCB privilege, so the test runs
|
|
// only as SYSTEM (which is how CI executes the suite). For local accounts the
|
|
// token's Administrators membership must agree with the SAM enumeration.
|
|
func TestS4UMembershipAgreesWithLocalGroups(t *testing.T) {
|
|
system, err := windows.CreateWellKnownSid(windows.WinLocalSystemSid)
|
|
require.NoError(t, err, "create SYSTEM SID")
|
|
current, err := user.Current()
|
|
require.NoError(t, err, "get current user")
|
|
if current.Uid != system.String() {
|
|
t.Skipf("S4U logon requires SYSTEM (running as %s)", current.Username)
|
|
}
|
|
|
|
adminSid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
|
require.NoError(t, err, "create Administrators SID")
|
|
|
|
checked := 0
|
|
for _, name := range localAccountNames(t) {
|
|
viaToken, err := s4uTokenIsMember(name, ".", adminSid)
|
|
if err != nil {
|
|
// Disabled or logon-restricted accounts cannot get an S4U logon.
|
|
t.Logf("skipping %s: %v", name, err)
|
|
continue
|
|
}
|
|
viaSAM, err := localGroupsContainSID(name, adminSid)
|
|
require.NoError(t, err, "enumerate local groups for %s", name)
|
|
|
|
assert.Equal(t, viaSAM, viaToken, "S4U token and SAM enumeration must agree on Administrators membership for %s", name)
|
|
checked++
|
|
}
|
|
// Ineligible accounts are skipped, so without this the test could report
|
|
// success while comparing nothing at all.
|
|
require.Positive(t, checked, "no local account completed an S4U logon, so nothing was compared")
|
|
t.Logf("checked %d local accounts via S4U", checked)
|
|
}
|
|
|
|
// TestLocalGroupsContainSID_Administrator checks the positive case against the
|
|
// built-in Administrator, a member of Administrators on every installation.
|
|
func TestLocalGroupsContainSID_Administrator(t *testing.T) {
|
|
adminSid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
|
require.NoError(t, err, "create Administrators SID")
|
|
|
|
administrator := localAccountNameByRID(t, 500)
|
|
member, err := localGroupsContainSID(administrator, adminSid)
|
|
require.NoError(t, err, "enumerate local groups for %s", administrator)
|
|
assert.True(t, member, "%s is a member of the Administrators group", administrator)
|
|
}
|
|
|
|
// TestLocalGroupsContainSID_UnresolvableGroupFailsClosed covers a wanted SID
|
|
// that resolves to no group: the error must surface rather than being reported
|
|
// as "not a member", so the privilege check treats the account as privileged.
|
|
func TestLocalGroupsContainSID_UnresolvableGroupFailsClosed(t *testing.T) {
|
|
unknown := mustParseSID(t, "S-1-5-21-1111111111-2222222222-3333333333-4444")
|
|
|
|
_, err := localGroupsContainSID(localAccountNameByRID(t, 500), unknown)
|
|
require.Error(t, err, "must report an error when the wanted group cannot be identified")
|
|
}
|
|
|
|
func TestLocalGroupsContainSID_Guest(t *testing.T) {
|
|
guestsSid, err := windows.CreateWellKnownSid(windows.WinBuiltinGuestsSid)
|
|
require.NoError(t, err, "create Guests SID")
|
|
adminsSid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
|
require.NoError(t, err, "create Administrators SID")
|
|
|
|
guest := localAccountNameByRID(t, 501)
|
|
|
|
inGuests, err := localGroupsContainSID(guest, guestsSid)
|
|
require.NoError(t, err, "enumerate local groups for %s", guest)
|
|
assert.True(t, inGuests, "%s is a member of the Guests group", guest)
|
|
|
|
inAdmins, err := localGroupsContainSID(guest, adminsSid)
|
|
require.NoError(t, err, "enumerate local groups for %s", guest)
|
|
assert.False(t, inAdmins, "%s is not a member of the Administrators group", guest)
|
|
}
|