feat: discover active Windows sessions
This commit is contained in:
@@ -805,6 +805,14 @@ Guest Control only with `--passwordfile`. The current VirtualBox `7.2.16`
|
|||||||
Guest Control build supports `--wait-stdout` and `--wait-stderr`, but not
|
Guest Control build supports `--wait-stdout` and `--wait-stderr`, but not
|
||||||
`--wait-exit`; adapters must not pass that unsupported flag.
|
`--wait-exit`; adapters must not pass that unsupported flag.
|
||||||
|
|
||||||
|
For a guest `cmd.exe` command payload, this Guest Control build requires
|
||||||
|
`--unquoted-args` so the controller preserves Windows backslashes and the
|
||||||
|
single `/c` command payload. Treat each Guest Control process as a bounded,
|
||||||
|
owned resource: record its guest session/PID, close it after completion, and
|
||||||
|
use `closeprocess` before reset if an output wait does not complete. Do not use
|
||||||
|
this compatibility rule to assemble untrusted command text; it is solely the
|
||||||
|
adapter transport for prevalidated test commands and exact executable paths.
|
||||||
|
|
||||||
Use a local, non-secret environment description when operating the lane. The
|
Use a local, non-secret environment description when operating the lane. The
|
||||||
host alias must resolve through the operator's SSH config; another controller
|
host alias must resolve through the operator's SSH config; another controller
|
||||||
may substitute an equivalent target, but must record the resulting host/VM
|
may substitute an equivalent target, but must record the resulting host/VM
|
||||||
|
|||||||
@@ -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