feat: discover active Windows sessions
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
//go:build windows
|
||||
|
||||
package windows
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
wtsCurrentServerHandle windows.Handle = 0
|
||||
invalidSessionID = ^uint32(0)
|
||||
)
|
||||
|
||||
// DiscoverActiveSessions translates only verified active WTS user tokens into
|
||||
// selector DTOs. It is intended for the LocalSystem service: WTSQueryUserToken
|
||||
// requires LocalSystem and SeTcbPrivilege. A token-query failure leaves that
|
||||
// WTS session unusable rather than guessing an identity.
|
||||
func DiscoverActiveSessions() ([]SessionCandidate, error) {
|
||||
var sessions *windows.WTS_SESSION_INFO
|
||||
var count uint32
|
||||
if err := windows.WTSEnumerateSessions(wtsCurrentServerHandle, 0, 1, &sessions, &count); err != nil {
|
||||
return nil, fmt.Errorf("enumerate WTS sessions: %w", err)
|
||||
}
|
||||
if sessions == nil {
|
||||
return nil, nil
|
||||
}
|
||||
defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(sessions)))
|
||||
|
||||
console := windows.WTSGetActiveConsoleSessionId()
|
||||
result := make([]SessionCandidate, 0, count)
|
||||
for _, session := range unsafe.Slice(sessions, count) {
|
||||
if session.State != windows.WTSActive {
|
||||
continue
|
||||
}
|
||||
candidate, err := candidateFromSession(session.SessionID, console)
|
||||
if err == nil {
|
||||
result = append(result, candidate)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func candidateFromSession(sessionID, console uint32) (SessionCandidate, error) {
|
||||
var token windows.Token
|
||||
if err := windows.WTSQueryUserToken(sessionID, &token); err != nil {
|
||||
return SessionCandidate{}, fmt.Errorf("query user token for session %d: %w", sessionID, err)
|
||||
}
|
||||
defer token.Close()
|
||||
|
||||
user, err := token.GetTokenUser()
|
||||
if err != nil || user.User.Sid == nil {
|
||||
return SessionCandidate{}, fmt.Errorf("query token user for session %d: %w", sessionID, err)
|
||||
}
|
||||
logonSID, err := tokenLogonSID(token)
|
||||
if err != nil {
|
||||
return SessionCandidate{}, fmt.Errorf("query token logon SID for session %d: %w", sessionID, err)
|
||||
}
|
||||
facts := tokenFacts(token)
|
||||
if !facts.Usable {
|
||||
return SessionCandidate{}, fmt.Errorf("token for session %d is not usable", sessionID)
|
||||
}
|
||||
return SessionCandidate{
|
||||
SessionID: sessionID, Console: console != invalidSessionID && sessionID == console,
|
||||
UserSID: user.User.Sid.String(), LogonSID: logonSID, Token: facts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func tokenFacts(token windows.Token) TokenFacts {
|
||||
facts := TokenFacts{Usable: true}
|
||||
if token.IsElevated() {
|
||||
facts.FullAdministrator = true
|
||||
return facts
|
||||
}
|
||||
// WTSQueryUserToken normally returns the user's standard/filtered primary
|
||||
// token. The launch adapter re-verifies integrity and can create a
|
||||
// restricted medium token if a legacy full-only token is encountered.
|
||||
facts.StandardOrFiltered = true
|
||||
linked, err := token.GetLinkedToken()
|
||||
if err == nil {
|
||||
facts.LinkedFullAvailable = linked.IsElevated()
|
||||
_ = linked.Close()
|
||||
}
|
||||
return facts
|
||||
}
|
||||
|
||||
func tokenLogonSID(token windows.Token) (string, error) {
|
||||
groups, err := token.GetTokenGroups()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, group := range groups.AllGroups() {
|
||||
if group.Sid != nil && group.Attributes&windows.SE_GROUP_LOGON_ID == windows.SE_GROUP_LOGON_ID {
|
||||
return group.Sid.String(), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("token has no logon SID")
|
||||
}
|
||||
Reference in New Issue
Block a user