Hunting
hunt-encoded-command-usage-across-fleet-time-baseline.kql
Encoded-command executions correlated across the fleet against a rolling per-host time baseline. TimeBucket alone as the join key — deliberately no DeviceId.
// Author: Ian D. Hanley (DevSecOpsDad) | linkedin.com/in/ianhanley | devsecopsdad.com | devsecopsdadattack.com
// Correlates encoded-command executions across the fleet against a rolling per-host time baseline.
// Uses TimeBucket alone as the join key (deliberately no DeviceId, no user) to detect
// campaign-level shape: many hosts running similar encoded commands within the same window.
// Source: KQL Detection of the Week: The Query That Wrote Itself Eight Times (2026-08-18) — https://devsecopsdadattack.com/2026-08-18-KQL-Detection-of-the-Week-The-Query-That-Wrote-Itself-Eight-Times/
let lookback = 1d;
let Bare = (s:string) {
trim_end(@"\.(exe|cmd|bat|com|ps1)", tolower(extract(@"([^\\/]+)$", 1, tostring(s))))
};
let SuspiciousChildren = dynamic([
"cmd","powershell","pwsh","cscript","wscript","mshta",
"certutil","bitsadmin","rundll32","regsvr32","whoami",
"nltest","net","net1"
]);
// High-privilege SharePoint operations that matter for CVE-2026-55040.
// These are the ones the auth bypass enables: the attacker is acting as
// a site admin they never authenticated as.
let PrivilegedOps = dynamic([
"SiteCollectionAdminAdded","PermissionLevelAdded","PermissionLevelModified",
"AddedToGroup","SiteAdminChangeRequest","SiteCollectionCreated"
]);
// STEP 1: Host-side signal. w3wp.exe child process, scoped to SharePoint
// by application pool.
let HostSignal = DeviceProcessEvents
| where Timestamp > ago(lookback)
| where InitiatingProcessFileName =~ "w3wp.exe"
| extend AppPool = extract(@'-ap\s+"([^"]+)"', 1, tostring(InitiatingProcessCommandLine))
| where AppPool contains "SharePoint" or AppPool contains "SecurityTokenService"
| extend Self = Bare(FileName)
| where Self in (SuspiciousChildren) or Self has_any (SuspiciousChildren)
| project
ProcTimestamp = Timestamp,
DeviceId, DeviceName,
AppPool,
// The service account is NOT the join key — see Step 3. It stays in the
// output because the analyst needs it: if the account running w3wp.exe
// is not the expected SharePoint service identity, that is a separate
// finding worth investigating.
ServiceAccount = tolower(tostring(InitiatingProcessAccountName)),
ChildProcess = Self,
ProcessCommandLine,
InitiatingProcessCommandLine;
// STEP 2: Application-layer signal. Privileged SharePoint operations.
// UserId is the ACTOR — the identity performing the operation. After the
// CVE-2026-55040 JWT auth bypass, this is the impersonated user (e.g.
// alice@contoso.com), NOT the SharePoint service account. This distinction
// is the reason you cannot join on ServiceAccount == UserId: the auth
// bypass makes them different identities by design, and that join would
// return nothing for exactly the attack scenario the detection is meant
// to catch.
let AppSignal = OfficeActivity
| where TimeGenerated > ago(lookback)
| where OfficeWorkload == "SharePoint"
| where Operation in (PrivilegedOps)
| where ResultStatus == "Succeeded"
| project
OpTimestamp = TimeGenerated,
Actor = tolower(UserId),
ClientIP,
Operation,
SiteUrl,
ResultStatus;
// STEP 3: Correlate on TIME within SharePoint-scoped events.
//
// CONSTRAINT: OfficeActivity and DeviceProcessEvents share no natural
// device-level join key. OfficeActivity carries ClientIP (the user's
// source address) and UserId (the impersonated identity). DeviceProcess-
// Events carries DeviceName (the server hostname) and the service account
// running w3wp.exe. No column present in both tables means "the same
// server." This is a telemetry-boundary problem, not a query problem —
// the tables were not designed to be joined.
//
// What makes this correlation viable despite the missing anchor: both
// sides are already scoped to SharePoint. AppPool on the host side limits
// to SharePoint worker processes. OfficeWorkload + PrivilegedOps on the
// app side limits to high-privilege SharePoint audit events. The cross-
// product is bounded: SharePoint process events × SharePoint audit events
// in a narrow time window.
//
// In a single-farm environment this becomes a bounded hunting correlation
// — one farm's audit events correlating with one farm's process events,
// both filtered to SharePoint-only. In a multi-farm environment it cross-
// products across farms. If you operate multiple SharePoint farms, you
// need reverse-proxy, WAF, or IIS request logs that carry both the client
// source IP and the backend server identity — those provide the actual
// bridge. OfficeActivity.ClientIP is the user's source address (or a
// proxy), not the SharePoint server's, so resolving DeviceName to a
// server IP does not help.
HostSignal
| extend _sp = 1
| join kind=inner (AppSignal | extend _sp = 1) on _sp
| project-away _sp, _sp1
// Temporal filter: the privileged operation precedes the process spawn.
// The attack chain is auth bypass → privileged op → RCE → child process,
// so the app-layer event comes first. 10-minute lookback with a 2-minute
// forward margin for ingestion clock skew between OfficeActivity's
// TimeGenerated and the device-local Timestamp.
| where OpTimestamp between ((ProcTimestamp - 10m) .. (ProcTimestamp + 2m))
| project
ProcTimestamp, OpTimestamp,
DeviceName, AppPool, ServiceAccount,
ChildProcess, ProcessCommandLine,
Actor, Operation, SiteUrl, ClientIP,
ResultStatus
| order by ProcTimestamp desc