Skip to content

Elevate the standalone before it unpacks, into a locked directory - #50

Merged
BiosSystem merged 1 commit into
masterfrom
feature/standalone-elevate-first
Sep 17, 2026
Merged

BiosSystem merged 1 commit into
masterfrom
feature/standalone-elevate-first

Conversation

@BiosSystem

Copy link
Copy Markdown
Owner

What

Fixes a local privilege-escalation path in the standalone build (the shipped single-file artifact).

The wrapper extracted its base64 payload to a per-user %TEMP%\Winnow_Run_<guid> and ran the extracted Winnow.ps1 as the unelevated user; Ensure-Admin then self-elevated via -Verb RunAs. So the elevated run dot-sourced its ~100 scripts from a directory the standard user still controls. A local user (or malware running as them) could overwrite one of those files between extraction and execution and have it run with the elevated process's rights.

Same bug class as the watchdog ACL fix (#47): a privileged process trusting a location a non-admin can write.

Change

  • Check for admin at the top of the wrapper. If not elevated, relaunch the standalone itself through RunAs (passing args through with the existing Win32-safe quoting) and exit with the child's code.
  • Only extract once elevated, into a fresh directory locked to Administrators and SYSTEM (SetAccessRuleProtection($true,$false), inheritance off), so the user cannot write to it even though it lives under their %TEMP%.
  • Directory name switched from an 8-char guid fragment to a full guid.

No UX change: today's flow already produces a UAC prompt and an elevated window (Winnow self-elevates); this just moves elevation ahead of extraction.

The modular path (running Winnow.ps1 from a source checkout) is unchanged and still elevates from wherever the tree lives; that path is for people running from source who control their own environment. Noted in the ledger.

Validation

  • Tests\Invoke-StaticValidation.ps1 -RequirePSScriptAnalyzer clean (121 files).
  • Tests/Unit/Test-StandaloneWrapper.ps1 (Pester 5.7.1): 5/5 pass. Builds the real artifact and asserts it parses, elevates before it extracts (guard index precedes Expand-Archive), locks the extraction dir (SYSTEM + Administrators SIDs, SetAccessRuleProtection), and uses a full guid.
  • Tests/Unit/Test-UpdateWatchdog.ps1: 16/16 pass (no regression).
  • Inspected the generated wrapper: escaping intact, elevate block correct.

Not run here: the interactive RunAs elevation itself (needs a UAC prompt). New CI step added.

The standalone extracted its payload to a per-user %TEMP% directory and ran
Winnow.ps1 from there as the unelevated user; Winnow then self-elevated via
RunAs. That means the elevated run dot-sourced its ~100 scripts from a
directory the standard user still controls, so a local user could swap one
of those files between extraction and execution and have it run with the
elevated process's rights. Local privilege escalation.

Flip the order: check for admin at the top, relaunch the standalone itself
through RunAs if needed, and only extract once elevated. Extract into a
fresh directory locked to Administrators and SYSTEM with inheritance off, so
the user cannot write to it even though it sits under their %TEMP%. Switched
the directory name from an 8-char guid fragment to a full guid.

Added Test-StandaloneWrapper.ps1: builds the artifact and asserts it parses,
elevates before it extracts, and locks the extraction directory.
@BiosSystem
BiosSystem merged commit f87f921 into master Sep 17, 2026
1 check passed
@BiosSystem
BiosSystem deleted the feature/standalone-elevate-first branch September 17, 2026 13: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