Fix: RDP Consent-Dialog via WTSQueryUserToken+CreateProcessAsUser (v2.4.0)
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ namespace ITNexusAgent;
|
|||||||
|
|
||||||
public class AgentWorker
|
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 DataDir = @"C:\ProgramData\IT Nexus Agent";
|
||||||
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
|
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
|
||||||
private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json";
|
private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json";
|
||||||
|
|||||||
@@ -141,55 +141,8 @@ public class RtcService
|
|||||||
|
|
||||||
private static bool SpawnConsentHelper(string exePath, string portStr)
|
private static bool SpawnConsentHelper(string exePath, string portStr)
|
||||||
{
|
{
|
||||||
try
|
var pid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-consent {portStr}");
|
||||||
{
|
return pid > 0;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CapturePipeLoopAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct)
|
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)
|
private static bool SpawnCaptureHelper(string exePath, string portStr, int screenIdx = 0)
|
||||||
{
|
{
|
||||||
try
|
var pid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-capture {portStr} {screenIdx}");
|
||||||
{
|
return pid > 0;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
160
agent-cs/Services/SessionSpawner.cs
Normal file
160
agent-cs/Services/SessionSpawner.cs
Normal file
@@ -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<STARTUPINFO>(),
|
||||||
|
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<WTS_SESSION_INFO>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var info = Marshal.PtrToStructure<WTS_SESSION_INFO>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#define MyAppName "IT Nexus Agent"
|
#define MyAppName "IT Nexus Agent"
|
||||||
#define MyAppVersion "2.3.0"
|
#define MyAppVersion "2.4.0"
|
||||||
#define MyAppPublisher "Cereda Systems GmbH"
|
#define MyAppPublisher "Cereda Systems GmbH"
|
||||||
#define MyAppURL "https://it-nexus.cereda-systems.de"
|
#define MyAppURL "https://it-nexus.cereda-systems.de"
|
||||||
#define MyAppExeName "IT-Nexus-Agent.exe"
|
#define MyAppExeName "IT-Nexus-Agent.exe"
|
||||||
|
|||||||
@@ -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 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_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 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' };
|
const COMMAND_LABELS = { check_updates: '🔍 Update-Scan', install_updates: '⬇️ Installation', reboot: '🔄 Neustart', upgrade_win11: '🪟 Win 11 Upgrade', update_agent: '⬆️ Agent-Update' };
|
||||||
|
|||||||
Reference in New Issue
Block a user