Query Details

HUNT 23 AD Scheduled Task Suspicious Encoding 30d

Query

// =========================================================
// HUNT-23 | AD-ScheduledTask-Suspicious-Encoding-30d
// Description : Hunts for scheduled task creations
//               (EventID 4698) and modifications (4702)
//               where the task action command contains
//               encoded payloads (Base64, hex), LOLBAS
//               binaries (certutil, mshta, regsvr32,
//               wscript, cscript, rundll32, msiexec),
//               UNC paths, or download cradles. Also
//               correlates with 4624 logon events to
//               identify the remote actor who scheduled
//               the task over network logon.
// Period      : 30 days
// Use Case    : Persistence via scheduled tasks, lateral
//               movement via SchtasksExec, C2 staging
// Tables      : SecurityEvent
// =========================================================

let Period = 30d;

// Suspicious task content patterns
let SuspiciousCommands = dynamic([
    "-enc ", "-EncodedCommand", "frombase64string",
    "iex(", "invoke-expression", "downloadstring",
    "downloadfile", "bitsadmin", "certutil -decode",
    "certutil -urlcache", "mshta http", "mshta vbscript",
    "regsvr32 /s /n /u /i:http", "regsvr32 /s /u /i:http",
    "cscript \\\\", "wscript \\\\",
    "rundll32 javascript:", "shell.application",
    "schtasks /create", "at \\\\",
    "cmd /c powershell", "powershell -w hidden",
    "powershell -nop", "%temp%\\", "%appdata%\\"
]);

let LOLBASBinaries = dynamic([
    "certutil", "mshta", "regsvr32", "wscript",
    "cscript", "rundll32", "msiexec", "wmic", "bitsadmin",
    "forfiles", "pcalua", "eventvwr", "sdclt", "fodhelper"
]);

// Scheduled task creation and modification events
let SchedTasks = SecurityEvent
    | where TimeGenerated > ago(Period)
    | where EventID in (4698, 4702)    // 4698=created, 4702=modified
    | extend
        TaskName      = tostring(column_ifexists("TaskName", "")),
        TaskContent   = tostring(column_ifexists("TaskContent", "")),
        Actor         = strcat(SubjectDomainName, "\\", SubjectUserName),
        ActorNorm     = tolower(SubjectUserName),
        EventType     = iff(EventID == 4698, "TaskCreated", "TaskModified"),
        ContentLower  = tolower(tostring(column_ifexists("TaskContent", ""))),
        IsOffHours    = hourofday(TimeGenerated) < 7 or hourofday(TimeGenerated) >= 20,
        IsNetwork     = SubjectLogonId !in ("0x3e7", "0x3e4", "0x3e5");

// Flag suspicious patterns
SchedTasks
| extend
    HasEncodedPayload = ContentLower has_any ("-enc ", "-encodedcommand",
                                              "frombase64string", "base64"),
    HasDownloadCradle = ContentLower has_any ("downloadstring", "downloadfile",
                                              "certutil -urlcache", "bitsadmin /transfer",
                                              "invoke-webrequest", "wget ", "curl "),
    HasLOLBAS         = ContentLower has_any (LOLBASBinaries),
    HasUNCPath        = ContentLower matches regex @"\\\\[a-z0-9\-\.]+\\[a-z]",
    HasSuspiciousCmd  = ContentLower has_any (SuspiciousCommands),
    SuspiciousCount   = countof(ContentLower, "-enc ")
                      + countof(ContentLower, "base64")
                      + countof(ContentLower, "downloadstring")
