A scheduled task privilege escalation path rarely comes from a missing patch. It comes from a permission that was left too open on something that runs automatically: a task action pointing at a script, a service binary path, a Run key value, a startup folder, or a WMI event subscription. Any principal who can write to what SYSTEM or an admin-context process executes next boot, next logon, or next timer tick can ride that execution to a higher privilege level. This is one of the most common local privilege escalation classes in real assessments, and one of the easiest to miss because vulnerability scanners look for missing patches, not misconfigured ACLs on autostart entries.
Autostart surfaces in one map: tasks, services, Run keys, startup folders, WMI subscriptions
Before auditing permissions, enumerate every mechanism that can execute code without direct interactive action from an attacker. On Windows there are five categories worth tracking as a single inventory, not five separate side projects.
| Mechanism | Where it lives | Typical execution context |
|---|---|---|
| Scheduled tasks | C:WindowsSystem32Tasks and Task Scheduler Library |
Often SYSTEM, sometimes a service account |
| Services | HKLMSYSTEMCurrentControlSetServices<name> |
SYSTEM, LocalService, NetworkService, or a domain account |
| Run / RunOnce keys | HKLM...Run and HKCU...Run |
Whichever user logs on; HKLM entries fire for every logon |
| Startup folders | Per-user and All Users Startup directories |
Same as Run keys, but file-based instead of registry-based |
| WMI event subscriptions | rootsubscription: __EventFilter, __EventConsumer, __FilterToConsumerBinding |
Depends on consumer type; CommandLineEventConsumer can run as SYSTEM |
Treat this table as the scope of an autorun audit. Each row has its own permission model (task ACL vs registry ACL vs NTFS ACL vs WMI namespace ACL), so a single tool rarely covers all five without help, which is why the collection step below combines several utilities rather than one.
Permission mistakes that let a standard user rewrite what SYSTEM executes
The failure pattern is consistent across all five surfaces: a non-admin principal has write access to something a high-privilege process will read or execute unattended. The specific mistakes worth checking for are:
- Writable target binary or script. A scheduled task or service points to a path where
Authenticated Users,Users, orEveryonehasWrite,WriteData,AppendData, orGenericWrite. Replacing the file gives you whatever the task’s run-as identity has. - Writable parent directory with a missing file. A service or task references a path that does not exist yet, but the folder above it is writable. Creating the file plants the payload before the next execution.
- Unquoted service path with an embedded space.
C:Program FilesAppservice host.exewithout quotes lets Windows tryC:Program.exefirst. IfC:orC:Program Filesis writable, that ambiguity is exploitable. - Weak ACL on the task or service object itself, not just its target. If
Authenticated UsershasFull ControlorWrite DACon the scheduled task definition, an attacker can change the action to any command without ever touching the original binary. - Writable Run/RunOnce key or registry hive permissions. Rare on
HKLMby default, but common on machines that had software installed with overly broad ACL grants left behind. - Shared or misconfigured Startup folder ACLs on multi-user hosts, particularly RDS/terminal servers where a standard user can drop a file into a path another user’s session will execute.
- WMI namespace permissions that allow a non-admin to create
__EventFilter/__EventConsumerpairs bound to a high-privilege consumer type.
None of these require a kernel exploit. They require icacls, a text editor, and patience, which is exactly why they show up in real intrusions as a lateral step after initial low-privilege access.
Collecting the inventory with built-in tooling and normalizing it across hosts
You do not need third-party agents to build the first pass of this inventory. Native tooling is enough to get a defensible dataset:
schtasks /query /fo csv /vfor a flat export of every task, its run-as account, and its action, thenGet-Aclagainst each task’s path underWindowsSystem32Tasksfor the underlying file permission.Get-ScheduledTask | Get-ScheduledTaskInfoin PowerShell for a structured object you can pipe into a CSV or a database table instead of parsing text.sc.exe qc <service>for the binary path and start type, paired withsc.exe sdshow <service>to pull the raw service security descriptor (decode withConvertFrom-SddlStringin PowerShell rather than reading SDDL by eye).- Sysinternals
accesschk.exe -qlc <servicename>andaccesschk.exe -wqs "Authenticated Users" C:pathto directly answer “who can write here” instead of manually parsing every ACE. - Sysinternals
autorunsc.exe -a * -c -h -s -nobannerfor a single CSV covering Run keys, startup folders, services, tasks, and more, including SHA256 hashes so you can diff by hash later. Get-CimInstance Win32_StartupCommandfor a lightweight cross-check of Run-key-equivalent entries, andGet-CimInstance -Namespace rootsubscription -ClassName __EventFilter(plus the consumer and binding classes) for WMI persistence.
Normalize the output into one table keyed by hostname, mechanism, path_or_key, run_as, sha256, acl_summary. The ACL summary matters more than the raw SDDL string for triage: reduce each entry to whether any non-admin SID has write-equivalent rights, and if so, which SID and which right. Store the hash alongside the ACL so a later diff can tell “permission changed” apart from “binary changed” apart from “both changed,” which changes what you investigate first.
Triage rules: which findings are exploitable today vs theoretical
A writable ACL is not automatically a scheduled task privilege escalation. Apply a short set of rules before opening a ticket:
- Confirm the run-as identity is actually higher privilege than the writer. A task that runs as the same standard user who can already write to it is not a privilege boundary crossing.
- Confirm the write right is real, not inherited noise. Test it: attempt to append a byte to the target file (or create the missing file) as a low-privilege test account, rather than trusting an ACL viewer’s summary line.
- Confirm the trigger will fire without additional access. A task set to run “only when a specific user is logged on” who never logs on interactively is a lower priority than one on a logon or timer trigger that fires unconditionally.
- Check for compensating controls. AppLocker or WDAC policies that block execution from the writable path, or a file integrity monitor that would catch the swap before the next trigger, downgrade urgency but do not close the finding.
- Separate “exploitable today” from “exploitable after a future install.” A missing file in a writable parent directory pointed to by a currently-disabled task is theoretical until something re-enables that task, but it still belongs in the backlog with a lower score, not off the list.
This is also where findings from different collection scripts need deduplication, since the same misconfigured directory often shows up once as a service target and once as a scheduled task target. Platforms that automate triage across large host counts, such as SITEY, apply this kind of exploitability scoring automatically and correlate the same underlying writable path across multiple autorun mechanisms instead of raising one alert per matching ACL, which is what keeps a hardening backlog from drowning in duplicate low-value tickets on a fleet of any real size.
Remediation patterns that survive vendor updates and reimaging
The fastest fix, running icacls once to strip the offending ACE, is also the least durable one. Vendor updates that reinstall a service, or a reimage that restores a golden configuration, silently reintroduce the same permission. Build remediation around patterns that hold up over time:
- Move the target, don’t just fix its ACL. If a service points to a binary under a user-writable directory, relocate the binary to
Program Filesand update the service’sImagePath, rather than tightening permissions on a directory whose purpose was to be writable. - Fix at the source of provisioning. If the weak ACL came from an installer or a Group Policy Preference, correct the installer’s post-install script or the GPP item itself, not just the live systems it already touched, or the next deployment reintroduces the finding.
- Quote every service path with spaces and confirm with
sc qcafter the change; this closes the unquoted-path class outright rather than relying on directory permissions to compensate. - Push corrected ACLs through Group Policy or a configuration management tool (DSC, Ansible, whatever the environment already uses) so the fix is declarative and gets reapplied automatically if drift occurs.
- Verify the fix by testing write access again as the low-privilege account, not by re-reading the ACL. An ACL viewer can report a change was applied while an inherited permission from a parent object still grants access.
Re-testing matters more here than in most vulnerability classes, because a patch to the ACL and a patch to the underlying directory structure can both report success while leaving a different inherited path still open. Platforms built around an automated retest phase, such as SITEY, re-check the specific writable path after remediation instead of trusting that a “fixed” status flag was set correctly, which catches the cases where the ACE was removed from one object but an inherited permission on a parent folder still grants the same access.
Baselining autoruns so new entries are visible instead of invisible
A one-time audit finds today’s misconfigurations. It does not catch the task a new installer adds next month with the same weak permission pattern. Baseline autoruns the same way you would baseline firewall rules or local admin group membership:
- Run
autorunsc.exe -a * -c -h -s -nobanneron a fixed schedule (weekly is reasonable for workstations, daily for servers with frequent deployments) and store the CSV with a timestamp. - Diff each run against the previous one on the combination of path, run-as account, and SHA256. A new entry, a changed hash on an existing entry, or a changed ACL summary on an existing entry are each worth a distinct alert, since they represent different risks (new persistence, tampered binary, and loosened permission respectively).
- Cross-reference new entries against your change management records. An entry that correlates with an approved software deployment is noise; one that does not is worth immediate investigation, ideally same-day given how directly this maps to privilege escalation.
- Feed the baseline into whatever holds your host inventory so autorun coverage tracks with asset coverage rather than living in a separate spreadsheet nobody remembers to update after a host is decommissioned or added.
The event log also helps close the loop: task creation and modification generate event IDs 4698 and 4702 (with the appropriate audit policy enabled), and service installation generates 7045. Forwarding these to your logging pipeline gives you near-real-time detection between baseline runs, rather than waiting for the next scheduled diff to notice that a task’s action changed three days ago.
About SITEY
SITEY is an autonomous vulnerability management platform. It discovers, validates, prioritizes, remediates and re-tests vulnerabilities through an eight-phase automated pipeline, unifying output from 17 integrated scanners. SITEY is self-hosted: it runs in your own infrastructure and your findings are stored there. Outbound connections are limited to licence activation and the optional services you enable, such as an AI provider, CVE enrichment and patch catalogues. Pricing is 599 USD per month or 5,999 USD for a perpetual lifetime license. See pricing or how the platform works.