bastion 0.53.2
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s

This commit is contained in:
Privatae
2026-10-20 04:42:14 +00:00
commit e4c9b69b46
471 changed files with 159794 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
package main
// The decisions behind the Windows drop (freeagent_drop_windows.go), kept in an
// untagged file so the suite checks them on every platform. The Windows calls
// that gather their inputs are in the tagged file.
import (
"sort"
"strconv"
"strings"
"unicode/utf16"
)
// winSidTypeUser is SidTypeUser: the SID names a person's account rather than
// a group, an alias or a well-known identity.
const winSidTypeUser = 1
// faWinOwnerRefusal says why a Windows account may NOT run a folder's agent,
// or "" when it may. It is the Windows reading of the unix rule (root, nobody,
// no account): the agent runs as the person who owns the folder, never as one
// of the machine's own identities, a group, or an account anyone can use.
//
// - SYSTEM, LOCAL SERVICE, NETWORK SERVICE, the service and virtual accounts
// (S-1-5-80, -82, -83, -90, -96) and the BUILTIN groups (S-1-5-32, which is
// where Administrators is, and the usual owner of a folder an elevated
// administrator made) are the machine's identities, not a person.
// - Only a user account issued by the machine or its domain (S-1-5-21-) or by
// Microsoft Entra ID (S-1-12-1-) is a person.
// - The built-in Administrator (RID 500) runs with full rights and no UAC
// split by default, so an agent under its token would be the machine's
// administrator: the root case. The built-in Guest (RID 501) is shared.
// Every RID below 1000 is reserved for a built-in account the machine or
// its domain made (krbtgt 502, DefaultAccount 503, WDAGUtilityAccount 504,
// and the rest); a person's account is issued a RID of 1000 or more. None
// of the reserved ones is a person, so all are refused.
func faWinOwnerRefusal(sid string, sidType uint32, name string) string {
grant := " — grant a folder owned by the user who should read it"
switch sid {
case "S-1-5-18":
return "refusing to run the agent as SYSTEM: the granted folder is owned by the system" + grant
case "S-1-5-19", "S-1-5-20":
return "refusing to run the agent as a service account: the granted folder is owned by " + name + grant
}
for _, p := range []string{"S-1-5-32-", "S-1-5-80-", "S-1-5-82-", "S-1-5-83-", "S-1-5-90-", "S-1-5-96-"} {
if strings.HasPrefix(sid, p) {
return "refusing to run the agent as " + name + ": the granted folder is owned by one of the machine's own identities, not a person" + grant
}
}
person := strings.HasPrefix(sid, "S-1-5-21-") || strings.HasPrefix(sid, "S-1-12-1-")
if !person || sidType != winSidTypeUser {
return "refusing to run the agent as " + name + ": the granted folder's owner is not a person's account" + grant
}
if strings.HasPrefix(sid, "S-1-5-21-") {
if rid, err := strconv.Atoi(sid[strings.LastIndex(sid, "-")+1:]); err == nil && rid < 1000 {
switch rid {
case 500:
return "refusing to run the agent as the built-in Administrator: it runs with the machine's full rights" + grant
case 501:
return "refusing to run the agent as the built-in Guest: anyone may use it" + grant
default:
return "refusing to run the agent as " + name + ": the granted folder is owned by a built-in account, not a person" + grant
}
}
}
return ""
}
// winJoin joins Windows path parts with a backslash, whatever platform this
// runs on, so the environment below is tested off Windows.
func winJoin(parts ...string) string {
out := strings.TrimRight(parts[0], `\`)
for _, p := range parts[1:] {
out += `\` + strings.Trim(p, `\`)
}
return out
}
// faWinDropEnv is what a Windows child is told about who it is, and which of
// the service's variables it keeps. The service runs as LocalSystem, so its
// USERPROFILE, APPDATA, LOCALAPPDATA and TEMP all name LocalSystem's profile,
// which the owner cannot open; they are replaced with the owner's own. A
// Python child on Windows also needs SystemRoot to open a socket, so that is
// kept, with the other machine-wide variables that say nothing about the
// account.
func faWinDropEnv(user, profile, stateDir string) (set []string, passthrough []string) {
home := profile
temp := stateDir
if profile != "" {
temp = winJoin(profile, `AppData\Local\Temp`)
} else {
home = stateDir
}
set = []string{"USERNAME=" + user, "HOME=" + home, "USERPROFILE=" + home, "TEMP=" + temp, "TMP=" + temp}
if profile != "" {
set = append(set, "APPDATA="+winJoin(profile, `AppData\Roaming`), "LOCALAPPDATA="+winJoin(profile, `AppData\Local`))
}
passthrough = []string{"PATH", "SystemRoot", "SYSTEMROOT", "windir", "WINDIR", "ComSpec", "PATHEXT",
"PROCESSOR_ARCHITECTURE", "NUMBER_OF_PROCESSORS", "LANG", "LC_ALL"}
return set, passthrough
}
// faWinEnvBlock is the environment block CreateProcessAsUser takes: each
// NAME=value NUL-terminated, the block ended by one more NUL, sorted by name
// without regard to case as Windows requires. A later duplicate of a name
// (in any case) wins, and the drive-letter entries that begin with '=' are
// left out.
func faWinEnvBlock(env []string) []uint16 {
byName := map[string]string{}
for _, kv := range env {
i := strings.IndexByte(kv, '=')
if i <= 0 {
continue
}
byName[strings.ToUpper(kv[:i])] = kv
}
keys := make([]string, 0, len(byName))
for k := range byName {
keys = append(keys, k)
}
sort.Strings(keys)
var out []uint16
for _, k := range keys {
out = append(out, utf16.Encode([]rune(byName[k]))...)
out = append(out, 0)
}
if len(out) == 0 {
out = append(out, 0)
}
return append(out, 0)
}