v1.13.1
ADDED: * ispriv, which returns some information useful for determining if running with extra permissions, in sudo, etc.
This commit is contained in:
14
ispriv/consts_nix.go
Normal file
14
ispriv/consts_nix.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
const (
|
||||
sudoEnvPfx string = "SUDO_"
|
||||
sudoUidEnv string = sudoEnvPfx + "UID"
|
||||
sudoGidEnv string = sudoEnvPfx + "GID"
|
||||
sudoUnameEnv string = sudoEnvPfx + "USER"
|
||||
)
|
||||
|
||||
const (
|
||||
curLoginUidFile string = "/proc/self/loginuid"
|
||||
)
|
||||
7
ispriv/doc_nix.go
Normal file
7
ispriv/doc_nix.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build unix
|
||||
|
||||
/*
|
||||
ispriv provides functions and a method to determine if a process is being run SUID/SGID, under sudo, etc.
|
||||
*/
|
||||
|
||||
package ispriv
|
||||
7
ispriv/doc_windows.go
Normal file
7
ispriv/doc_windows.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build windows
|
||||
|
||||
/*
|
||||
ispriv provides functions on Windows to determine the currentl privilege status.
|
||||
*/
|
||||
|
||||
package ispriv
|
||||
68
ispriv/funcs_nix.go
Normal file
68
ispriv/funcs_nix.go
Normal file
@@ -0,0 +1,68 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`os`
|
||||
|
||||
`github.com/shirou/gopsutil/v4/process`
|
||||
)
|
||||
|
||||
/*
|
||||
GetProcIDs returns a ProcIDs from a given PID. An error will be raised if the process ID doesn't exist.
|
||||
A negative value indicates "self" (see also GetProcIDsSelf).
|
||||
|
||||
Note that if you are not EUID == 0 (root) or you/the sudo target user does not own the process,
|
||||
the returning ProcIDs is HIGHLY LIKELY to be very inaccurate.
|
||||
*/
|
||||
func GetProcIDs(pid int32) (p *ProcIDs, err error) {
|
||||
|
||||
var proc ProcIDs
|
||||
var ids []uint32
|
||||
|
||||
if pid < 0 {
|
||||
pid = int32(os.Getpid())
|
||||
}
|
||||
|
||||
if proc.proc, err = process.NewProcess(pid); err != nil {
|
||||
return
|
||||
}
|
||||
if ids, err = proc.proc.Gids(); err != nil {
|
||||
return
|
||||
}
|
||||
p.gids = &IdInfo{
|
||||
real: uint(ids[0]),
|
||||
effective: uint(ids[1]),
|
||||
savedSet: uint(ids[2]),
|
||||
filesystem: nil,
|
||||
}
|
||||
if len(ids) == 4 {
|
||||
p.gids.filesystem = new(uint)
|
||||
*p.gids.filesystem = uint(ids[3])
|
||||
}
|
||||
if ids, err = proc.proc.Uids(); err != nil {
|
||||
return
|
||||
}
|
||||
p.uids = &IdInfo{
|
||||
real: uint(ids[0]),
|
||||
effective: uint(ids[1]),
|
||||
savedSet: uint(ids[2]),
|
||||
filesystem: nil,
|
||||
}
|
||||
if len(ids) == 4 {
|
||||
p.uids.filesystem = new(uint)
|
||||
*p.uids.filesystem = uint(ids[3])
|
||||
}
|
||||
|
||||
p = &proc
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetProcIDsSelf returns a ProcIDs from the current process' PID.
|
||||
func GetProcIDsSelf() (p *ProcIDs, err error) {
|
||||
|
||||
p, err = GetProcIDs(int32(os.Getpid()))
|
||||
|
||||
return
|
||||
}
|
||||
426
ispriv/funcs_procids_nix.go
Normal file
426
ispriv/funcs_procids_nix.go
Normal file
@@ -0,0 +1,426 @@
|
||||
//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
|
||||
}
|
||||
60
ispriv/funcs_windows.go
Normal file
60
ispriv/funcs_windows.go
Normal file
@@ -0,0 +1,60 @@
|
||||
//go:build windows
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`golang.org/x/sys/windows`
|
||||
)
|
||||
|
||||
// IsAdmin returns true if currently running with Administrator privileges.
|
||||
func IsAdmin() (admin bool, err error) {
|
||||
|
||||
var sid *windows.SID
|
||||
var tok windows.Token
|
||||
|
||||
if err = windows.AllocateAndInitializeSid(
|
||||
&windows.SECURITY_NT_AUTHORITY, // identAuth
|
||||
2, // subAuth
|
||||
windows.SECURITY_BUILTIN_DOMAIN_RID, // subAuth0
|
||||
windows.DOMAIN_ALIAS_RID_ADMINS, // subAuth1
|
||||
0, 0, 0, 0, 0, 0, // subAuth2-10
|
||||
&sid, // sid
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
defer windows.FreeSid(sid)
|
||||
|
||||
tok = windows.Token(0)
|
||||
if admin, err = tok.IsMember(sid); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsElevated returns true if running in an elevated ("Run as Administrator") context.
|
||||
func IsElevated() (elevated bool) {
|
||||
|
||||
var tok windows.Token = windows.Token(0)
|
||||
|
||||
elevated = tok.IsElevated()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsPrivileged indicates that the current security context is running both
|
||||
with Administrator priviliges AND is elevated.
|
||||
*/
|
||||
func IsPrivileged() (privileged bool, err error) {
|
||||
|
||||
if privileged, err = IsAdmin(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if privileged {
|
||||
privileged = IsElevated()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
19
ispriv/types_nix.go
Normal file
19
ispriv/types_nix.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`github.com/shirou/gopsutil/v4/process`
|
||||
)
|
||||
|
||||
type ProcIDs struct {
|
||||
proc *process.Process
|
||||
uids *IdInfo
|
||||
gids *IdInfo
|
||||
}
|
||||
type IdInfo struct {
|
||||
real uint
|
||||
effective uint
|
||||
savedSet uint
|
||||
filesystem *uint
|
||||
}
|
||||
Reference in New Issue
Block a user