mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 09:01:29 +02:00
216 lines
7.5 KiB
Go
216 lines
7.5 KiB
Go
package elevate
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"slices"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const (
|
|
// fileDeleteChild is FILE_DELETE_CHILD, which x/sys does not define: the
|
|
// right to delete an entry of a directory without holding DELETE on it.
|
|
fileDeleteChild = 0x00000040
|
|
|
|
// accessAllowedCallbackACEType is an allow ACE with a condition appended to
|
|
// the ACCESS_ALLOWED_ACE layout, so its trustee is still at SidStart.
|
|
accessAllowedCallbackACEType = 0x9
|
|
|
|
// The allow ACE types that carry object GUIDs ahead of the trustee, so the
|
|
// SID is not at SidStart. They occur on directory-service objects rather
|
|
// than files, and are refused rather than skipped: see aceTrustee.
|
|
accessAllowedObjectACEType = 0x5
|
|
accessAllowedCallbackObjectACEType = 0xB
|
|
)
|
|
|
|
// fileWriteAccess are the rights that let a trustee rewrite or replace a file,
|
|
// or take it over and then do so.
|
|
const fileWriteAccess = windows.FILE_WRITE_DATA | windows.FILE_APPEND_DATA |
|
|
windows.DELETE | windows.WRITE_DAC | windows.WRITE_OWNER |
|
|
windows.GENERIC_WRITE | windows.GENERIC_ALL
|
|
|
|
// dirWriteAccess are the rights over a directory that let a trustee replace an
|
|
// entry somebody else owns. Creating a new entry is not one of them, which is
|
|
// what the Unix sticky bit says in one bit: the root of every volume grants
|
|
// BUILTIN\Users the right to add directories under it, and that reaches nothing
|
|
// already there.
|
|
const dirWriteAccess = fileDeleteChild | windows.DELETE |
|
|
windows.WRITE_DAC | windows.WRITE_OWNER | windows.GENERIC_ALL
|
|
|
|
// trustedInstallerSID owns much of what Windows itself installs. x/sys has no
|
|
// well-known constant for it.
|
|
const trustedInstallerSID = "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464"
|
|
|
|
// checkOnlyOwnerWritable reports an error unless path, and every directory
|
|
// leading to it, is owned by an account that can elevate (or by this user) and
|
|
// grants write access to nobody else. A writable directory is as good as a
|
|
// writable file, since an entry in it can be replaced, so the whole chain is
|
|
// checked.
|
|
func checkOnlyOwnerWritable(path string) error {
|
|
owners, err := trustedOwners()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writers, err := trustedWriters(owners)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
writeAccess := windows.ACCESS_MASK(fileWriteAccess)
|
|
for target := path; ; target = filepath.Dir(target) {
|
|
if err := checkSecurity(target, writeAccess, owners, writers); err != nil {
|
|
return err
|
|
}
|
|
if parent := filepath.Dir(target); parent == target {
|
|
return nil
|
|
}
|
|
writeAccess = dirWriteAccess
|
|
}
|
|
}
|
|
|
|
// trustedOwners are the accounts we accept as the owner of the executable and of
|
|
// the directories above it: the ones that can already answer the UAC prompt,
|
|
// plus this user, whose own executable is theirs to write. Code running as the
|
|
// user could prompt them for anything anyway; what matters is that no *other*
|
|
// unprivileged account can reach it.
|
|
func trustedOwners() ([]*windows.SID, error) {
|
|
self, err := currentUserSID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
owners := []*windows.SID{self}
|
|
for _, wellKnown := range []windows.WELL_KNOWN_SID_TYPE{
|
|
windows.WinLocalSystemSid,
|
|
windows.WinBuiltinAdministratorsSid,
|
|
} {
|
|
sid, err := windows.CreateWellKnownSid(wellKnown)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build well-known SID %d: %w", wellKnown, err)
|
|
}
|
|
owners = append(owners, sid)
|
|
}
|
|
|
|
installer, err := windows.StringToSid(trustedInstallerSID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse TrustedInstaller SID: %w", err)
|
|
}
|
|
return append(owners, installer), nil
|
|
}
|
|
|
|
// trustedWriters are the trustees whose write access does not widen who could
|
|
// decide what runs behind the prompt. The owners, and CREATOR OWNER, which
|
|
// resolves to the object's owner and is therefore already vetted.
|
|
func trustedWriters(owners []*windows.SID) ([]*windows.SID, error) {
|
|
creatorOwner, err := windows.CreateWellKnownSid(windows.WinCreatorOwnerSid)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build the CREATOR OWNER SID: %w", err)
|
|
}
|
|
return append(slices.Clone(owners), creatorOwner), nil
|
|
}
|
|
|
|
func checkSecurity(path string, writeAccess windows.ACCESS_MASK, owners, writers []*windows.SID) error {
|
|
sd, err := windows.GetNamedSecurityInfo(path, windows.SE_FILE_OBJECT,
|
|
windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION)
|
|
if err != nil {
|
|
return fmt.Errorf("read security descriptor of %s: %w", path, err)
|
|
}
|
|
|
|
owner, _, err := sd.Owner()
|
|
if err != nil {
|
|
return fmt.Errorf("read owner of %s: %w", path, err)
|
|
}
|
|
if !containsSID(owners, owner) {
|
|
return fmt.Errorf("%s is owned by %s, which is neither this user nor an account that can elevate", path, owner)
|
|
}
|
|
|
|
dacl, _, err := sd.DACL()
|
|
if err != nil {
|
|
return fmt.Errorf("read DACL of %s: %w", path, err)
|
|
}
|
|
// A NULL DACL grants everyone everything; only an absent security
|
|
// descriptor would have got us here without one, and neither is trustworthy.
|
|
if dacl == nil {
|
|
return fmt.Errorf("%s has no DACL, so it grants write access to everyone", path)
|
|
}
|
|
|
|
return checkDACL(path, dacl, writeAccess, writers)
|
|
}
|
|
|
|
// checkDACL refuses an ACL that grants write access to a trustee outside
|
|
// writers.
|
|
//
|
|
// An allowlist, because the trustees that must not have it cannot be listed: an
|
|
// ACE naming an ordinary user account hands that account the same power as one
|
|
// naming Everyone, and only the accounts that may hold it are knowable.
|
|
func checkDACL(path string, dacl *windows.ACL, writeAccess windows.ACCESS_MASK, writers []*windows.SID) error {
|
|
for i := uint32(0); i < uint32(dacl.AceCount); i++ {
|
|
var ace *windows.ACCESS_ALLOWED_ACE
|
|
if err := windows.GetAce(dacl, i, &ace); err != nil {
|
|
return fmt.Errorf("read ACE %d of %s: %w", i, path, err)
|
|
}
|
|
// An inherit-only ACE says what children of this object get, not what
|
|
// this object grants.
|
|
if ace.Header.AceFlags&windows.INHERIT_ONLY_ACE != 0 {
|
|
continue
|
|
}
|
|
if ace.Mask&writeAccess == 0 {
|
|
continue
|
|
}
|
|
// Only an allow ACE grants anything; a deny ACE narrows what one gave.
|
|
if !isAllowACE(ace.Header.AceType) {
|
|
continue
|
|
}
|
|
|
|
trustee, err := aceTrustee(ace)
|
|
if err != nil {
|
|
return fmt.Errorf("read the trustee of ACE %d of %s: %w", i, path, err)
|
|
}
|
|
if !containsSID(writers, trustee) {
|
|
return fmt.Errorf("%s grants write access to %s", path, trustee)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// isAllowACE reports whether an ACE type grants rights, rather than denying,
|
|
// auditing or labelling them.
|
|
func isAllowACE(aceType uint8) bool {
|
|
switch aceType {
|
|
case windows.ACCESS_ALLOWED_ACE_TYPE, accessAllowedCallbackACEType,
|
|
accessAllowedObjectACEType, accessAllowedCallbackObjectACEType:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// aceTrustee returns who an allow ACE grants its rights to. An ACE whose trustee
|
|
// cannot be located is an error rather than something to skip past: being unable
|
|
// to read who is being given write access is a refusal.
|
|
func aceTrustee(ace *windows.ACCESS_ALLOWED_ACE) (*windows.SID, error) {
|
|
switch ace.Header.AceType {
|
|
case windows.ACCESS_ALLOWED_ACE_TYPE, accessAllowedCallbackACEType:
|
|
//nolint:gosec // SidStart is the first uint32 of the variable-length SID that follows the ACE header.
|
|
return (*windows.SID)(unsafe.Pointer(&ace.SidStart)), nil
|
|
default:
|
|
return nil, errors.New("an object-type allow ACE does not carry its trustee where we can read it")
|
|
}
|
|
}
|
|
|
|
func containsSID(sids []*windows.SID, sid *windows.SID) bool {
|
|
return slices.ContainsFunc(sids, sid.Equals)
|
|
}
|
|
|
|
func currentUserSID() (*windows.SID, error) {
|
|
token := windows.GetCurrentProcessToken()
|
|
user, err := token.GetTokenUser()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read this process's user: %w", err)
|
|
}
|
|
return user.User.Sid, nil
|
|
}
|