//go:build unix package ispriv import ( `errors` `os` `os/user` `strconv` `strings` `github.com/shirou/gopsutil/v4/process` `golang.org/x/sys/unix` `r00t2.io/sysutils/envs` `r00t2.io/sysutils/paths` ) // GetEffective returns the EUID/EGID. func (p *ProcIDs) GetEffective() (euid, egid uint) { euid = p.uids.effective egid = p.gids.effective return } // GetFS returns the FSUID/FSGID. Not all platforms have this, in which case they'll be nil. func (p *ProcIDs) GetFS() (fsuid, fsgid *uint) { if p.uids.filesystem != nil { fsuid = new(uint) *fsuid = *p.uids.filesystem } if p.gids.filesystem != nil { fsgid = new(uint) *fsgid = *p.gids.filesystem } return } /* GetGids returms a set of a ProcIDs GIDs. fs will be nil if unsupported on the platform. If invoked with SGID, "savedSet" will be the SGID GID. */ func (p *ProcIDs) GetGids() (real, effective, savedSet uint, fs *uint) { real = p.gids.real effective = p.gids.effective savedSet = p.gids.savedSet if p.gids.filesystem != nil { fs = new(uint) *fs = *p.gids.filesystem } return } // GetReal returns the (R)UID/(R)GID. func (p *ProcIDs) GetReal() (ruid, rgid uint) { ruid = p.uids.real rgid = p.gids.real return } // GetSaved returns the SUID/SGID. func (p *ProcIDs) GetSaved() (suid, sgid uint) { suid = p.uids.savedSet sgid = p.gids.savedSet return } /* GetUids returms a set of a ProcIDs UIDs. fs will be nil if unsupported on the platform. If invoked with SUID, "savedSet" will be the SUID UID. */ func (p *ProcIDs) GetUids() (real, effective, savedSet uint, fs *uint) { real = p.uids.real effective = p.uids.effective savedSet = p.uids.savedSet if p.uids.filesystem != nil { fs = new(uint) *fs = *p.uids.filesystem } return } /* IsSGID returns true if the process is Set GID/SGID. Note that it will return false if invoked by a group with the same GID as an SGID that's set. */ func (p *ProcIDs) IsSGID() (isSgid bool) { isSgid = p.gids.real != p.gids.savedSet return } /* IsSUID returns true if the process is Set UID/SUID. Note that it will return false if invoked by a user with the same UID as an SUID that's set. */ func (p *ProcIDs) IsSUID() (isSuid bool) { isSuid = p.uids.real != p.uids.savedSet return } /* IsSudo does a very fast (and potentially inaccurate) evaluation of whether the process is running under sudo. DO NOT use this function for security-sensitive uses, fully accurate results, or critical implementations! Use IsSudoWithConfidence instead for those cases. IsSudo only does the most basic of checking, which can be easily and completely overridden by a non-privileged user. */ func (p *ProcIDs) IsSudo() (isSudo bool) { // This is how every other Joe Blow does this. It's an extremely dumb way to do it. The caller has been warned. for k, _ := range envs.GetEnvMap() { if strings.HasPrefix(k, sudoEnvPfx) { isSudo = true return } } return } /* IsSudoDetailed returns true for a very fast evaluation of whether the process is running under sudo, and information about that context. (If isSudo is false, originalUid/originalGid will both be -1 and originalUser will be nil.) DO NOT use this function for security-sensitive uses, fully accurate results, or critical implementations! Use IsSudoWithConfidenceDetailed instead for those cases. IsSudoDetailed only does the most basic of checking, which can be easily and completely overridden by a non-privileged user. */ func (p *ProcIDs) IsSudoDetailed() (isSudo bool, originalUid, originalGid int, originalUser *user.User, err error) { if originalUid, originalGid, originalUser, err = p.getSudoInfoEnv(); err != nil { return } if originalUid >= 0 || originalGid >= 0 || originalUser != nil { isSudo = true } return } /* IsSudoWithConfidence is like IsSudo, but is *much* more throrough. It not only returns isSudo, which is true if *any* indicators pass, but also: * a confidence value (which indicates *how many* indicators *passed*) * a maxConfidence value (which indicates how many indicators were *tested*) * a score value (which is a float indicating overall confidence on a fixed and weighted scale; higher is more confident, 1.0 indicates 100% confidence) */ func (p *ProcIDs) IsSudoWithConfidence() (isSudo bool, confidence, maxConfidence uint, score float64, err error) { // confidence/maxConfidence are not used directly; they're unweighted counters. var scoreConf uint var scoreMaxConf uint score = float64(scoreConf) / float64(scoreMaxConf) return } /* IsSudoWithConfidenceDetailed is like IsSudoDetailed, but is *much* more throrough. It not only returns the same results as IsSudoDetailed, but includes the same scoring values/system as IsSudoWithConfidence. */ func (p *ProcIDs) IsSudoWithConfidenceDetailed() (isSudo bool, confidence, maxConfidence uint, score float64, originalUid, originalGid int, originalUser *user.User, err error) { var b []byte var ok bool var permErr bool var envUid int var envGid int var scoreConf uint var scoreMaxConf uint var curUser *user.User var envUser *user.User var curUid uint64 var fstat unix.Stat_t var fsUid int var procFiles []process.OpenFilesStat var loginUidFile string = curLoginUidFile if curUser, err = user.Current(); err != nil { return } if curUid, err = strconv.ParseUint(curUser.Uid, 10, 32); err != nil { return } if procFiles, err = p.proc.OpenFiles(); err != nil { return } // Env vars; only score 1x/each. maxConfidence += 3 scoreMaxConf += 3 if envUid, envGid, envUser, err = p.getSudoInfoEnv(); err != nil { return } originalUid, originalGid, originalUser = envUid, envGid, envUser if envUid >= 0 { confidence++ scoreConf++ } if envGid >= 0 { confidence++ scoreConf++ } if envUser != nil { confidence++ scoreConf++ } /* TTY/PTY ownership. We (can) only check this if we're running in an interactive session. Typically this is done via (golang.org/x/term).IsTerminal(), That pulls in a bunch of stuff I don't need, though, so I'll just replicate (...).IsTerminal() here; it's just a wrapped single function call. */ // procFiles[0] is always STDIN. Whether it's a pipe, or TTY/PTY, or file, etc. // (likewise, procFiles[1] is always STDOUT, procFiles[2] is always STDERR); however... if _, err = unix.IoctlGetTermios(int(procFiles[0].Fd), unix.TCGETS); err == nil { // Interactive maxConfidence++ // This is only worth 2. It's pretty hard to fake unless origin user is root, // but it's ALSO usually set to the target user. scoreMaxConf += 2 fstat = unix.Stat_t{} if err = unix.Fstat(int(procFiles[0].Fd), &fstat); err != nil { return } if uint64(fstat.Uid) != curUid { // This is a... *potential* indicator, if a lateral sudo was done (user1 => user2), // or root used sudo to *drop* privs to a regular user. // We mark it as a pass for confidence since it IS a terminal, and it's permission-related. confidence++ scoreConf += 2 originalUid = int(fstat.Uid) } } else { // err is OK; just means non-interactive. No counter or score/max score increase; basically a NO-OP. err = nil } // /proc/self/loginuid // This is a REALLY good indicator. Probably the strongest next to reverse-walking the proc tree. It depends on PAM and auditd support, I think, // BUT if it's present it's *really* really strong. if ok, err = paths.RealPathExists(&loginUidFile); err != nil { return } if ok { maxConfidence++ scoreMaxConf += 5 if b, err = os.ReadFile(loginUidFile); err != nil { return } if fsUid, err = strconv.Atoi(strings.TrimSpace(string(b))); err != nil { return } if uint64(fsUid) != curUid { confidence++ scoreConf += 5 originalUid = fsUid } } // proc tree reverse walking. // This is, by far, the most reliable method. // There are some valid conditions in which this would fail due to permissions // (e.g. lateral sudo: user1 => user2), but if it's a permission error it's *probably* // a lateral move anyways. if isSudo, permErr, originalUid, originalGid, originalUser, err = p.revProcWalk(); err != nil { return } maxConfidence++ scoreMaxConf += 10 if permErr { confidence++ scoreConf += 5 } else if isSudo { confidence++ scoreConf += 10 } score = float64(scoreConf) / float64(scoreMaxConf) return } /* getSudoInfoEnv returns env var driven sudo information. These are in no way guaranteed to be accurate as the user can remove or override them. */ func (p *ProcIDs) getSudoInfoEnv() (uid, gid int, u *user.User, err error) { var ok bool var val string var envMap map[string]string = envs.GetEnvMap() uid = -1 gid = -1 if val, ok = envMap[sudoUnameEnv]; ok { if u, err = user.Lookup(val); err != nil { return } } if val, ok = envMap[sudoUidEnv]; ok { if uid, err = strconv.Atoi(val); err != nil { return } } if val, ok = envMap[sudoGidEnv]; ok { if gid, err = strconv.Atoi(val); err != nil { return } } return } /* revProcWalk walks up the process tree ("proctree") until it either: * finds a process invoked with sudo (true) * hits PID == 1 (false) * hits a permission error (true-ish) */ func (p *ProcIDs) revProcWalk() (sudoFound, isPermErr bool, origUid, origGid int, origUser *user.User, err error) { var cmd []string var parent *ProcIDs var parentPid int32 var parentUname string var parentUids []uint32 var parentGids []uint32 origUid = -1 origGid = -1 parent = p for { if parent == nil || parent.proc.Pid == 1 { break } if cmd, err = parent.proc.CmdlineSlice(); err != nil { if errors.Is(err, os.ErrPermission) { isPermErr = true err = nil } return } if cmd[0] == "sudo" { sudoFound = true if parentUname, err = parent.proc.Username(); err != nil { if errors.Is(err, os.ErrPermission) { isPermErr = true err = nil } return } if parentUids, err = parent.proc.Uids(); err != nil { if errors.Is(err, os.ErrPermission) { isPermErr = true err = nil } return } if parentGids, err = parent.proc.Gids(); err != nil { if errors.Is(err, os.ErrPermission) { isPermErr = true err = nil } return } if origUser, err = user.Lookup(parentUname); err != nil { return } origUid = int(parentUids[0]) origGid = int(parentGids[0]) } if sudoFound { break } if parentPid, err = parent.proc.Ppid(); err != nil { if errors.Is(err, os.ErrPermission) { isPermErr = true err = nil } return } if parent, err = GetProcIDs(parentPid); err != nil { if errors.Is(err, os.ErrPermission) { isPermErr = true err = nil } return } } return }