Home / Blog / Threat Hunting
How to Hunt Malicious and Hidden Scheduled Tasks on Windows
A practical hunt for T1053.005: stack scheduled tasks across hosts, spot risky actions, catch tasks hidden by SD value removal and sweep the registry for what your logs missed.
Scheduled tasks give attackers a simple way to run code and survive a reboot, and they blend in well because Windows and most software create plenty of their own. This hunt is for analysts and threat hunters who want to find the few tasks that do not belong, including tasks hidden from the usual admin tools.
Scenario: An intrusion report describes an actor persisting through scheduled tasks with names that imitate Windows updates, launched from user-writable paths. You have no matching alerts. Your lead asks you to check whether anything similar exists across 3,000 Windows endpoints, including tasks that schtasks /query would not show.
Form your hunt hypotheses
A hunt across all tasks without a clear question quickly becomes a noisy list. Start with specific, testable hypotheses, then choose the data that can prove or disprove each one.
Confirm your data sources
Check your visibility first, because a clean result means little if the logs were never collected. Microsoft notes that neither Event 4698 in the Security log nor the Task Scheduler Operational log is audited by default.
If auditing is missing, raise the gap and agree a fix with the team that owns your endpoint policy. On a single test host, the settings look like this:
auditpol /set /subcategory:"Other Object Access Events" /success:enable
wevtutil sl Microsoft-Windows-TaskScheduler/Operational /e:true
Watch out: Sysmon only logs what its configuration includes. Before you rely on a Sysmon hunt, confirm your config captures registry events under \Schedule\TaskCache\Tree\. Otherwise, a lack of results tells you nothing.
Stack tasks to find the rare ones
Stacking groups events by task name and counts the hosts involved. Tasks deployed by IT tend to show up widely, while targeted persistence tends to sit at the low end. Low prevalence is a lead, not a verdict, because small teams install niche software too.
Defender for Endpoint
DeviceEvents
| where Timestamp > ago(30d)
| where ActionType in ("ScheduledTaskCreated", "ScheduledTaskUpdated")
| extend Fields = parse_json(AdditionalFields)
| extend TaskName = tostring(Fields.TaskName),
TaskContent = tostring(Fields.TaskContent)
| summarize Hosts = dcount(DeviceName), Events = count(),
FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
Creators = make_set(InitiatingProcessAccountName, 10),
SampleContent = take_any(TaskContent)
by TaskName
| where Hosts <= 3
| sort by Hosts asc, FirstSeen desc
Splunk
This version assumes the Windows add-on's XML format. Your index and field names may differ.
index=wineventlog source="XmlWinEventLog:Security" EventCode IN (4698, 4702)
| stats dc(host) as hosts count min(_time) as firstSeen max(_time) as lastSeen
values(SubjectUserName) as creators by TaskName
| where hosts <= 3
| sort hosts, -firstSeen
| convert ctime(firstSeen) ctime(lastSeen)
Tip: Tasks with random suffixes, such as per-user updater tasks, can fragment the count and look rare. Normalise names by stripping GUIDs and SIDs if you see this, and keep your list of benign patterns so the next hunt starts ahead.
Inspect task actions and context
Next, look at what each task runs. The Security events store the XML in TaskContent for 4698 and TaskContentNew for 4702. In Sentinel, the SecurityEvent table has no dedicated columns for these fields, so extract them from EventData.
SecurityEvent
| where TimeGenerated > ago(30d)
| where EventID in (4698, 4702)
| extend TaskName = extract(@'<Data Name="TaskName">([^<]+)</Data>', 1, EventData)
| extend TaskContent = extract(@'(?s)<Data Name="TaskContent(?:New)?">(.*?)</Data>', 1, EventData)
| where TaskContent matches regex @'(?i)(powershell|pwsh|cmd\.exe|mshta|rundll32|regsvr32|wscript|cscript|certutil|bitsadmin)'
or TaskContent matches regex @'(?i)\\(Users\\Public|AppData|ProgramData|Temp)\\'
or TaskName matches regex @'^\\[^\\]+$'
| project TimeGenerated, Computer, EventID, SubjectUserName, TaskName, TaskContent
| sort by TimeGenerated desc
The last condition matches tasks in the Task Scheduler Library root, which Microsoft's Event 4698 guidance recommends monitoring. It will match some legitimate software too, so review the results in context.
Review every candidate against these questions:
- Does the name imitate a Microsoft task but sit outside the usual
\Microsoft\Windows\path? - Does the action use encoded commands, download content or call a script from a user-writable folder?
- Does it run as SYSTEM or with highest privileges when its purpose does not need that?
- Does the task XML contain
<LogonType>Password</LogonType>, meaning that credentials were stored with the task? - Who created it, from which process, and was it a remote or unusual logon session?
- Does the binary it launches have a valid signature and a known prevalence in your estate?
To see tasks in action rather than just their creation, look for suspicious children of the Task Scheduler service.
DeviceProcessEvents
| where Timestamp > ago(14d)
| where InitiatingProcessFileName =~ "svchost.exe"
| where InitiatingProcessCommandLine has "Schedule"
| where FileName in~ ("powershell.exe", "pwsh.exe", "cmd.exe", "mshta.exe",
"rundll32.exe", "regsvr32.exe", "wscript.exe", "cscript.exe",
"certutil.exe", "bitsadmin.exe")
| summarize Runs = count(), Hosts = dcount(DeviceName),
FirstSeen = min(Timestamp), LastSeen = max(Timestamp)
by FileName, ProcessCommandLine, AccountName
| sort by Hosts asc, Runs asc
Hunt for hidden tasks
Every task has registry entries under the TaskCache keys. In 2022, Microsoft documented the Tarrask malware deleting a task's Security Descriptor (SD) value, which makes the task disappear from schtasks /query and Task Scheduler. Deleting the value needs SYSTEM context, so a successful deletion also suggests the attacker already holds high privileges on that host.
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\<TaskName>
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks\{GUID}
Catch the deletion in logs
Legitimate software has little reason to remove an SD value directly, so treat any hit as high priority until it is explained.
DeviceRegistryEvents
| where Timestamp > ago(30d)
| where ActionType == "RegistryValueDeleted"
| where RegistryKey contains @"\Schedule\TaskCache\Tree\"
or PreviousRegistryKey contains @"\Schedule\TaskCache\Tree\"
| where RegistryValueName == "SD" or PreviousRegistryValueName == "SD"
| project Timestamp, DeviceName, RegistryKey, PreviousRegistryKey,
InitiatingProcessAccountName, InitiatingProcessFileName,
InitiatingProcessCommandLine
index=sysmon source="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=12
EventType=DeleteValue TargetObject="*\\Schedule\\TaskCache\\Tree\\*\\SD"
| table _time, host, User, Image, TargetObject
Sweep hosts directly
Logs only show what happened during your retention period, while a hidden task may be older. Microsoft recommends checking the Tree key for tasks without an SD value. This read-only script does that and also flags Index set to 0, because MITRE notes that altered metadata such as Index can also hide tasks.
$root = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree'
Get-ChildItem -Path $root -Recurse | ForEach-Object {
$names = $_.GetValueNames()
# Task keys have an Id value, folder keys do not
if ($names -contains 'Id') {
$missingSd = $names -notcontains 'SD'
$index = $_.GetValue('Index')
if ($missingSd -or $index -eq 0) {
[pscustomobject]@{
Computer = $env:COMPUTERNAME
TaskKey = $_.Name
TaskId = $_.GetValue('Id')
Index = $index
MissingSD = $missingSd
}
}
}
}
Deploy it through your EDR live response or management tooling and collect the output centrally. Run it on a known-good build first, so you understand what a normal result looks like in your environment. For each hit, use TaskId to find the matching Tasks\{GUID} key, then collect any XML file under C:\Windows\System32\Tasks.
Watch out: Do not delete a suspicious task as soon as you find it. Record the registry keys, the task file, the binary it runs and any related logs first. Removing it early destroys evidence and may alert the attacker before you understand the scope.
Grade findings and respond
Grade each candidate on the evidence you have, then hand over anything confirmed with enough detail for responders to act.
Unusual
A rare task, but with a signed binary, a known owner and a clear business purpose.
Niche vendor updaterSuspicious
A risky action or unusual path, with no owner yet. Investigate the host, creator and binary.
Script run from ProgramDataMalicious
A hidden task, known bad content or links to other intrusion activity. Escalate as an incident.
SD value removed by SYSTEMWhen a Grade 3 finding comes up, widen the search. Use the task name, command line, file hash and creating account as pivots across all hosts. Because removing the SD value requires SYSTEM context, also look for how the attacker gained that level of access.
Turn the hunt into detections
A hunt delivers lasting value when its best logic becomes a monitored rule. Some of these checks are precise enough to alert on, while others work better as scheduled reports.
| Hunt logic | Expected noise | Suggested use |
|---|---|---|
| SD value deleted under TaskCache\Tree | Low | High priority alert |
| Task XML with LogonType set to Password | Low to medium | Alert, with exceptions for known service tasks |
| Task action runs a script host from a user-writable path | Medium | Alert after tuning |
| New task in the Task Scheduler Library root | Medium to high | Daily review |
| Task seen on three or fewer hosts | High | Weekly hunt report |
Write down the data sources, time range, hypotheses and findings, including anything you could not test. A hunt that finds nothing is still useful if it records what was checked and which visibility gaps remain.
Key takeaways
- Confirm that Event 4698, Task Scheduler Operational logs and any Sysmon registry rules are actually being collected.
- Stack task names across hosts to surface rare tasks, and treat rarity as a lead rather than proof.
- Inspect the task action, path, privileges, stored credentials and creator before making a call.
- Hunt for deletion of the SD value in logs, then sweep the registry directly for hidden tasks your logs may have missed.
- Preserve evidence before removing a task, and find out how the attacker gained the access they needed.
- Convert your most precise hunt logic into alerts and keep the noisier checks as scheduled reviews.
Run this hunt again on a regular schedule and after any intrusion report that mentions scheduled tasks. Each pass adds to your baseline of normal tasks, which makes the next unusual one easier to spot.