Finding Weak Service Executable Paths with Defender TVM and PowerShell
Most organizations focus on vulnerabilities, missing patches, exposed services and risky configurations. That is good, it can in all cases always be even better.
However, one of the the more practical privilege escalation risks is often overlooked because they are hard to spot:
A (Windows) service runs from a folder where normal users have write permissions.
That creates a real risk.
If a service executable or related service files are located in a folder that can be modified by broad principals such as Authenticated Users, Users, or Domain Users, a local user or attacker may be able to tamper with that service path.
If that service runs with elevated privileges, such as LocalSystem or a privileged service account, this can become a local privilege escalation or persistence path.
This is where the Microsoft Defender recommendation becomes useful:
Change service executable path to a common protected location
The idea is simple. Service binaries should preferably live in common protected locations such as C:\Program Files or C:\Windows, where normal users should not have write access.
But as always: do not just accept the recommendation blindly. Because just because this recommendation is there doesn’t mean something is inherently wrong, however during my frequent customer meetings, I always ask, if they regularly check the ACL permissions these folders have, the answer so far has always been ’no'.
So where do we start? First check what is actually exposed.
Why
A weak service executable path can matter in several scenarios:
- Local privilege escalation
- Service binary hijacking
- Persistence through service tampering
- Ransomware staging or defense evasion
This is especially relevant for devices where services run from custom folders such as:
C:\tempD:\appsD:\azuredevopsC:\tools- vendor-specific application folders (I am looking at you Oracle!)
- old migration or installer directories
The folder location itself is not always the problem.
The real question is:
Who can write to that folder? Because Program Files or Windows have set ACL permission lists while anything outside of these standard OS Folders does not.
If a service runs from D:\SomeApp, and only Administrators and SYSTEM can modify that folder, the practical risk is much lower.
If Authenticated Users or Domain Users have Modify, Write, or FullControl, the risk becomes much more great or interesting depending on who looks at it.
Most of these folders go unnoticed because in general they slowly grow as most organisations don’t keep this in check and some applications even say in their manual; create a folder in C:\ and work from there. They do not want to bother you with ACL permissions, because than the barrier to entry might become to great and you might not use their software. Or maybe they also are not aware of the potential risk, either way, as an IT organisation you are responsible for what happens in your organisation.
This can be quite a daunting task however I wrote a KQL and a powershell script that, depending on how you want to put this in operation might need adjusting, can help with the tedious check of checking ACL permissions.
Workflow
The workflow is straightforward:
- Run the KQL in Microsoft Defender Advanced Hunting.
- Review the affected services and extracted base folders.
- Copy the
ServiceName,ExecutableDirectory&BaseFoldervalues. - Paste them into the PowerShell array.
- Run the PowerShell script on the affected device.
- Review
ServiceExecutableFolderAclEvidence.csv.
The KQL tells you which folders Defender is concerned about.
The PowerShell collects the ACL evidence needed to determine whether those folders are writable by broad principals. You as the engineer need to make the decission if this is according to least privilege.
Find services outside common protected locations
Use the following KQL in Microsoft Defender Advanced Hunting.
let RecommendationName = "Change service executable path to a common protected location";
DeviceTvmSecureConfigurationAssessment
| join kind=inner (
DeviceTvmSecureConfigurationAssessmentKB
| where ConfigurationName =~ RecommendationName
| project ConfigurationId, ConfigurationName
) on ConfigurationId
| where IsApplicable == 1
| where IsCompliant == 0
| extend ContextText = tostring(Context)
| extend Parsed = parse_json(ContextText)
| extend
ServiceName = tostring(Parsed[0]),
RawServicePath = tostring(Parsed[1])
| extend ServicePath = replace_string(RawServicePath, @"\""", @"""")
| extend ServicePath = trim(@'"', ServicePath)
| extend ServicePath = replace_regex(ServicePath, @"^\\\?\?\\", "")
| extend ServicePath = replace_regex(ServicePath, @"^\\\\\?\\", "")
| extend ExecutablePath = extract(@"([A-Za-z]:\\[^""]+?\.exe)", 1, ServicePath)
| extend BaseFolder = extract(@"^([A-Za-z]:\\[^\\]+)", 1, ExecutablePath)
| extend ExecutableDirectory = replace_regex(ExecutablePath, @"\\[^\\]+\.exe$", "")
| summarize arg_max(Timestamp, *) by DeviceId, ServiceName, ExecutablePath
| project
DeviceName,
Recommendation = ConfigurationName,
ServiceName,
RawServicePath,
ServicePath,
ExecutablePath,
BaseFolder,
ExecutableDirectory,
Timestamp
| order by DeviceName asc, BaseFolder asc, ServiceName asc
This query:
- Finds devices exposed to the Defender recommendation.
- ServiceName
- RawServicePath
- ServicePath
- ExecutablePath
- BaseFolder
- ExecutableDirectory
- Provides the exact folders that should be reviewed.
KQL output
Example:
| DeviceName | ServiceName | ExecutablePath | BaseFolder | ExecutableDirectory |
|---|---|---|---|---|
| ENG-LT-024 | DockerDesktopService | C:\Tools\Docker\com.docker.service.exe | C:\Tools | C:\Tools\Docker |
| DEV-LT-118 | JenkinsAgent | D:\BuildAgents\Jenkins\agent.exe | D:\BuildAgents | D:\BuildAgents\Jenkins |
| IT-LT-009 | PDQDeployRunner | C:\Applications\PDQ\Runner\runner.exe | C:\Applications | C:\Applications\PDQ\Runner |
| DEV-LT-203 | CustomUpdater | C:\DevTools\Updater\updater.exe | C:\DevTools | C:\DevTools\Updater |
| ENG-LT-077 | AzureDevOpsAgent | D:\azuredevops\a01\bin\AgentService.exe | D:\azuredevops | D:\azuredevops\a01\bin |
| APP-LT-041 | OracleServiceXE | C:\Oracle\product\21c\dbhomeXE\bin\oracle.exe | C:\Oracle | C:\Oracle\product\21c\dbhomeXE\bin |
Transfer from the Defender portal to Local on the device
From the KQL output, copy the ServiceName, ExecutableDirectory & BaseFolder values for the affected device. into the array below in the powershell.
Below is an example of multiple entries:
$ServicePathsToCheck = @(
[PSCustomObject]@{
ServiceName = 'battlenet_helpersvc'
BaseFolder = 'C:\ProgramData'
ExecutableDirectory = 'C:\ProgramData\Battle.net_components\battlenet_helpersvc'
},
[PSCustomObject]@{
ServiceName = 'EABackgroundService'
BaseFolder = 'D:\Program Files'
ExecutableDirectory = 'D:\Program Files\EA\DesktopApp\EA Desktop'
},
[PSCustomObject]@{
ServiceName = 'MDCoreSvc'
BaseFolder = 'C:\ProgramData'
ExecutableDirectory = 'C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.26030.3011-0'
},
[PSCustomObject]@{
ServiceName = 'TempTestService'
BaseFolder = 'C:\Temp'
ExecutableDirectory = 'C:\Temp\TestService'
},
[PSCustomObject]@{
ServiceName = 'WdNisSvc'
BaseFolder = 'C:\ProgramData'
ExecutableDirectory = 'C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.26030.3011-0'
},
[PSCustomObject]@{
ServiceName = 'WinDefend'
BaseFolder = 'C:\ProgramData'
ExecutableDirectory = 'C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.26030.3011-0'
}
)
Also note that below $ServicePathsToCheck is $EnableRecursiveCheck = $false.
This can be set to $true, which will scan all folders recursively.
Because of obvious resource considerations, recursive scanning is disabled by default. However, if needed, the script can also be used recursively.
$ServicePathsToCheck = @(
[PSCustomObject]@{
ServiceName = ''
BaseFolder = ''
ExecutableDirectory = ''
}
)
$EnableRecursiveCheck = $false
$OutputFolder = Join-Path $env:USERPROFILE 'Desktop\ServiceAclAudit'
if (-not (Test-Path -LiteralPath $OutputFolder)) {
New-Item -ItemType Directory -Path $OutputFolder | Out-Null
}
function Get-IdentitySid {
param(
[Parameter(Mandatory)]
[System.Security.Principal.IdentityReference]$IdentityReference
)
try {
return $IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
}
catch {
return $null
}
}
function Get-ServicePathAclEvidence {
param(
[Parameter(Mandatory)]
[string]$ServiceName,
[Parameter(Mandatory)]
[string]$BaseFolder,
[Parameter(Mandatory)]
[string]$ExecutableDirectory,
[bool]$Recursive = $false
)
$pathsToInspect = @(
[PSCustomObject]@{
Path = $BaseFolder
PathType = 'BaseFolder'
},
[PSCustomObject]@{
Path = $ExecutableDirectory
PathType = 'ExecutableDirectory'
}
) | Where-Object {
-not [string]::IsNullOrWhiteSpace($_.Path)
}
if ($Recursive -and (Test-Path -LiteralPath $BaseFolder)) {
$childFolders = Get-ChildItem -LiteralPath $BaseFolder -Directory -Recurse -Force -ErrorAction SilentlyContinue
foreach ($childFolder in $childFolders) {
$pathsToInspect += [PSCustomObject]@{
Path = $childFolder.FullName
PathType = 'SubFolder'
}
}
}
$pathsToInspect = $pathsToInspect | Sort-Object Path -Unique
foreach ($pathItem in $pathsToInspect) {
if (-not (Test-Path -LiteralPath $pathItem.Path)) {
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
ServiceName = $ServiceName
BaseFolder = $BaseFolder
ExecutableDirectory = $ExecutableDirectory
CheckedPath = $pathItem.Path
PathType = $pathItem.PathType
Owner = ''
InheritanceEnabled = ''
IdentityReference = ''
IdentitySid = ''
FileSystemRights = ''
AccessControlType = ''
IsInherited = ''
InheritanceFlags = ''
PropagationFlags = ''
}
continue
}
try {
$acl = Get-Acl -LiteralPath $pathItem.Path -ErrorAction Stop
}
catch {
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
ServiceName = $ServiceName
BaseFolder = $BaseFolder
ExecutableDirectory = $ExecutableDirectory
CheckedPath = $pathItem.Path
PathType = $pathItem.PathType
Owner = ''
InheritanceEnabled = ''
IdentityReference = "Could not read ACL: $($_.Exception.Message)"
IdentitySid = ''
FileSystemRights = ''
AccessControlType = ''
IsInherited = ''
InheritanceFlags = ''
PropagationFlags = ''
}
continue
}
$inheritanceEnabled = -not $acl.AreAccessRulesProtected
foreach ($ace in $acl.Access) {
$identitySid = Get-IdentitySid -IdentityReference $ace.IdentityReference
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
ServiceName = $ServiceName
BaseFolder = $BaseFolder
ExecutableDirectory = $ExecutableDirectory
CheckedPath = $pathItem.Path
PathType = $pathItem.PathType
Owner = $acl.Owner
InheritanceEnabled = $inheritanceEnabled
IdentityReference = $ace.IdentityReference.Value
IdentitySid = $identitySid
FileSystemRights = $ace.FileSystemRights.ToString()
AccessControlType = $ace.AccessControlType.ToString()
IsInherited = $ace.IsInherited
InheritanceFlags = $ace.InheritanceFlags.ToString()
PropagationFlags = $ace.PropagationFlags.ToString()
}
}
}
}
$AclReport = foreach ($servicePath in $ServicePathsToCheck) {
Get-ServicePathAclEvidence `
-ServiceName $servicePath.ServiceName `
-BaseFolder $servicePath.BaseFolder `
-ExecutableDirectory $servicePath.ExecutableDirectory `
-Recursive $EnableRecursiveCheck
}
$CsvPath = Join-Path $OutputFolder 'ServiceExecutableFolderAclEvidence.csv'
$AclReport |
Select-Object `
ComputerName,
ServiceName,
BaseFolder,
ExecutableDirectory,
CheckedPath,
PathType,
Owner,
InheritanceEnabled,
IdentityReference,
IdentitySid,
FileSystemRights,
AccessControlType,
IsInherited,
InheritanceFlags,
PropagationFlags |
Sort-Object ServiceName, BaseFolder, CheckedPath, IdentityReference |
Export-Csv -Path $CsvPath -NoTypeInformation -Delimiter ';' -Encoding UTF8
Write-Host ''
Write-Host 'ACL evidence exported to:'
Write-Host $CsvPath
Write-Host ''
$AclReport |
Select-Object `
ComputerName,
ServiceName,
BaseFolder,
ExecutableDirectory,
CheckedPath,
PathType,
Owner,
InheritanceEnabled,
IdentityReference,
IdentitySid,
FileSystemRights,
AccessControlType,
IsInherited,
InheritanceFlags,
PropagationFlags |
Format-Table -AutoSize
Example of Output
| ComputerName | ServiceName | BaseFolder | ExecutableDirectory | CheckedPath | PathType | Owner | InheritanceEnabled | IdentityReference | IdentitySid | FileSystemRights | AccessControlType | IsInherited | InheritanceFlags | PropagationFlags |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Host-01 | battlenet_helpersvc | C:\ProgramData | C:\ProgramData\Battle.net_components\battlenet_helpersvc | C:\ProgramData | BaseFolder | NT AUTHORITY\SYSTEM | FALSE | BUILTIN\Administrators | S-1-5-32-544 | FullControl | Allow | FALSE | ContainerInherit, ObjectInherit | None |
| Host-01 | battlenet_helpersvc | C:\ProgramData | C:\ProgramData\Battle.net_components\battlenet_helpersvc | C:\ProgramData | BaseFolder | NT AUTHORITY\SYSTEM | FALSE | BUILTIN\Users | S-1-5-32-545 | ReadAndExecute, Synchronize | Allow | FALSE | ContainerInherit, ObjectInherit | None |
| Host-01 | battlenet_helpersvc | C:\ProgramData | C:\ProgramData\Battle.net_components\battlenet_helpersvc | C:\ProgramData | BaseFolder | NT AUTHORITY\SYSTEM | FALSE | BUILTIN\Users | S-1-5-32-545 | Write | Allow | FALSE | ContainerInherit | None |
| Host-01 | battlenet_helpersvc | C:\ProgramData | C:\ProgramData\Battle.net_components\battlenet_helpersvc | C:\ProgramData | BaseFolder | NT AUTHORITY\SYSTEM | FALSE | CREATOR OWNER | S-1-3-0 | 268435456 | Allow | FALSE | ContainerInherit, ObjectInherit | InheritOnly |
| Host-01 | battlenet_helpersvc | C:\ProgramData | C:\ProgramData\Battle.net_components\battlenet_helpersvc | C:\ProgramData | BaseFolder | NT AUTHORITY\SYSTEM | FALSE | NT AUTHORITY\SYSTEM | S-1-5-18 | FullControl | Allow | FALSE | ContainerInherit, ObjectInherit | None |
| Host-01 | battlenet_helpersvc | C:\ProgramData | C:\ProgramData\Battle.net_components\battlenet_helpersvc | C:\ProgramData\Battle.net_components\battlenet_helpersvc | ExecutableDirectory | BUILTIN\Administrators | TRUE | BUILTIN\Administrators | S-1-5-32-544 | FullControl | Allow | TRUE | ContainerInherit, ObjectInherit | None |
| Host-01 | battlenet_helpersvc | C:\ProgramData | C:\ProgramData\Battle.net_components\battlenet_helpersvc | C:\ProgramData\Battle.net_components\battlenet_helpersvc | ExecutableDirectory | BUILTIN\Administrators | TRUE | BUILTIN\Users | S-1-5-32-545 | ReadAndExecute, Synchronize | Allow | TRUE | ContainerInherit, ObjectInherit | None |
| Host-01 | battlenet_helpersvc | C:\ProgramData | C:\ProgramData\Battle.net_components\battlenet_helpersvc | C:\ProgramData\Battle.net_components\battlenet_helpersvc | ExecutableDirectory | BUILTIN\Administrators | TRUE | NT AUTHORITY\INTERACTIVE | S-1-5-4 | ReadAndExecute, Synchronize | Allow | TRUE | ContainerInherit, ObjectInherit | None |
| Host-01 | TempTestService | C:\Temp | C:\Temp | C:\Temp | BaseFolder | BUILTIN\Administrators | TRUE | BUILTIN\Administrators | S-1-5-32-544 | FullControl | Allow | TRUE | ContainerInherit, ObjectInherit | None |
| Host-01 | TempTestService | C:\Temp | C:\Temp | C:\Temp | BaseFolder | BUILTIN\Administrators | TRUE | BUILTIN\Users | S-1-5-32-545 | ReadAndExecute, Synchronize | Allow | TRUE | ContainerInherit, ObjectInherit | None |
| Host-01 | TempTestService | C:\Temp | C:\Temp | C:\Temp | BaseFolder | BUILTIN\Administrators | TRUE | NT AUTHORITY\Authenticated Users | S-1-5-11 | -536805376 | Allow | TRUE | ContainerInherit, ObjectInherit | InheritOnly |
| Host-01 | TempTestService | C:\Temp | C:\Temp | C:\Temp | BaseFolder | BUILTIN\Administrators | TRUE | NT AUTHORITY\Authenticated Users | S-1-5-11 | Modify, Synchronize | Allow | TRUE | None | None |
| Host-01 | TempTestService | C:\Temp | C:\Temp | C:\Temp | BaseFolder | BUILTIN\Administrators | TRUE | NT AUTHORITY\SYSTEM | S-1-5-18 | FullControl | Allow | TRUE | ContainerInherit, ObjectInherit | None |
| Host-01 | TempTestService | C:\Temp | C:\Temp | C:\Temp | ExecutableDirectory | BUILTIN\Administrators | TRUE | BUILTIN\Administrators | S-1-5-32-544 | FullControl | Allow | TRUE | ContainerInherit, ObjectInherit | None |
| Host-01 | TempTestService | C:\Temp | C:\Temp | C:\Temp | ExecutableDirectory | BUILTIN\Administrators | TRUE | BUILTIN\Users | S-1-5-32-545 | ReadAndExecute, Synchronize | Allow | TRUE | ContainerInherit, ObjectInherit | None |
| Host-01 | TempTestService | C:\Temp | C:\Temp | C:\Temp | ExecutableDirectory | BUILTIN\Administrators | TRUE | NT AUTHORITY\Authenticated Users | S-1-5-11 | -536805376 | Allow | TRUE | ContainerInherit, ObjectInherit | InheritOnly |
| Host-01 | TempTestService | C:\Temp | C:\Temp | C:\Temp | ExecutableDirectory | BUILTIN\Administrators | TRUE | NT AUTHORITY\Authenticated Users | S-1-5-11 | Modify, Synchronize | Allow | TRUE | None | None |
| Host-01 | TempTestService | C:\Temp | C:\Temp | C:\Temp | ExecutableDirectory | BUILTIN\Administrators | TRUE | NT AUTHORITY\SYSTEM | S-1-5-18 | FullControl | Allow | TRUE | ContainerInherit, ObjectInherit | None |
| Host-01 | WinDefend | C:\ProgramData | C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.26030.3011-0 | C:\ProgramData | BaseFolder | NT AUTHORITY\SYSTEM | FALSE | BUILTIN\Administrators | S-1-5-32-544 | FullControl | Allow | FALSE | ContainerInherit, ObjectInherit | None |
| Host-01 | WinDefend | C:\ProgramData | C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.26030.3011-0 | C:\ProgramData | BaseFolder | NT AUTHORITY\SYSTEM | FALSE | BUILTIN\Users | S-1-5-32-545 | Write | Allow | FALSE | ContainerInherit | None |
What to look for
The most important thing to review is whether broad principals have write-capable permissions.
Examples:
Authenticated Users Modify
BUILTIN\Users Modify
Domain Users Write
Everyone FullControl
These are the entries that usually matter.
Read-only entries such as:
BUILTIN\Users ReadAndExecute
are generally less concerning. Still use your best judgement and take into context the product we are reviewing, is it least privilege? If not, can we change it? Or can we accept the risk. Also take into account that
Deployment options
There are several operational ways to deploy this at scale.
Option 1: Manual assessment
Best for:
- one-off reviews
- small environments
- focused investigations
Workflow:
- Run the KQL.
- Copy the folders.
- Run the PowerShell manually.
- Review the CSV locally.
This is the simplest approach.
Option 2: Intune deployment with local CSV output
Workflow:
- Use the KQL to identify affected devices.
- Deploy the PowerShell using:
- Intune Platform Scripts
- Intune Proactive Remediations
- The script generates the CSV locally.
- Retrieve results using:
- Intune Collect Diagnostics
- Defender Live Response
- RMM tooling
- remote collection methods
Option 3: Centralized ingestion into Sentinel or Log Analytics
Best for:
- large environments
- security engineering
- posture management
- ongoing monitoring
Workflow:
- Run the script through Intune.
- Convert findings to JSON.
- Send results to:
- Log Analytics
This allows:
- dashboards
- trending
- potential alerting
- Centralized information for decision making
Important nuance
Do not blindly remediate this recommendation. The presence of this finding does not automatically mean something is exploitable or insecure, but it does warrant review.
Many environments intentionally run services from locations outside standard protected operating system folders. Common examples include:
- build agents
- deployment frameworks
- middleware platforms
- legacy applications
- vendor-managed software
- Azure VM extension paths
The folder location itself is not necessarily the problem.
The real question is:
Can broad principals modify the folder contents?
If a custom service folder is writable only by principals such as:
- SYSTEM
- TrustedInstaller
- Administrators
the practical risk may be limited or acceptable depending on the environment and operational requirements.
The actual concern is when broad principals such as:
- Authenticated Users
- BUILTIN\Users
- Domain Users
- Everyone
have write-capable permissions on directories used by (privileged) services.
One thing you will likely notice quickly when running this KQL in enterprise environments is the large number of findings related to:
C:\Packages\Plugins\
This is especially common on Azure virtual machines running components such as:
- Microsoft Defender for SQL
- Azure Automation Hybrid Workers
- monitoring agents
- backup agents
- Azure VM extensions
These directories are commonly used by Microsoft-managed extension frameworks and are therefore not automatically insecure simply because they exist outside C:\Program Files.
The same validation principle still applies however:
Do not trust the folder location alone. Validate the ACLs.
In many environments these folders are still properly protected and writable only by privileged principals. In those cases, the finding may represent an accepted or low practical risk.
This recommendation should therefore be treated as a signal for review and validation, not as proof of exploitation or immediate misconfiguration.
Why ransomware operators care
Ransomware is not only about encryption.
Before encryption, attackers often try to:
- escalate privileges
- maintain persistence
- disable security tooling
- tamper with services
- abuse trusted execution paths
A writable service executable folder can help with that.
If an attacker already has a foothold as a normal user and can modify files inside a privileged service path, that may become a local privilege escalation or persistence mechanism.
That does not mean every finding is immediately exploitable.
But it absolutely means the finding deserves review.
Final thought
This Defender recommendation is useful, but the real value is not the recommendation itself.
The real value is validating the actual ACLs.
A service running outside C:\Program Files is not automatically dangerous.
A service running from a folder where Authenticated Users or Domain Users can modify files is a different story.
So the practical workflow becomes:
- Find the exposed service path with KQL.
- Extract the data.
- Validate the ACLs locally or centrally.
- Determine whether folders are adhered to least privilege.
- Then decide whether remediation is needed.
