Overnight Post-RDP Activity Detection
Correlates remote interactive logons (UserLogon, LogonType=10) occurring during overnight hours with suspicious process execution (ProcessRollup2) from the same logon session, within 30 minutes of the logon. Processes are matched on ImageFileName against a broad set of native tools and LOLBins (shells, script engines, recon utilities, transfer/archive tools, remote-exec binaries) and classified by CommandLine into signal categories: Enumeration, PowerShell Enumeration, Transfer or Archive Utility, PowerShell Transfer or Archive, and encoded Command Processor/PowerShell. Uncategorized processes are dropped.
#event_simpleName=ProcessRollup2 event_platform=Win
| ImageFileName=/\\(cmd|powershell|pwsh|wscript|cscript|mshta|rundll32|regsvr32|wmic|msbuild|installutil|regasm|regsvcs|certutil|bitsadmin|schtasks|sc|net|net1|nltest|whoami|quser|query|systeminfo|hostname|tasklist|netstat|ipconfig|curl|wget|rclone|scp|sftp|ftp|makecab|tar|7z|7za|rar|winrar|psexec|paexec|winrs|ssh|python|pythonw)\.exe$/i
// Parent/GrandParent: DENYLIST (fails open) — blank or unknown lineage still passes; only listed off-hours noise is dropped.
// To tune: add a process BASE name (no path, no .exe), case-insensitive, pipe-separated, inside the ( ) on BOTH lines as you confirm benign off-hours jobs.
// Example (parent): | ParentBaseFileName!=/^(AteraAgent|Syncro|Pulseway)\.exe$/i
// Example (grandparent): | GrandParentBaseFileName!=/^(MeshAgent|NinjaRMMAgent|CagService)\.exe$/i
| ParentBaseFileName!=/^()\.exe$/i
| GrandParentBaseFileName!=/^()\.exe$/i
| case {
CommandLine=/\b(whoami|quser|query\s+user|net1?(\.exe)?"?\s+(users?|group|localgroup|session)|nltest|ipconfig\s+\/all|systeminfo|hostname|tasklist|wmic|netstat)\b/i
| SignalType := "Enumeration";
CommandLine=/\b(Get-Process|Get-NetTCPConnection|Get-SmbShare|Get-ADUser|Get-ADComputer|Get-DomainUser|Get-DomainComputer)\b/i
| SignalType := "PowerShell Enumeration";
CommandLine=/\b(rclone|curl|wget|scp|sftp|ftp|bitsadmin|certutil|makecab|tar|7z|7za|rar|winrar)\b/i
| SignalType := "Transfer or Archive Utility";
CommandLine=/\b(Invoke-WebRequest|Invoke-RestMethod|Start-BitsTransfer|Compress-Archive|System\.IO\.Compression)\b/i
| SignalType := "PowerShell Transfer or Archive";
ImageFileName=/\\(cmd|powershell|pwsh)\.exe$/i AND CommandLine=/\s-(?:enc|encodedcommand)(?:\s|$|:)/i
| SignalType := "Windows Command Processor or PowerShell";
*
| SignalType := "Other"
}
| SignalType!="Other"
| CommandTimestampMs := ProcessStartTime * 1000
| join(
{
#event_simpleName=UserLogon event_platform=Win LogonType=10
| remoteHour := formatTime("%H", field=@timestamp, locale=en_US, timezone="America/Vancouver") // adjust timezone to where your clients/company operate (e.g. America/New_York for Eastern)
| in(field=remoteHour, values=["21","22","23","00","01","02","03"]) // adjust hours here as well if needed: Example: "02" will detect up to 02:59:99
| LogonTimestampMs := LogonTime * 1000
},
field=[aid, AuthenticationId],
include=[LogonTimestampMs, UserPrincipal, RemoteAddressIP4, LogonType]
)
| TimeFromLogonMinutes := (CommandTimestampMs - LogonTimestampMs) / 60000
| TimeFromLogonMinutes >= 0
| TimeFromLogonMinutes <= 30 // adjust for a longer capture window from logon to command execution
| table([@timestamp, cid, LogonTimestampMs, CommandTimestampMs, TimeFromLogonMinutes, aid, ComputerName, UserName, UserPrincipal, LogonType, RemoteAddressIP4, SignalType, ImageFileName, FileName, FilePath, CommandLine, ParentBaseFileName, GrandParentBaseFileName, OriginalFilename, SHA256HashData], limit=1000)
| sort(@timestamp, order=desc)Overnight Post-RDP Activity — Query Explanation
Design
Finds recon/staging commands run within 30 min of an overnight RDP logon.
- Primary: ProcessRollup2 (processes) • Subquery: UserLogon (logons)
- Join key: AuthenticationId (constant per logon session)
- Gate: process starts 0–30 min after logon
ProcessRollup2 is primary because join() returns only one subquery row per key. A session has one AuthenticationId but many processes, so a logon-primary join would collapse to one row. Process-primary = each process is its own row.
Sections
- Event + image filter — Windows
ProcessRollup2, restricted to shells, script engines, LOLBins, recon and transfer/archive tools. Kept broad; the time window controls volume. - Lineage denylist (
ParentBaseFileName!=/^()\.exe$/i, grandparent same) — fails open: blank/unknown parents pass. Positive allowlists were avoided (they silently drop real hits with blank lineage). Add benign off-hours job names inside( )to trim noise. - Classification (
case) — tags each process byCommandLine: Enumeration, PowerShell Enumeration, Transfer/Archive, PowerShell Transfer/Archive, encoded PowerShell; elseOther.net1?(\.exe)?"?\s+matchesnet/net1and both full-path-quoted and bare forms (PowerShell full-paths binaries, cmd passes them raw). - Drop + timestamp —
SignalType!="Other"removes unclassified rows.ProcessStartTime * 1000(epoch sec → ms). - Join — subquery keeps
LogonType=10(RDP) logons in 21:00–03:59 local viaformatTime+in();LogonTime * 1000→ ms; matched onAuthenticationId. - Window + output —
TimeFromLogonMinutes = (Command - Logon) / 60000, bounded>= 0and<= 30, tabled and sorted newest-first. UsesLogonTime/ProcessStartTime(real event times), NOT@timestamp(sensor report time — would skew the delta).
Tuning
- Timezone — set subquery TZ to endpoint location (
America/New_York= Eastern). Mismatch silently drops logons. - Hours — edit
remoteHourvalues. - Window — adjust
TimeFromLogonMinutes <= 30. - Noise — add parent/grandparent names to the denylists.
