无需psexec.exe,如何从Windows服务以高权限注销用户?
解决方案:SYSTEM账户下的Windows Service实现用户会话注销(无外部工具依赖)
核心思路
服务运行在SYSTEM账户下,本身已具备足够权限,无需额外获取SYSTEM令牌(你原有代码中重复获取SYSTEM令牌属于冗余操作,且权限问题根源在于会话关联与API调用方式)。直接调用Windows原生API WTSLogoffSession 即可实现会话注销,这是最直接且高效的方案,无需依赖外部工具。
关键步骤
- 启用必要权限:SYSTEM账户默认拥有
SeRemoteShutdownPrivilege,这是注销会话的必需权限,需先启用该权限。 - 枚举目标会话:通过
WTSEnumerateSessions获取所有活跃用户会话,可根据需求筛选特定会话(如用户名、会话ID)。 - 执行注销操作:调用
WTSLogoffSession注销指定会话。
完整Go代码实现
package main import ( "fmt" "log" "syscall" "unsafe" "golang.org/x/sys/windows" ) const ( SE_REMOTE_SHUTDOWN_NAME = "SeRemoteShutdownPrivilege" WTS_CURRENT_SERVER_HANDLE = 0 WTS_SESSIONSTATE_ACTIVE = 0 ) type WTS_SESSION_INFO struct { SessionID uint32 pWinStationName *uint16 State uint32 } func main() { // 启用当前进程的SeRemoteShutdownPrivilege权限 if err := enableCurrentProcessPrivilege(SE_REMOTE_SHUTDOWN_NAME); err != nil { log.Fatalf("启用权限失败: %v", err) } // 枚举所有活跃会话 sessions, err := enumerateActiveSessions() if err != nil { log.Fatalf("枚举会话失败: %v", err) } // 注销每个活跃会话(可根据需求筛选特定会话,比如用户名) for _, session := range sessions { log.Printf("正在注销会话ID: %d", session.SessionID) if err := logoffSession(session.SessionID); err != nil { log.Printf("注销会话ID %d失败: %v", session.SessionID, err) } else { log.Printf("会话ID %d注销成功", session.SessionID) } } } // 启用当前进程的指定权限 func enableCurrentProcessPrivilege(privilegeName string) error { var token windows.Token err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token) if err != nil { return fmt.Errorf("打开进程令牌失败: %v", err) } defer token.Close() var luid windows.LUID privNamePtr, err := syscall.UTF16PtrFromString(privilegeName) if err != nil { return fmt.Errorf("转换权限名称失败: %v", err) } err = windows.LookupPrivilegeValue(nil, privNamePtr, &luid) if err != nil { return fmt.Errorf("查找权限值失败: %v", err) } tp := windows.Tokenprivileges{ PrivilegeCount: 1, Privileges: [1]windows.LUIDAndAttributes{ {Luid: luid, Attributes: windows.SE_PRIVILEGE_ENABLED}, }, } err = windows.AdjustTokenPrivileges(token, false, &tp, 0, nil, nil) if err != nil { return fmt.Errorf("调整权限失败: %v", err) } return nil } // 枚举所有活跃的用户会话 func enumerateActiveSessions() ([]WTS_SESSION_INFO, error) { var pSessionInfo *WTS_SESSION_INFO var count uint32 var bytesReturned uint32 procWTSEnumerateSessions := windows.NewLazyDLL("wtsapi32.dll").NewProc("WTSEnumerateSessionsW") ret, _, err := procWTSEnumerateSessions.Call( uintptr(WTS_CURRENT_SERVER_HANDLE), 0, 1, uintptr(unsafe.Pointer(&pSessionInfo)), uintptr(unsafe.Pointer(&count)), ) if ret == 0 { return nil, fmt.Errorf("WTSEnumerateSessions调用失败: %v", err) } defer func() { procWTSFreeMemory := windows.NewLazyDLL("wtsapi32.dll").NewProc("WTSFreeMemory") procWTSFreeMemory.Call(uintptr(unsafe.Pointer(pSessionInfo))) }() sessions := make([]WTS_SESSION_INFO, count) for i := uint32(0); i < count; i++ { sessions[i] = (*(*[1 << 16]WTS_SESSION_INFO)(unsafe.Pointer(pSessionInfo)))[i] } // 过滤掉非活跃会话 activeSessions := []WTS_SESSION_INFO{} for _, s := range sessions { if s.State == WTS_SESSIONSTATE_ACTIVE { activeSessions = append(activeSessions, s) } } return activeSessions, nil } // 注销指定会话ID func logoffSession(sessionID uint32) error { procWTSLogoffSession := windows.NewLazyDLL("wtsapi32.dll").NewProc("WTSLogoffSessionW") ret, _, err := procWTSLogoffSession.Call( uintptr(WTS_CURRENT_SERVER_HANDLE), uintptr(sessionID), 0, // 0表示强制注销,1表示等待用户进程结束 ) if ret == 0 { return fmt.Errorf("WTSLogoffSession调用失败: %v", err) } return nil }
原有代码问题说明
- 冗余的SYSTEM令牌获取:服务本身已运行在SYSTEM账户,直接使用当前进程令牌即可,无需从winlogon/lsass进程获取,此操作会额外引入权限风险。
- 错误的权限选择:注销会话需要的是
SeRemoteShutdownPrivilege,而非SeDebugPrivilege或SeImpersonatePrivilege。 - 绕路的实现方式:通过
CreateProcessAsUser启动cmd执行注销命令属于间接方式,直接调用WTSLogoffSession才是原生、高效的正确路径。
内容的提问来源于stack exchange,提问作者John Carter
相关产品推荐
相关产品推荐

