Windows Scheduled Tasks
Introduction
Scheduled tasks allow programs or scripts to be executed automatically according to defined triggers.
Fundamental Concepts
Components of a Task
- Trigger: When the task runs
- Action: What the task does
- Conditions: Additional criteria
- Settings: Execution options
Types of Triggers
| Type | Description |
|---|---|
| AtStartup | At system startup |
| AtLogOn | At user logon |
| Once | One time only |
| Daily | Daily |
| Weekly | Weekly |
| OnEvent | On a specific event |
| OnIdle | When the system is idle |
Management via GUI
Task Scheduler (taskschd.msc)
Win+R > taskschd.msc
Structure:
- Task Scheduler Library
- Microsoft (system tasks)
- Custom folders
Create a Basic Task
- Action > Create Basic Task
- Follow the wizard
- Define name, trigger, action
Create an Advanced Task
- Action > Create Task
- General tab: Name, account, privileges
- Triggers tab: Triggers
- Actions tab: Programs to run
- Conditions tab: Criteria
- Settings tab: Options
Management via PowerShell
List Tasks
# All tasks
Get-ScheduledTask
# Active tasks
Get-ScheduledTask | Where-Object {$_.State -eq "Ready"}
# Search by name
Get-ScheduledTask -TaskName "*Update*"
# Task details
Get-ScheduledTask -TaskName "TaskName" | Get-ScheduledTaskInfo
Create a Task
# Define the action
$action = New-ScheduledTaskAction `
-Execute "powershell.exe" `
-Argument "-File C:\Scripts\MyScript.ps1"
# Define the trigger
$trigger = New-ScheduledTaskTrigger -Daily -At "08:00"
# Define the principal (who runs it)
$principal = New-ScheduledTaskPrincipal `
-UserId "SYSTEM" `
-RunLevel Highest
# Options
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries
# Create the task
Register-ScheduledTask `
-TaskName "MyTask" `
-Action $action `
-Trigger $trigger `
-Principal $principal `
-Settings $settings `
-Description "Description of my task"
Types of Triggers
# At startup
$trigger = New-ScheduledTaskTrigger -AtStartup
# At logon
$trigger = New-ScheduledTaskTrigger -AtLogOn
# Once
$trigger = New-ScheduledTaskTrigger -Once -At "2025-01-15 10:00"
# Daily
$trigger = New-ScheduledTaskTrigger -Daily -At "08:00"
# Weekly
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At "09:00"
# On an event
$trigger = New-ScheduledTaskTrigger -AtStartup
# (for specific events, use schtasks.exe)
Modify a Task
# Change the trigger
$newTrigger = New-ScheduledTaskTrigger -Daily -At "10:00"
Set-ScheduledTask -TaskName "MyTask" -Trigger $newTrigger
# Enable/Disable
Enable-ScheduledTask -TaskName "MyTask"
Disable-ScheduledTask -TaskName "MyTask"
Delete a Task
Unregister-ScheduledTask -TaskName "MyTask" -Confirm:$false
Run Manually
Start-ScheduledTask -TaskName "MyTask"
Management via CMD
SCHTASKS Command
:: List tasks
schtasks /query
:: Task details
schtasks /query /tn "TaskName" /v /fo LIST
:: Create a task
schtasks /create /tn "MyTask" /tr "C:\program.exe" /sc daily /st 08:00
:: Run
schtasks /run /tn "MyTask"
:: Delete
schtasks /delete /tn "MyTask" /f
:: Modify
schtasks /change /tn "MyTask" /st 10:00
Common Options
/sc Schedule type (minute, hourly, daily, weekly, monthly, once, onstart, onlogon)
/st Start time (HH:MM)
/sd Start date (MM/DD/YYYY)
/ru Run as user
/rp Run as password
/rl Run level (LIMITED, HIGHEST)
Security of Scheduled Tasks
Risks
Scheduled tasks are a very common persistence vector used by attackers:
- Run automatically
- Can have elevated privileges
- Often ignored during audits
Security Checks
# Tasks running as SYSTEM
Get-ScheduledTask | ForEach-Object {
$task = $_
$principal = $task.Principal
if ($principal.UserId -eq "SYSTEM" -or $principal.UserId -like "*SYSTEM*") {
[PSCustomObject]@{
Name = $task.TaskName
Path = $task.TaskPath
User = $principal.UserId
}
}
} | Format-Table
# Tasks with executables in suspicious paths
Get-ScheduledTask | ForEach-Object {
$actions = $_.Actions
foreach ($action in $actions) {
if ($action.Execute -like "*\Temp\*" -or
$action.Execute -like "*\AppData\*" -or
$action.Execute -like "*\Downloads\*") {
[PSCustomObject]@{
Task = $_.TaskName
Execute = $action.Execute
Arguments = $action.Arguments
}
}
}
} | Format-Table
# Tasks created recently (last 24h)
Get-ScheduledTask | Where-Object {
$_.Date -gt (Get-Date).AddDays(-1)
}
Indicators of Compromise (IOC)
| Indicator | Description |
|---|---|
| Task with random name | Possible malware |
| Executable in Temp | Suspicious |
| Encoded PowerShell | Very suspicious |
| Hidden task | Evasion technique |
| SYSTEM running user scripts | High risk |
Hardening
- Regularly audit scheduled tasks
- Limit who can create tasks
- Monitor task creations (Event ID 4698)
- Use dedicated service accounts
Persistence via Scheduled Tasks
Attack Technique (to know for defense)
# Example of malicious persistence (DO NOT USE)
# Shows how attackers proceed
$action = New-ScheduledTaskAction `
-Execute "powershell.exe" `
-Argument "-WindowStyle Hidden -EncodedCommand <base64>"
$trigger = New-ScheduledTaskTrigger -AtStartup
Register-ScheduledTask `
-TaskName "WindowsUpdateHelper" `
-Action $action `
-Trigger $trigger `
-Principal (New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest) `
-Force
Detection
# Script to detect suspicious tasks
$suspicious = @()
Get-ScheduledTask | ForEach-Object {
$task = $_
$isSuspicious = $false
$reasons = @()
# Check actions
foreach ($action in $task.Actions) {
# PowerShell with encoded commands
if ($action.Arguments -match "-[Ee]ncoded") {
$isSuspicious = $true
$reasons += "EncodedCommand"
}
# Executables in suspicious paths
if ($action.Execute -match "\\(Temp|Downloads|AppData)\\") {
$isSuspicious = $true
$reasons += "Suspicious path"
}
# Names mimicking Windows services
if ($task.TaskName -match "^(WindowsUpdate|MicrosoftEdge|Chrome|Adobe).*\d{4,}") {
$isSuspicious = $true
$reasons += "Suspicious name"
}
}
if ($isSuspicious) {
$suspicious += [PSCustomObject]@{
TaskName = $task.TaskName
TaskPath = $task.TaskPath
Reasons = ($reasons -join ", ")
}
}
}
if ($suspicious) {
Write-Host "Suspicious tasks detected:" -ForegroundColor Red
$suspicious | Format-Table -AutoSize
} else {
Write-Host "No suspicious tasks detected." -ForegroundColor Green
}
Logging
Event IDs
| Event ID | Log | Description |
|---|---|---|
| 4698 | Security | Task created |
| 4699 | Security | Task deleted |
| 4700 | Security | Task enabled |
| 4701 | Security | Task disabled |
| 4702 | Security | Task updated |
| 106 | TaskScheduler/Operational | Task registered |
| 140 | TaskScheduler/Operational | Task updated |
| 141 | TaskScheduler/Operational | Task deleted |
| 200 | TaskScheduler/Operational | Action started |
| 201 | TaskScheduler/Operational | Action completed |
Enable Auditing
# Enable auditing of scheduled tasks (GPO or command)
auditpol /set /subcategory:"Other Object Access Events" /success:enable /failure:enable
View Logs
# Task creation events
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
ID = 4698
} -MaxEvents 10 | Format-Table TimeCreated, Message -Wrap
# Operational logs
Get-WinEvent -LogName 'Microsoft-Windows-TaskScheduler/Operational' -MaxEvents 20
Exercises
- Create a task that runs a PowerShell script every day at 9 AM
- Create a task that starts at system boot
- Audit existing tasks to identify suspicious ones
- View task creation logs from the last 7 days