HackBlue

Tools · Windows security and Event Logs · 08 / 27

Event ID 4698: how to detect new scheduled tasks on Windows

Enable the audit setting for scheduled task events, read the task XML inside 4698, and compare against the Task Scheduler operational log.

9 min read · practice only with authorization

Task events

4698 means a scheduled task was created, 4699 deleted, 4700 enabled, 4701 disabled, and 4702 updated. They require the Audit Other Object Access Events subcategory. The event includes the full task XML, so you can read the trigger, the action’s command and arguments, and the account it runs as.

The Microsoft-Windows-TaskScheduler/Operational log, which must also be enabled, records registration (106), execution starts (200, 129), and completion (201), which helps you see whether a task actually ran.

Snippet
auditpol /set /subcategory:"Other Object Access Events" /success:enable
wevtutil sl Microsoft-Windows-TaskScheduler/Operational /e:true

What a malicious task looks like

Tasks that run every few minutes or at logon, whose action is powershell, cmd, mshta, rundll32, or a binary in a user profile or ProgramData. Tasks with names copying Windows components but stored in the root folder instead of \Microsoft\Windows. Tasks running as SYSTEM created by a non-administrative workflow.

Software updaters create many legitimate tasks, so compare against a known-good list per image.

Lab: register a task and read its XML

Create a harmless task that runs at logon and writes the date to a file, using schtasks. Then query 4698 and extract the TaskName and the command from the TaskContent XML. Delete the task and find 4699.

Finally, list every task outside \Microsoft\ with its action, which is a good periodic inventory.

Snippet
schtasks /create /tn "UpdaterCheck" /tr "cmd.exe /c echo %date% >> C:\Users\Public\t.txt" /sc onlogon /ru SYSTEM
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4698; StartTime=(Get-Date).AddHours(-1)} |
  % { $x=[xml]$_.ToXml(); $d=@{}; $x.Event.EventData.Data | % { $d[$_.Name]=$_.'#text' }
      $t=[xml]$d.TaskContent; [pscustomobject]@{Task=$d.TaskName; By=$d.SubjectUserName; Cmd=$t.Task.Actions.Exec.Command; Args=$t.Task.Actions.Exec.Arguments} }
schtasks /delete /tn "UpdaterCheck" /f
Get-ScheduledTask | Where-Object TaskPath -notlike '\Microsoft\*' | % { [pscustomobject]@{Name=$_.TaskName; Path=$_.TaskPath; Action=($_.Actions.Execute -join ';')} }

Response

Export the task XML with schtasks /query /tn name /xml before deleting it. Hash any binary it runs. Look for the same task name on other hosts, since attackers reuse names across a network.

Pair this check with services and Run keys. Tasks are one of several persistence locations, and finding one should prompt a check of the others.

Diagram

From task creation to verdict

  1. 4698

    Task created; XML includes trigger, action, and account.

  2. Parse

    Read Actions.Exec.Command and Arguments.

  3. Ran?

    Check TaskScheduler/Operational 200 and 201.

  4. Compare

    Match against known-good tasks for that image.

Check yourself

1. Which audit subcategory produces 4698?

2. Where in a 4698 event is the command the task runs?

3. Which log shows whether a task actually executed?