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