| where HasEncodedPayload or HasDownloadCradle or HasLOLBAS or HasUNCPath or HasSuspiciousCmd
| summarize
    TaskEventCount    = count(),
    CreatedTasks      = countif(EventType == "TaskCreated"),
    ModifiedTasks     = countif(EventType == "TaskModified"),
    EncodedPayloads   = countif(HasEncodedPayload),
    DownloadCradles   = countif(HasDownloadCradle),
    LOLBASUsage       = countif(HasLOLBAS),
    UNCPaths          = countif(HasUNCPath),
    OffHoursCount     = countif(IsOffHours),
    NetworkActors     = countif(IsNetwork),
    Actors            = make_set(Actor, 10),
    TaskNames         = make_set(tostring(column_ifexists("TaskName", "")), 20),
    SampleContent     = take_any(tostring(column_ifexists("TaskContent", ""))),
    FirstSeen         = min(TimeGenerated),
    LastSeen          = max(TimeGenerated)
  by Computer
| extend
    RiskScore = (EncodedPayloads * 40)
              + (DownloadCradles * 35)
              + (LOLBASUsage * 20)
              + (UNCPaths * 25)
              + (OffHoursCount * 10)
              + (NetworkActors * 15),
    RiskLevel = case(
        EncodedPayloads >= 1 and DownloadCradles >= 1,
            "Critical - Encoded_Payload_With_DownloadCradle",
        EncodedPayloads >= 1,
            "High - Encoded_Payload_In_Task",
        DownloadCradles >= 1,
            "High - DownloadCradle_In_Task",
        LOLBASUsage >= 2,
            "High - Multiple_LOLBAS_In_Task",
        LOLBASUsage >= 1 and OffHoursCount >= 1,
            "Medium - LOLBAS_OffHours_Task",
        "Medium - Suspicious_Task_Pattern"
    )
| project
    Computer,
    RiskLevel,
    RiskScore,
    EncodedPayloads,
    DownloadCradles,
    LOLBASUsage,
    UNCPaths,
    OffHoursCount,
    NetworkActors,
    Actors,
    TaskNames,
    SampleContent,
    FirstSeen,
    LastSeen
| order by RiskScore desc

Explanation

This query is designed to identify potentially suspicious scheduled tasks on computers by analyzing security events over the past 30 days. It focuses on tasks that might be used for persistence, lateral movement, or command and control (C2) staging. Here's a simplified breakdown of what the query does:

  1. Time Frame: It examines events from the last 30 days.

  2. Event Types: It looks for scheduled task creation (EventID 4698) and modification (EventID 4702) events.

  3. Suspicious Patterns: The query checks if the task's command content includes:

    • Encoded payloads (e.g., Base64).
    • Download cradles (methods to download files from the internet).
    • Use of known Living Off The Land Binaries and Scripts (LOLBAS) like certutil, mshta, regsvr32, etc.
    • UNC paths (network paths).
  4. Off-Hours and Network Logons: It flags tasks created or modified outside typical working hours and those initiated over a network logon.

  5. Suspicious Task Detection: It identifies tasks with any of the above patterns and counts how often each pattern appears.

  6. Risk Assessment: It calculates a risk score based on the presence of these patterns, with higher scores indicating more suspicious activity. It also assigns a risk level (Critical, High, Medium) based on specific criteria.

  7. Summary and Output: For each computer, it summarizes:

    • The number of suspicious tasks.
    • The types of suspicious activities detected.
    • The actors involved.
    • Sample task content.
    • The first and last time such tasks were seen.
    • The risk score and level.
  8. Ordering: The results are ordered by risk score, with the most suspicious computers listed first.

This query helps security analysts quickly identify and prioritize potentially malicious scheduled tasks for further investigation.

Details

David Alonso profile picture

David Alonso

Released: March 24, 2026

Tables

SecurityEvent

Keywords

SecurityEventDevicesNetworkTaskComputerActorsTaskNamesSampleContentTimeGeneratedEventIDTaskCreatedTaskModifiedTaskContentSubjectDomainNameSubjectUserNameSubjectLogonIdTaskEventCountCreatedTasksModifiedTasksEncodedPayloadsDownloadCradlesLOLBASUsageUNCPathsOffHoursCountNetworkActorsRiskScoreRiskLevel

Operators

letdynamicagointostringcolumn_ifexistsstrcattoloweriffhourofdayhas_anymatches regexcountofwheresummarizecountcountifmake_settake_anyminmaxcaseprojectorder bydesc

Actions