diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs index 687d9b6e..f52a2a42 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs @@ -7,6 +7,7 @@ using System.Net.NetworkInformation; using System.Text.Json; using System.Threading.Tasks; using AutopilotMonitor.Agent.Core.Logging; +using AutopilotMonitor.Agent.Core.Security; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; @@ -635,7 +636,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors { var psi = new ProcessStartInfo { - FileName = "netsh.exe", + FileName = SystemPaths.Netsh, Arguments = "wlan show interfaces", UseShellExecute = false, RedirectStandardOutput = true, diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherCollectors/CommandCollector.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherCollectors/CommandCollector.cs index 27706fd8..7cc449a9 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherCollectors/CommandCollector.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherCollectors/CommandCollector.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; +using AutopilotMonitor.Agent.Core.Security; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors.GatherCollectors @@ -56,7 +57,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors.GatherCollectors var encodedCommand = Convert.ToBase64String(Encoding.Unicode.GetBytes(command)); psi = new ProcessStartInfo { - FileName = "powershell.exe", + FileName = SystemPaths.PowerShell, Arguments = $"-NoProfile -ExecutionPolicy RemoteSigned -EncodedCommand {encodedCommand}", UseShellExecute = false, RedirectStandardOutput = true, @@ -68,7 +69,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors.GatherCollectors { psi = new ProcessStartInfo { - FileName = "cmd.exe", + FileName = SystemPaths.Cmd, Arguments = $"/c {command}", UseShellExecute = false, RedirectStandardOutput = true, diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/NetworkChangeDetector.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/NetworkChangeDetector.cs index 224f2837..127ddd6b 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/NetworkChangeDetector.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/NetworkChangeDetector.cs @@ -8,6 +8,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using AutopilotMonitor.Agent.Core.Logging; +using AutopilotMonitor.Agent.Core.Security; using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; @@ -401,7 +402,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors { var psi = new ProcessStartInfo { - FileName = "netsh.exe", + FileName = SystemPaths.Netsh, Arguments = "wlan show interfaces", UseShellExecute = false, RedirectStandardOutput = true, diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/CleanupService.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/CleanupService.cs index 23dbc123..98789ee2 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/CleanupService.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/CleanupService.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using AutopilotMonitor.Agent.Core.Configuration; using AutopilotMonitor.Agent.Core.Logging; +using AutopilotMonitor.Agent.Core.Security; namespace AutopilotMonitor.Agent.Core.Monitoring.Core { @@ -101,8 +102,10 @@ Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue // creates a new process group that breaks job inheritance, even under SYSTEM. var psi = new ProcessStartInfo { - FileName = "cmd.exe", - Arguments = $"/c start \"\" /b powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"{tempScriptPath}\"", + FileName = SystemPaths.Cmd, + // Absolute path to powershell.exe is passed to `start` to prevent PATH hijacking + // (the `start` builtin does its own PATH resolution for the target command). + Arguments = $"/c start \"\" /b \"{SystemPaths.PowerShell}\" -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"{tempScriptPath}\"", UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = Path.GetTempPath() diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs index 3376015f..d8ba9eab 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs @@ -12,6 +12,7 @@ using AutopilotMonitor.Agent.Core.Logging; using AutopilotMonitor.Agent.Core.Monitoring.Collectors; using AutopilotMonitor.Agent.Core.Monitoring.Network; using AutopilotMonitor.Agent.Core.Monitoring.Tracking; +using AutopilotMonitor.Agent.Core.Security; using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; @@ -635,7 +636,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core var psi = new ProcessStartInfo { - FileName = "shutdown.exe", + FileName = SystemPaths.Shutdown, Arguments = $"/r /t {_configuration.RebootDelaySeconds} /c \"Autopilot enrollment completed - rebooting\"", UseShellExecute = false, CreateNoWindow = true diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Network/TimezoneService.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Network/TimezoneService.cs index e26d188b..76b26820 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Network/TimezoneService.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Network/TimezoneService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using AutopilotMonitor.Agent.Core.Logging; +using AutopilotMonitor.Agent.Core.Security; namespace AutopilotMonitor.Agent.Core.Monitoring.Network { @@ -50,7 +51,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network var psi = new ProcessStartInfo { - FileName = "tzutil", + FileName = SystemPaths.TzUtil, Arguments = $"/s \"{windowsId}\"", UseShellExecute = false, RedirectStandardOutput = true, diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Security/SystemPaths.cs b/src/Agent/AutopilotMonitor.Agent.Core/Security/SystemPaths.cs new file mode 100644 index 00000000..1281a31c --- /dev/null +++ b/src/Agent/AutopilotMonitor.Agent.Core/Security/SystemPaths.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; + +namespace AutopilotMonitor.Agent.Core.Security +{ + /// + /// Resolved absolute paths to Windows system binaries. + /// + /// Rationale: launching processes by bare name (e.g. "cmd.exe", "tzutil") lets + /// Windows walk the PATH to find them. As SYSTEM this is a DLL/binary-hijacking + /// vector — an attacker able to plant a file in any PATH entry ahead of System32 + /// gets code execution with SYSTEM privileges. Always use these resolved paths. + /// + /// respects WOW64 redirection + /// automatically: + /// - 64-bit process on any Windows → C:\Windows\System32 + /// - 32-bit process on 64-bit Windows → C:\Windows\SysWOW64 + /// The agent ships as AnyCPU/net48 and runs 64-bit on 64-bit Windows, so this + /// resolves to the real System32. It also honors non-default %SystemRoot% + /// (rare, but seen in custom OEM images on drives other than C:). + /// + /// All listed binaries are in-box on every supported Windows version + /// (Windows 7 / 8.1 / 10 / 11 / Server 2012R2 – 2025). + /// + public static class SystemPaths + { + private static readonly string System32 = Environment.SystemDirectory; + + /// C:\Windows\System32\cmd.exe + public static readonly string Cmd = Path.Combine(System32, "cmd.exe"); + + /// C:\Windows\System32\shutdown.exe + public static readonly string Shutdown = Path.Combine(System32, "shutdown.exe"); + + /// C:\Windows\System32\tzutil.exe + public static readonly string TzUtil = Path.Combine(System32, "tzutil.exe"); + + /// C:\Windows\System32\netsh.exe + public static readonly string Netsh = Path.Combine(System32, "netsh.exe"); + + /// + /// C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe + /// Note: the "v1.0" folder is historical — it hosts every in-box PowerShell + /// version including 5.1 on Windows 10/11. Do not "fix" it to v5.1. + /// + public static readonly string PowerShell = Path.Combine(System32, "WindowsPowerShell", "v1.0", "powershell.exe"); + } +} diff --git a/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs b/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs index 68339fcb..79620fc3 100644 --- a/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs +++ b/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs @@ -105,9 +105,7 @@ namespace AutopilotMonitor.Agent bool enableGeoLocationConfig = true; bool useClientCertAuthConfig = true; - // Environment variable overrides built-in default - apiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? apiBaseUrl; - // CLI override wins over environment variable/default + // CLI override wins over built-in default if (!string.IsNullOrWhiteSpace(apiBaseUrlOverride)) apiBaseUrl = apiBaseUrlOverride; diff --git a/src/Agent/AutopilotMonitor.Agent/SelfUpdater.cs b/src/Agent/AutopilotMonitor.Agent/SelfUpdater.cs index 3947d17b..d1e2780d 100644 --- a/src/Agent/AutopilotMonitor.Agent/SelfUpdater.cs +++ b/src/Agent/AutopilotMonitor.Agent/SelfUpdater.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +using AutopilotMonitor.Agent.Core.Security; using AutopilotMonitor.Shared; using Newtonsoft.Json.Linq; @@ -451,7 +452,7 @@ namespace AutopilotMonitor.Agent var psi = new ProcessStartInfo { - FileName = "powershell.exe", + FileName = SystemPaths.PowerShell, Arguments = $"-NoProfile -NonInteractive -EncodedCommand {encoded}", WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index b9cb48f6..4fdb2984 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -885 +887