24d8701b65
Native per-endpoint identity instead of the schtasks bridge: - Service (default) keeps the existing path - hooks inherit the service account (SYSTEM by default, or whatever you installed under). - SpecificUser binds ProcessStartInfo.UserName / Password / Domain so the hook runs in a batch logon session as the named account. Useful for AD-write hooks that should NOT run as SYSTEM. - InteractiveUser uses WTSQueryUserToken(WTSGetActiveConsoleSessionId) + DuplicateTokenEx + CreateProcessAsUser to drop the child into the logged-in user's session with their environment block. This is the real fix for "calc.exe should pop up on my desktop" - no Task Scheduler bridge required. Stdio is captured via inheritable anonymous pipes so the hook still returns stdout/stderr to the caller normally. Implementation: - New RunAsMode enum + RunAsConfig model on EndpointConfig - ConfigStore round-trips RunAs.Password through DPAPI alongside bearer/HMAC/PFX secrets - AdminPipeServer's secret-merge logic preserves the encrypted blob when the GUI saves an endpoint without re-typing the password - New WebhookServer.Core.Execution.Native namespace with NativeMethods (P/Invoke) and InteractiveProcessLauncher (token-based launcher) - ProcessExecutor branches on RunAs.Mode; the Service/SpecificUser paths share .NET's Process; InteractiveUser uses the launcher - GUI editor gets a "Run as" section: dropdown + conditional username/password/load-profile fields under SpecificUser Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
145 lines
4.6 KiB
C#
145 lines
4.6 KiB
C#
using System.ComponentModel;
|
|
using System.Runtime.InteropServices;
|
|
using System.Runtime.Versioning;
|
|
|
|
namespace WebhookServer.Core.Execution.Native;
|
|
|
|
/// <summary>
|
|
/// Win32 P/Invoke layer for launching processes in another user's session.
|
|
/// Used by <see cref="InteractiveProcessLauncher"/>; not intended for general use.
|
|
/// </summary>
|
|
[SupportedOSPlatform("windows")]
|
|
internal static class NativeMethods
|
|
{
|
|
public const uint INVALID_SESSION_ID = 0xFFFFFFFF;
|
|
public const int MAXIMUM_ALLOWED = 0x02000000;
|
|
|
|
public const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
|
|
public const uint CREATE_NO_WINDOW = 0x08000000;
|
|
public const uint CREATE_NEW_CONSOLE = 0x00000010;
|
|
public const uint NORMAL_PRIORITY_CLASS = 0x00000020;
|
|
|
|
public const int STARTF_USESTDHANDLES = 0x00000100;
|
|
|
|
public const int HANDLE_FLAG_INHERIT = 1;
|
|
|
|
public const uint INFINITE = 0xFFFFFFFF;
|
|
|
|
public enum SECURITY_IMPERSONATION_LEVEL
|
|
{
|
|
SecurityAnonymous = 0,
|
|
SecurityIdentification = 1,
|
|
SecurityImpersonation = 2,
|
|
SecurityDelegation = 3,
|
|
}
|
|
|
|
public enum TOKEN_TYPE
|
|
{
|
|
TokenPrimary = 1,
|
|
TokenImpersonation = 2,
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct SECURITY_ATTRIBUTES
|
|
{
|
|
public int nLength;
|
|
public IntPtr lpSecurityDescriptor;
|
|
public int bInheritHandle;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
public 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 ushort wShowWindow;
|
|
public ushort cbReserved2;
|
|
public IntPtr lpReserved2;
|
|
public IntPtr hStdInput;
|
|
public IntPtr hStdOutput;
|
|
public IntPtr hStdError;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct PROCESS_INFORMATION
|
|
{
|
|
public IntPtr hProcess;
|
|
public IntPtr hThread;
|
|
public uint dwProcessId;
|
|
public uint dwThreadId;
|
|
}
|
|
|
|
[DllImport("kernel32.dll")]
|
|
public static extern uint WTSGetActiveConsoleSessionId();
|
|
|
|
[DllImport("wtsapi32.dll", SetLastError = true)]
|
|
public static extern bool WTSQueryUserToken(uint sessionId, out IntPtr phToken);
|
|
|
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
public static extern bool DuplicateTokenEx(
|
|
IntPtr hExistingToken,
|
|
uint dwDesiredAccess,
|
|
IntPtr lpTokenAttributes,
|
|
SECURITY_IMPERSONATION_LEVEL impersonationLevel,
|
|
TOKEN_TYPE tokenType,
|
|
out IntPtr phNewToken);
|
|
|
|
[DllImport("userenv.dll", SetLastError = true)]
|
|
public static extern bool CreateEnvironmentBlock(
|
|
out IntPtr lpEnvironment,
|
|
IntPtr hToken,
|
|
bool bInherit);
|
|
|
|
[DllImport("userenv.dll", SetLastError = true)]
|
|
public static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);
|
|
|
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
public 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("kernel32.dll", SetLastError = true)]
|
|
public static extern bool CreatePipe(
|
|
out IntPtr hReadPipe,
|
|
out IntPtr hWritePipe,
|
|
ref SECURITY_ATTRIBUTES lpPipeAttributes,
|
|
uint nSize);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool SetHandleInformation(IntPtr hObject, int dwMask, int dwFlags);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool CloseHandle(IntPtr hObject);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
|
|
public static Win32Exception LastError(string what) =>
|
|
new(Marshal.GetLastWin32Error(), $"{what} failed");
|
|
}
|