You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无需psexec.exe,如何从Windows服务以高权限注销用户?

解决方案:SYSTEM账户下的Windows Service实现用户会话注销(无外部工具依赖)

核心思路

服务运行在SYSTEM账户下,本身已具备足够权限,无需额外获取SYSTEM令牌(你原有代码中重复获取SYSTEM令牌属于冗余操作,且权限问题根源在于会话关联与API调用方式)。直接调用Windows原生API WTSLogoffSession 即可实现会话注销,这是最直接且高效的方案,无需依赖外部工具。

关键步骤

  1. 启用必要权限:SYSTEM账户默认拥有SeRemoteShutdownPrivilege,这是注销会话的必需权限,需先启用该权限。
  2. 枚举目标会话:通过WTSEnumerateSessions获取所有活跃用户会话,可根据需求筛选特定会话(如用户名、会话ID)。
  3. 执行注销操作:调用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
}

原有代码问题说明

  1. 冗余的SYSTEM令牌获取:服务本身已运行在SYSTEM账户,直接使用当前进程令牌即可,无需从winlogon/lsass进程获取,此操作会额外引入权限风险。
  2. 错误的权限选择:注销会话需要的是SeRemoteShutdownPrivilege,而非SeDebugPrivilege或SeImpersonatePrivilege。
  3. 绕路的实现方式:通过CreateProcessAsUser启动cmd执行注销命令属于间接方式,直接调用WTSLogoffSession才是原生、高效的正确路径。

内容的提问来源于stack exchange,提问作者John Carter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 09:58:11