Get-ScheduledTask: find suspicious scheduled tasks on Windows

Get-ScheduledTask: find suspicious scheduled tasks on Windows

Malware that runs once and disappears is less worrying than malware that comes back after a reboot. One of the cheapest ways to guarantee that return is to create a scheduled task: it fires at logon, every hour or every Monday, without asking the user anything. If you have never looked at the task list on your Windows machine, you probably don't know what's in it. PowerShell answers that in one line.

Why do scheduled tasks matter for security?

Task Scheduler is a legitimate Windows feature and runs with whatever privileges the task defines, including SYSTEM. That's why it shows up as a persistence and execution technique in MITRE ATT&CK, catalogued as T1053.005 (Scheduled Task). A malicious task usually has a name that looks like something from the system, and what it runs is a powershell.exe with long arguments, a script in %APPDATA% or a binary in a temp folder.

The Get-ScheduledTask cmdlet is part of the ScheduledTasks module, available since Windows 8 and Windows Server 2012, according to Microsoft's documentation. It returns the definition of the tasks registered on the computer, while the run data (last result, next run time) comes from another cmdlet, Get-ScheduledTaskInfo.

How do you list all scheduled tasks with PowerShell?

How do you see Windows scheduled tasks with PowerShell?

Run Get-ScheduledTask in an elevated PowerShell. It lists the name, path, state and description of every registered task. Unfiltered, the output runs past a hundred lines, almost all of them from Microsoft itself.

# All tasks, sorted by state
Get-ScheduledTask | Sort-Object State | Format-Table TaskName, TaskPath, State -AutoSize

# Only the ones that are enabled and ready to run
Get-ScheduledTask | Where-Object State -eq 'Ready'

The TaskPath field helps the most. Native tasks live under \Microsoft\Windows\.... Whatever is left outside that is either third-party software (Chrome, Office, drivers) or something worth a closer look.

# Tasks outside the Microsoft folder
Get-ScheduledTask |
    Where-Object { $_.TaskPath -notlike '\Microsoft\*' } |
    Format-Table TaskName, TaskPath, State -AutoSize

Heads up: an attacker can register the task inside \Microsoft\Windows\ precisely to blend in. Filtering by path is a first cut, not a guarantee.

What each task actually runs

The task name tells you nothing. What matters is the action: which program runs and with which arguments. That information is in the Actions property, and the account used is in Principal.

Get-ScheduledTask | ForEach-Object {
    [PSCustomObject]@{
        Tarefa    = $_.TaskName
        Caminho   = $_.TaskPath
        Executa   = ($_.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }) -join ' | '
        Conta     = $_.Principal.UserId
        Elevacao  = $_.Principal.RunLevel
    }
} | Where-Object { $_.Caminho -notlike '\Microsoft\*' } | Format-List

Watch for these signs in the Executa column (the property names are in Portuguese in this script; rename them if you like):

  • powershell.exe or cmd.exe with encoded arguments (-enc, -EncodedCommand) or with -WindowStyle Hidden;
  • binaries in user-writable folders: %TEMP%, %APPDATA%, C:\Users\Public, C:\ProgramData;
  • mshta.exe, wscript.exe, regsvr32.exe, rundll32.exe calling something remote or an odd file;
  • SYSTEM account with RunLevel Highest on a task you don't recognize;
  • a name that imitates the system with a subtle typo, or an empty description.

When did the task last run?

Combine it with Get-ScheduledTaskInfo to see recent history. The cmdlet's documentation describes it as returning the last run information, and it accepts pipeline input straight from Get-ScheduledTask.

Get-ScheduledTask |
    Where-Object { $_.TaskPath -notlike '\Microsoft\*' } |
    Get-ScheduledTaskInfo |
    Select-Object TaskName, LastRunTime, LastTaskResult, NextRunTime |
    Sort-Object LastRunTime -Descending

A LastTaskResult of 0 means the last run finished without error. An unknown task that runs successfully every hour is more suspicious than one that never ran.

How do you know when a task was created?

Windows logs task creation in the Security log with Event ID 4698, as long as the "Other Object Access Events" audit policy is turned on. Without that policy there is no record, and you only find the task by looking at the list.

# Scheduled task creation in the last 30 days
Get-WinEvent -FilterHashtable @{
    LogName   = 'Security'
    Id        = 4698
    StartTime = (Get-Date).AddDays(-30)
} | Select-Object TimeCreated, Message

The message field carries the task name, the account that created it and the full XML with the action. If that command returns nothing, it's worth enabling the audit. The post on auditing Windows security logs with PowerShell shows how, and the one on Get-WinEvent explains the filters used above.

Disable, export and remove safely

Found a strange task? Before deleting it, preserve the evidence. Export-ScheduledTask returns the definition XML, and you save it to a file:

# 1. Save the definition (evidence)
Export-ScheduledTask -TaskName "TaskName" -TaskPath "\" | Out-File "$env:USERPROFILE\Desktop\suspicious-task.xml"

# 2. Disable without deleting
Disable-ScheduledTask -TaskName "TaskName" -TaskPath "\"

# 3. Only after investigating: remove
Unregister-ScheduledTask -TaskName "TaskName" -TaskPath "\" -Confirm:$false

Deleting the task doesn't solve the problem if the file it runs is still on disk, or if other persistence mechanisms exist. Hash the binary with Get-FileHash, check it on VirusTotal and also review services with Get-Service. If the machine matters, treat it as an incident: isolate it from the network and change the passwords used on it.

Quick audit checklist

  1. List the tasks outside \Microsoft\ and identify each one.
  2. Check the Execute and Arguments of all of them, including those inside the Microsoft folder.
  3. Distrust any action pointing to a user-writable folder.
  4. Cross-check with LastRunTime and Event ID 4698.
  5. Export the XML, disable, investigate and only then remove.

Conclusion

A scheduled task is cheap, quiet persistence that looks legitimate to the system, which is exactly why the list is worth reviewing now and then. Three cmdlets (Get-ScheduledTask, Get-ScheduledTaskInfo and Export-ScheduledTask) cover the whole audit. For the other side of the coin, creating legitimate tasks from the terminal, read the guide on schtasks for scheduling tasks on Windows.

Comments