101 lines
3.1 KiB
Go
101 lines
3.1 KiB
Go
//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")
|
|
}
|