Skip to content

fix: WindowsFeaturesService binary-planting LPE via extension-less powershell launch#1419

Merged
laurentiu021 merged 1 commit into
mainfrom
fix/windowsfeatures-powershell-exe-lpe
Jul 10, 2026
Merged

fix: WindowsFeaturesService binary-planting LPE via extension-less powershell launch#1419
laurentiu021 merged 1 commit into
mainfrom
fix/windowsfeatures-powershell-exe-lpe

Conversation

@laurentiu021

Copy link
Copy Markdown
Owner

Summary

Fresh ultra-audit finding (security-deep, P1), confirmed by 3 adversarial verifier lenses (isReal=true, high confidence) against real source.

All three WindowsFeaturesService call sites — ListFeaturesAsync (:36), EnableFeatureAsync (:70), DisableFeatureAsync (:107); the enable/disable paths run elevated — launched PowerShell as the extension-less bare name "powershell".

PowerShellRunner.RunProcessAsync hardens the target via SystemPaths.ResolveSystemTool, but that helper resolved by File.Exists on the exact name only: File.Exists(@"…\System32\WindowsPowerShell\v1.0\powershell") is false (the file is powershell.exe), so both probes missed and it returned the unrooted "powershell". With UseShellExecute=false, Win32 CreateProcess then searches the calling process's own directory first — so an attacker-planted powershell.exe next to SysManager's single portable .exe (often run from a user-writable folder like Downloads, sometimes elevated) would execute with the app's privileges. This is the same binary-planting / LPE class already fixed for System32 tools and winget.

Fix

  • All three sites now pass "powershell.exe" — matching PowerShellRunner.RunScriptViaPwshAsync (:163) and every other system-tool caller (reg.exe, schtasks.exe, mdsched.exe, netplwiz.exe), which all carry the .exe suffix.
  • Fix-the-class: SystemPaths.ResolveSystemTool now also probes "<name>.exe" when handed an extension-less bare name, so a future caller that forgets the suffix cannot silently reopen the same hole.

Regression tests

  • ResolveSystemTool_BareNameWithoutExeSuffix_ResolvesToRootedExistingPath"powershell" must resolve to a rooted, existing powershell.exe. Red before (returned the unrooted bare name).
  • ResolveSystemTool_BareSystem32NameWithoutExe_ResolvesToRootedExistingPath"cmd" → full path to cmd.exe. Red before.

Checks

  • All 3 projects build 0 warnings / 0 errors
  • Leak scan on diff: clean
  • PROPAGATE: the remaining unqualified winget launches are a separate finding (winget is an App Execution Alias, not a System32 tool — needs a winget-specific resolver); tracked for its own PR.

…wershell launch

All three WindowsFeaturesService call sites (ListFeaturesAsync, EnableFeatureAsync,
DisableFeatureAsync — enable/disable run elevated) launched PowerShell as the bare
name "powershell". PowerShellRunner hardens the target via
SystemPaths.ResolveSystemTool, but that helper probed only the exact name via
File.Exists — File.Exists(...\WindowsPowerShell\v1.0\powershell) is false (the
file is powershell.exe), so both probes missed and it returned the UNROOTED bare
name. With UseShellExecute=false, Win32 CreateProcess then searches the calling
process's own directory first, so an attacker-planted powershell.exe beside
SysManager's portable .exe (often run from Downloads, sometimes elevated) would run
with the app's privileges — the same binary-planting/LPE class already fixed for
System32 tools (#3) and winget (#4).

Fix: all three sites now pass "powershell.exe" (matching
PowerShellRunner.RunScriptViaPwshAsync and every other system-tool caller).
Fix-the-class: SystemPaths.ResolveSystemTool now also probes "<name>.exe" for an
extension-less bare name, so a future caller can't silently reopen the hole.

Source: fresh multi-agent ultra-audit (security-deep dimension), confirmed by 3
adversarial verifier lenses (isReal=true, high confidence) against real source.

Regression tests: ResolveSystemTool_BareNameWithoutExeSuffix_ResolvesToRootedExistingPath
+ ResolveSystemTool_BareSystem32NameWithoutExe_ResolvesToRootedExistingPath — both
red before the fix (bare name returned unrooted), green after.
@laurentiu021 laurentiu021 merged commit 572678e into main Jul 10, 2026
4 checks passed
@laurentiu021 laurentiu021 deleted the fix/windowsfeatures-powershell-exe-lpe branch July 10, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant