Hardening

Scheduled Tasks and Autoruns: Auditing Weak Permissions

22 September 2026 8 min read

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, or Everyone has Write, WriteData, AppendData, or GenericWrite. 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.exe without quotes lets Windows try C:Program.exe first. If C: or C:Program Files is writable, that ambiguity is exploitable.
  • Weak ACL on the task or service object itself, not just its target. If Authenticated Users has Full Control or Write DAC on 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 HKLM by 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/__EventConsumer pairs 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 /v for a flat export of every task, its run-as account, and its action, then Get-Acl against each task’s path under WindowsSystem32Tasks for the underlying file permission.
  • Get-ScheduledTask | Get-ScheduledTaskInfo in 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 with sc.exe sdshow <service> to pull the raw service security descriptor (decode with ConvertFrom-SddlString in PowerShell rather than reading SDDL by eye).
  • Sysinternals accesschk.exe -qlc <servicename> and accesschk.exe -wqs "Authenticated Users" C:path to directly answer “who can write here” instead of manually parsing every ACE.
  • Sysinternals autorunsc.exe -a * -c -h -s -nobanner for 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_StartupCommand for a lightweight cross-check of Run-key-equivalent entries, and Get-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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 Files and update the service’s ImagePath, 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 qc after 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 -nobanner on 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.

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing