Skip to content

v2026.09.26.001: harden C:\ProgramData\ShellKnight ACL against local privilege escalation - #6

Open
cdburgess75 wants to merge 2 commits into
fix/intel-engine-loadfrom
fix/programdata-acl
Open

cdburgess75 wants to merge 2 commits into
fix/intel-engine-loadfrom
fix/programdata-acl

Conversation

@cdburgess75

Copy link
Copy Markdown
Owner

Suspected issue (not yet confirmed on real Windows)

C:\ProgramData\ShellKnight\ holds config.json (read at startup), run.ps1 (the native ShellKnight scheduled task executes it as SYSTEM every 8 h), and the Logs, JSON and Intel folders. By default ProgramData lets BUILTIN\Users create files and folders in its subfolders, and CREATOR OWNER gets full control of what they create.

So on an endpoint where ShellKnight has never run, a standard user can pre-create C:\ProgramData\ShellKnight (or run.ps1, or config.json) and own it. They could then:

  • change the script SYSTEM executes by owning/replacing run.ps1 -> local privilege escalation;
  • redirect the run report and the tenant API key to a URL of their own via config.json (BattlefieldURL / BattlefieldApiKey).

Once SYSTEM has created a file a user cannot modify it, but if the user owns the folder they can still delete and replace its files.

This is the same class of exposure PR #5 already guards for the Intel cache (owner must be S-1-5-18 or S-1-5-32-544); this PR applies that model to the whole state directory and the two sensitive files, and locks the folder ACL down so the create right does not exist in the first place.

What this changes

A new guard runs during config load, before config.json is read or the scheduled task is trusted (verified by a source-order assertion in the test):

  • Explicit ACL, set by SID (icacls, never localized names): SYSTEM (S-1-5-18) and Administrators (S-1-5-32-544) full control, Users (S-1-5-32-545) read/execute, inheritance removed (/inheritance:r) so ProgramData's Users-create ACEs are gone, owner reclaimed to Administrators. Set on the base folder only; existing Logs/JSON/Intel children pick up the change through inheritance, so no /T sweep runs on every check-in.
  • A folder a user already owns is removed and recreated (a user-owned tree has no run history worth keeping and may carry ACEs the guard cannot enumerate).
  • config.json and run.ps1 are owner-checked: either one not owned by SYSTEM or Administrators is deleted, so a planted config.json is never read and a planted run.ps1 is never executed. run.ps1 is rewritten later in the run; a removed config.json falls back to built-in defaults and env vars.
  • Owner read with (Get-Acl).GetOwner([SecurityIdentifier]), the PR v2026.09.25.004: the Intel Engine loads threat intel; every intel match is report-only #5 model; an unreadable owner is treated as untrusted. The whole guard is wrapped so a hardening failure logs a warning and never stops the run.
  • New payload fields health.state_dir_repaired and health.state_dir_files_removed so Battlefield can flag an endpoint that showed signs of a local tampering attempt (report stored whole per ADR 0002; nothing displays them yet).

Verification status

  • [System.Management.Automation.Language.Parser]::ParseFile clean
  • All six tests/Test-*.ps1 pass under pwsh (incl. Test-IntelEngine's whole-script $Script:Config.<Name> AST check)
  • PSScriptAnalyzer: 0 errors (warnings are the same Write-Host / ShouldProcess / singular-noun classes the existing script already triggers)
  • New tests/Test-StateDirGuard.ps1 (18 assertions): fresh box, steady state, SYSTEM- and Administrators-owned, user-owned (rebuilt), unreadable owner, trusted/user/unknown/absent config.json and run.ps1, Test-TrustedOwner, icacls args are *-prefixed SIDs with no localized principal, and the source-order guarantee
  • NOT yet run on a real Windows 10/11 endpoint. Get-Acl, icacls and NTFS inheritance are Windows behaviours the test mocks. Per the repo rule, this needs one real SYSTEM run before it reaches main — please do not merge until then.

Suggested real-Windows checks

  1. icacls C:\ProgramData on a stock box to confirm the inherited Users create ACEs ((CI)(IO)(WD) on subfolders, (CI)(WD), plus CREATOR OWNER:(OI)(CI)(IO)(F)).
  2. As a standard user, mkdir C:\ProgramData\ShellKnight (or drop a run.ps1) before ShellKnight's first run; confirm the user owns it (icacls / Get-Acl).
  3. Run ShellKnight as SYSTEM; confirm the folder is reclaimed, the ACL becomes SYSTEM:(OI)(CI)(F) Administrators:(OI)(CI)(F) Users:(OI)(CI)(RX) with inheritance off, the planted file is gone, and the run report shows state_dir_repaired: true.
  4. Confirm a normal SYSTEM run on an already-hardened box logs State directory ... owner and ACL verified and reports state_dir_repaired: false, state_dir_files_removed: 0.

Stacked on #5 (base fix/intel-engine-load).

🤖 Generated with Claude Code

…privilege escalation

By default ProgramData lets BUILTIN\Users create files and folders in its
subfolders, and CREATOR OWNER gets full control of what they create. On a box
where ShellKnight had never run, a standard user could pre-create
C:\ProgramData\ShellKnight (or run.ps1 / config.json) and own it, then choose
the code the SYSTEM scheduled task runs (local privilege escalation) or redirect
the run report and its API key through config.json.

Before config.json is read or the scheduled task (which runs run.ps1) is
trusted, a new guard:
- creates or repairs the folder with an explicit ACL set by SID
  (SYSTEM / Administrators full control, Users read only), inheritance removed
  so ProgramData's Users-create ACEs are gone, owner reclaimed to Administrators;
- removes and recreates a folder that SYSTEM or Administrators does not own;
- deletes any config.json or run.ps1 not owned by SYSTEM or Administrators, so a
  planted config.json is never read and a planted run.ps1 is never executed.

Owner is read with (Get-Acl).GetOwner, the model the Intel cache check uses
(v2026.09.25.004); an unreadable owner is untrusted; icacls uses SIDs
(S-1-5-18 / S-1-5-32-544 / S-1-5-32-545), never localized names. The health
object gains state_dir_repaired and state_dir_files_removed so the dashboard can
flag an endpoint that showed signs of tampering.

New tests/Test-StateDirGuard.ps1 runs the extracted guard verbatim under
StrictMode 2 with Get-Acl and icacls mocked and real temp directories. NOT yet
run on real Windows: the ACL and owner checks need one real SYSTEM run before
this reaches main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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