From 7449b6439826bb857c96fbab44abc03a355b371e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Gr=C3=BCssing?= Date: Wed, 17 Jun 2026 09:51:40 +0200 Subject: [PATCH] Fix: RDP Consent-Dialog via WTSQueryUserToken+CreateProcessAsUser (v2.4.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SessionSpawner.cs: Neuer Helper — WTSQueryUserToken + CreateProcessAsUser ersetzt fragilen schtasks-Ansatz für UI-Spawn aus SYSTEM-Service - RtcService: SpawnConsentHelper + SpawnCaptureHelper nutzen jetzt SessionSpawner - Findet aktive Console-Session automatisch, Fallback auf alle Sessions - Version: 2.3.0 → 2.4.0 Co-Authored-By: Claude Sonnet 4.6 --- agent-cs/AgentWorker.cs | 2 +- agent-cs/Services/RtcService.cs | 104 +------------- agent-cs/Services/SessionSpawner.cs | 160 +++++++++++++++++++++ agent-cs/setup.iss | 2 +- frontend/src/pages/PatchManagementPage.jsx | 2 +- 5 files changed, 167 insertions(+), 103 deletions(-) create mode 100644 agent-cs/Services/SessionSpawner.cs diff --git a/agent-cs/AgentWorker.cs b/agent-cs/AgentWorker.cs index 736529b..70845fa 100644 --- a/agent-cs/AgentWorker.cs +++ b/agent-cs/AgentWorker.cs @@ -6,7 +6,7 @@ namespace ITNexusAgent; public class AgentWorker { - private const string Version = "2.3.0"; + private const string Version = "2.4.0"; private const string DataDir = @"C:\ProgramData\IT Nexus Agent"; private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json"; private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json"; diff --git a/agent-cs/Services/RtcService.cs b/agent-cs/Services/RtcService.cs index e6172e5..356c630 100644 --- a/agent-cs/Services/RtcService.cs +++ b/agent-cs/Services/RtcService.cs @@ -141,55 +141,8 @@ public class RtcService private static bool SpawnConsentHelper(string exePath, string portStr) { - try - { - var fullUser = NotificationService.GetLoggedOnUser(); - if (string.IsNullOrEmpty(fullUser)) - { - AgentWorker.Log("RDP: Kein eingeloggter User für Consent"); - return false; - } - - var taskName = $"ITNexus-RDPConsent-{portStr}"; - Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f") - { CreateNoWindow = true })?.WaitForExit(); - - var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss"); - var ruArg = fullUser.StartsWith("AzureAD\\", StringComparison.OrdinalIgnoreCase) - ? "/ru \"INTERACTIVE\"" - : $"/ru \"{fullUser}\""; - - var args = $"/create /tn \"{taskName}\" /tr \"\\\"{exePath}\\\" --rdp-consent {portStr}\" " + - $"/sc ONCE /st {triggerTime} {ruArg} /it /f"; - - var p = Process.Start(new ProcessStartInfo("schtasks.exe", args) - { CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false }); - p?.WaitForExit(); - - if (p?.ExitCode != 0) - { - AgentWorker.Log($"RDP: schtasks consent fehlgeschlagen (ExitCode={p?.ExitCode})"); - return false; - } - - Process.Start(new ProcessStartInfo("schtasks.exe", $"/run /tn \"{taskName}\"") - { CreateNoWindow = true })?.WaitForExit(); - - _ = Task.Run(async () => - { - await Task.Delay(40000); - Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f") - { CreateNoWindow = true })?.WaitForExit(); - }); - - AgentWorker.Log($"RDP: Consent-Helper gestartet als '{fullUser}'"); - return true; - } - catch (Exception ex) - { - AgentWorker.Log($"RDP: SpawnConsentHelper Fehler: {ex.Message}"); - return false; - } + var pid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-consent {portStr}"); + return pid > 0; } private async Task CapturePipeLoopAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct) @@ -273,56 +226,7 @@ public class RtcService private static bool SpawnCaptureHelper(string exePath, string portStr, int screenIdx = 0) { - try - { - var fullUser = NotificationService.GetLoggedOnUser(); - if (string.IsNullOrEmpty(fullUser)) - { - AgentWorker.Log("RDP: Kein eingeloggter User gefunden"); - return false; - } - - var taskName = $"ITNexus-RDP-{portStr}"; - - Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f") - { CreateNoWindow = true })?.WaitForExit(); - - var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss"); - var ruArg = fullUser.StartsWith("AzureAD\\", StringComparison.OrdinalIgnoreCase) - ? "/ru \"INTERACTIVE\"" - : $"/ru \"{fullUser}\""; - - var args = $"/create /tn \"{taskName}\" /tr \"\\\"{exePath}\\\" --rdp-capture {portStr} {screenIdx}\" " + - $"/sc ONCE /st {triggerTime} {ruArg} /it /f"; - - var p = Process.Start(new ProcessStartInfo("schtasks.exe", args) - { CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false }); - p?.WaitForExit(); - - if (p?.ExitCode != 0) - { - AgentWorker.Log($"RDP: schtasks create fehlgeschlagen (ExitCode={p?.ExitCode})"); - return false; - } - - Process.Start(new ProcessStartInfo("schtasks.exe", $"/run /tn \"{taskName}\"") - { CreateNoWindow = true })?.WaitForExit(); - - // Aufräumen nach kurzer Zeit - _ = Task.Run(async () => - { - await Task.Delay(5000); - Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f") - { CreateNoWindow = true })?.WaitForExit(); - }); - - AgentWorker.Log($"RDP: Helper gestartet als '{fullUser}' (Task: {taskName})"); - return true; - } - catch (Exception ex) - { - AgentWorker.Log($"RDP: SpawnHelper Fehler: {ex.Message}"); - return false; - } + var pid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-capture {portStr} {screenIdx}"); + return pid > 0; } } diff --git a/agent-cs/Services/SessionSpawner.cs b/agent-cs/Services/SessionSpawner.cs new file mode 100644 index 0000000..b319803 --- /dev/null +++ b/agent-cs/Services/SessionSpawner.cs @@ -0,0 +1,160 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace ITNexusAgent.Services; + +// Startet Prozesse in der aktiven User-Session (aus SYSTEM-Service heraus) +// Korrekte Win32-Methode: WTSQueryUserToken + CreateProcessAsUser +public static class SessionSpawner +{ + #region Win32 P/Invoke + + [DllImport("kernel32.dll")] static extern uint WTSGetActiveConsoleSessionId(); + + [DllImport("Wtsapi32.dll", SetLastError = true)] + static extern bool WTSQueryUserToken(uint sessionId, out IntPtr phToken); + + [DllImport("Wtsapi32.dll", SetLastError = true)] + static extern bool WTSEnumerateSessions(IntPtr hServer, uint reserved, uint version, + out IntPtr ppSessionInfo, out uint pCount); + + [DllImport("Wtsapi32.dll")] static extern void WTSFreeMemory(IntPtr pMemory); + + [DllImport("advapi32.dll", SetLastError = true)] + static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, + IntPtr lpTokenAttributes, int impersonationLevel, int tokenType, out IntPtr phNewToken); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + 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); + + [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("kernel32.dll", SetLastError = true)] + static extern bool CloseHandle(IntPtr hObject); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + struct STARTUPINFO + { + public int cb; public string? lpReserved; public string? lpDesktop; public string? lpTitle; + public uint dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute, dwFlags; + public ushort wShowWindow, cbReserved2; public IntPtr lpReserved2; + public IntPtr hStdInput, hStdOutput, hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + struct PROCESS_INFORMATION + { + public IntPtr hProcess, hThread; + public uint dwProcessId, dwThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + struct WTS_SESSION_INFO + { + public uint SessionId; [MarshalAs(UnmanagedType.LPStr)] public string? pWinStationName; + public int State; // 0=Active + } + + #endregion + + // Spawnt Prozess als eingeloggter Desktop-User. Gibt PID zurück oder -1 bei Fehler. + public static int SpawnInUserSession(string exePath, string args) + { + var sessionId = FindActiveUserSession(); + if (sessionId == uint.MaxValue) + { + AgentWorker.Log("SessionSpawner: Keine aktive User-Session gefunden"); + return -1; + } + + if (!WTSQueryUserToken(sessionId, out var userToken)) + { + AgentWorker.Log($"SessionSpawner: WTSQueryUserToken fehlgeschlagen (Session={sessionId}, Error={Marshal.GetLastWin32Error()})"); + return -1; + } + + try + { + if (!DuplicateTokenEx(userToken, 0x10000000u, IntPtr.Zero, 2, 1, out var dupToken)) + { + AgentWorker.Log($"SessionSpawner: DuplicateTokenEx fehlgeschlagen (Error={Marshal.GetLastWin32Error()})"); + return -1; + } + + try + { + CreateEnvironmentBlock(out var envBlock, dupToken, false); + + var si = new STARTUPINFO + { + cb = Marshal.SizeOf(), + lpDesktop = "winsta0\\default", + dwFlags = 1, // STARTF_USESHOWWINDOW + wShowWindow = 1 // SW_SHOWNORMAL + }; + + var cmdLine = $"\"{exePath}\" {args}"; + bool ok = CreateProcessAsUser(dupToken, null, cmdLine, + IntPtr.Zero, IntPtr.Zero, false, + 0x0400, // CREATE_UNICODE_ENVIRONMENT + envBlock, null, ref si, out var pi); + + if (envBlock != IntPtr.Zero) DestroyEnvironmentBlock(envBlock); + + if (!ok) + { + AgentWorker.Log($"SessionSpawner: CreateProcessAsUser fehlgeschlagen (Error={Marshal.GetLastWin32Error()})"); + return -1; + } + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + AgentWorker.Log($"SessionSpawner: Prozess gestartet (PID={pi.dwProcessId}, Session={sessionId})"); + return (int)pi.dwProcessId; + } + finally { CloseHandle(dupToken); } + } + finally { CloseHandle(userToken); } + } + + private static uint FindActiveUserSession() + { + // Zuerst Console-Session versuchen + var consoleSession = WTSGetActiveConsoleSessionId(); + if (consoleSession != uint.MaxValue && TryGetTokenForSession(consoleSession)) + return consoleSession; + + // Alle Sessions durchsuchen → erste aktive (State=0) nehmen + if (!WTSEnumerateSessions(IntPtr.Zero, 0, 1, out var pInfo, out var count)) + return uint.MaxValue; + + try + { + var size = Marshal.SizeOf(); + for (int i = 0; i < count; i++) + { + var info = Marshal.PtrToStructure(IntPtr.Add(pInfo, i * size)); + if (info.State == 0 && info.SessionId != 0) // Active, nicht Session 0 + return info.SessionId; + } + } + finally { WTSFreeMemory(pInfo); } + + return uint.MaxValue; + } + + private static bool TryGetTokenForSession(uint sessionId) + { + if (!WTSQueryUserToken(sessionId, out var tok)) return false; + CloseHandle(tok); + return true; + } +} diff --git a/agent-cs/setup.iss b/agent-cs/setup.iss index c6cc89e..d85dfbb 100644 --- a/agent-cs/setup.iss +++ b/agent-cs/setup.iss @@ -1,5 +1,5 @@ #define MyAppName "IT Nexus Agent" -#define MyAppVersion "2.3.0" +#define MyAppVersion "2.4.0" #define MyAppPublisher "Cereda Systems GmbH" #define MyAppURL "https://it-nexus.cereda-systems.de" #define MyAppExeName "IT-Nexus-Agent.exe" diff --git a/frontend/src/pages/PatchManagementPage.jsx b/frontend/src/pages/PatchManagementPage.jsx index 3616567..7ed5310 100644 --- a/frontend/src/pages/PatchManagementPage.jsx +++ b/frontend/src/pages/PatchManagementPage.jsx @@ -5,7 +5,7 @@ import { useAuth } from '../context/AuthContext'; const ANN_TYPES = { maintenance: { icon: '🔧', label: 'Wartung', color: '#f59e0b' }, warning: { icon: '⚠️', label: 'Warnung', color: '#ef4444' }, info: { icon: 'ℹ️', label: 'Info', color: '#6366f1' } }; -const LATEST_AGENT_VERSION = '2.3.0'; +const LATEST_AGENT_VERSION = '2.4.0'; const SEVERITY_LABELS = { critical: 'Kritisch', important: 'Wichtig', moderate: 'Moderat', low: 'Niedrig', all: 'Alle' }; const SEVERITY_COLORS = { critical: '#EF4444', important: '#F59E0B', moderate: '#3B82F6', low: '#6B7280', all: '#0D9488' }; const COMMAND_LABELS = { check_updates: '🔍 Update-Scan', install_updates: '⬇️ Installation', reboot: '🔄 Neustart', upgrade_win11: '🪟 Win 11 Upgrade', update_agent: '⬆️ Agent-Update' };