Security Searches by Question¶
In one sentence
The searches you run during an incident, grouped by the question you are answering, each with a plain-English breakdown of what the SPL does and what the event codes mean — swap index=botsv3 and the sourcetype names for your environment's.
Conventions: SEC = index=botsv3 sourcetype=WinEventLog (Security log; use source="WinEventLog:Security" if System/Application share the sourcetype), SYSMON = index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational", PS = index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-PowerShell/Operational". Event ID meanings: Windows Event IDs.
Who logged on, from where?¶
Successful logons by type and source
index=botsv3 sourcetype=WinEventLog EventCode=4624
| eval user=mvindex(Account_Name,1)
| eval type=case(Logon_Type==2,"2 Interactive",Logon_Type==3,"3 Network",Logon_Type==4,"4 Batch",Logon_Type==5,"5 Service",Logon_Type==7,"7 Unlock",Logon_Type==9,"9 NewCredentials",Logon_Type==10,"10 RDP",Logon_Type==11,"11 Cached",true(),Logon_Type)
| stats count, earliest(_time) as first, latest(_time) as last by host, user, type, Source_Network_Address
| convert ctime(first) ctime(last)
| sort host, - count
4624 = a logon succeeded. Account_Name has two values (subject, target); mvindex(...,1) takes the account that logged on. case turns the numeric Logon_Type into a label — type 3 (network: SMB/WinRM/PsExec), 10 (RDP) and 9 (runas /netonly, pass-the-hash tooling) are the ones to read carefully. stats gives one row per host/user/type/source with first and last time.
Failed logons — brute force and spraying
index=botsv3 sourcetype=WinEventLog EventCode=4625
| bin _time span=10m
| stats count, dc(Account_Name) as users, values(Sub_Status) as substatus by _time, Source_Network_Address, host
| where count >= 10
| eval pattern=if(users >= 5, "spraying (many users)", "brute force (few users)")
| sort - count
4625 = logon failed. Bucketing to 10 minutes and counting per source IP finds bursts; dc(Account_Name) (distinct accounts) separates spraying (one password, many users) from brute force (one user, many passwords). Sub_Status explains why: 0xC000006A wrong password, 0xC0000064 user doesn't exist (spraying a wordlist), 0xC0000234 locked out, 0xC0000072 disabled account.
Explicit credentials / lateral movement source side
index=botsv3 sourcetype=WinEventLog EventCode=4648
| stats count, values(Target_Server_Name) as targets, values(Process_Name) as procs by host, Subject_Account_Name, Account_Name
| where mvcount(targets) > 3
4648 = a logon was attempted with explicit credentials (runas, scheduled task creds, tools that supply a username/password/hash). It's logged on the source machine, so host is where the attacker is, targets is where they went. One account reaching many servers in a short window is lateral movement.
RDP sessions with source IP
index=botsv3 sourcetype=WinEventLog (EventCode=4624 Logon_Type=10) OR EventCode=4778 OR EventCode=4779 OR (sourcetype="WinEventLog:Microsoft-Windows-TerminalServices-LocalSessionManager/Operational" EventCode IN (21,24,25))
| eval src=coalesce(Source_Network_Address, Client_Address, Source_Network_Address)
| eval user=coalesce(mvindex(Account_Name,1), Account_Name, User)
| table _time, host, EventCode, user, src
| sort 0 _time
4624/10 = RDP logon; 4778/4779 = RDP session reconnected/disconnected (with client name/IP); LocalSessionManager 21 = session logon succeeded, 24 disconnect, 25 reconnect. coalesce merges the differently-named IP fields into one src column so the timeline reads cleanly.
Admin logons
index=botsv3 sourcetype=WinEventLog EventCode=4672
| stats count by host, Account_Name
| sort - count
4672 = special privileges assigned at logon — effectively "an admin-level account logged on". Pair it with the 4624 that has the same Logon_ID.
What ran?¶
Process creation with command lines (Security 4688)
index=botsv3 sourcetype=WinEventLog EventCode=4688
| eval user=mvindex(Account_Name,0)
| stats count by host, user, New_Process_Name, Process_Command_Line, Creator_Process_Name
| sort - count
4688 = new process created. Process_Command_Line is only present if "Include command line in process creation events" is enabled by policy; Creator_Process_Name (parent) exists on Win10+/2016+.
Sysmon process tree — suspicious parents
index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventID=1
| eval parent=lower(replace(ParentImage,".*\\\\","")), child=lower(replace(Image,".*\\\\",""))
| search (parent IN ("winword.exe","excel.exe","powerpnt.exe","outlook.exe","acrord32.exe","msedge.exe","chrome.exe") AND child IN ("cmd.exe","powershell.exe","wscript.exe","cscript.exe","mshta.exe","rundll32.exe","regsvr32.exe","certutil.exe","bitsadmin.exe","msiexec.exe"))
OR (parent="wmiprvse.exe" AND child IN ("cmd.exe","powershell.exe"))
OR (parent="services.exe" AND child IN ("cmd.exe","powershell.exe"))
OR (parent IN ("w3wp.exe","httpd.exe","tomcat*.exe") AND child IN ("cmd.exe","powershell.exe","whoami.exe"))
| table _time, host, User, parent, child, CommandLine
Sysmon 1 = process create with full parent/child and hashes. replace(ParentImage,".*\\\\","") strips the path, leaving the file name. The search lists classic bad parent→child pairs: Office spawning a shell (macro), WMI provider host spawning a shell (remote WMI exec), services.exe spawning a shell (PsExec-style service), a web server spawning a shell (web shell).
LOLBins & suspicious command lines
index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventID=1
| where match(CommandLine, "(?i)(-enc|-encodedcommand|frombase64string|iex\s*\(|invoke-expression|downloadstring|downloadfile|net\.webclient|invoke-webrequest|certutil.*-urlcache|certutil.*-decode|bitsadmin.*transfer|mshta.*http|regsvr32.*/i:http|rundll32.*javascript|vssadmin.*delete|wbadmin.*delete|bcdedit.*recoveryenabled|wevtutil\s+cl|schtasks.*/create|sc\s+create|reg\s+add.*\\run|whoami|net\s+(user|group|localgroup)|nltest|dsquery|adfind|mimikatz|sekurlsa|procdump.*lsass|comsvcs.*minidump|ntdsutil|esentutl.*ntds)")
| stats count, values(CommandLine) as cmds, earliest(_time) as first by host, User, Image
| convert ctime(first)
| sort first
One long case-insensitive regex covers download cradles, encoded PowerShell, LOLBin downloaders, shadow-copy/backup deletion (ransomware prep), log clearing, persistence commands, discovery commands and credential dumping. values(CommandLine) collects every matching command per host/user/binary so you read them in one row.
PowerShell script blocks
index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-PowerShell/Operational" EventCode=4104
| where len(ScriptBlockText) > 500 OR match(ScriptBlockText,"(?i)frombase64string|invoke-mimikatz|amsi|bypass|-nop|hidden|downloadstring|reflection\.assembly|virtualalloc|kernel32")
| stats count, min(_time) as first, values(Path) as paths by host, ScriptBlockText
| convert ctime(first)
| sort first
4104 = the actual decoded script text, even for -enc payloads — it's the single best PowerShell artifact. Long blocks and the keyword set (AMSI bypass, reflection, memory allocation, download cradles) surface offensive tooling.
Renamed / out-of-place binaries (Sysmon)
index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventID=1
| eval fname=lower(replace(Image,".*\\\\","")), orig=lower(OriginalFileName)
| where isnotnull(orig) AND orig!="?" AND fname!=orig AND NOT (fname="powershell.exe" AND orig="powershell.exe")
| stats count by host, Image, OriginalFileName, CommandLine
Sysmon records the PE header's OriginalFileName. If the file on disk is svchost.exe but the header says mimikatz.exe, the attacker renamed it. Also useful: Image under \Users\Public\, \ProgramData\, \Windows\Temp\, \AppData\Local\Temp\, \PerfLogs\.
How do they persist?¶
New services
index=botsv3 sourcetype="WinEventLog:System" EventCode=7045
| table _time, host, Service_Name, Service_File_Name, Service_Type, Service_Start_Type, Account_Name
| sort 0 _time
System 7045 = a service was installed. PSEXESVC = PsExec; random 8-char names with %COMSPEC% /b /c start /b /min powershell -nop -w hidden -enc = Cobalt Strike/Metasploit service payloads; a service whose binary sits in a user folder is persistence.
Scheduled tasks
index=botsv3 sourcetype=WinEventLog EventCode IN (4698, 4702)
| rex field=Task_Content "<Command>(?<cmd>[^<]+)</Command>"
| rex field=Task_Content "<Arguments>(?<args>[^<]+)</Arguments>"
| table _time, host, Subject_Account_Name, Task_Name, cmd, args
4698 = task created, 4702 = task updated (requires audit policy). The task XML lives in Task_Content; rex pulls the command and arguments out of it.
Registry run keys & other ASEPs (Sysmon)
index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventID IN (12, 13, 14)
| where match(TargetObject, "(?i)\\\\CurrentVersion\\\\Run|\\\\RunOnce|\\\\Winlogon\\\\(Shell|Userinit)|\\\\Image File Execution Options|\\\\Services\\\\[^\\\\]+\\\\ImagePath|\\\\AppInit_DLLs|\\\\Command Processor\\\\AutoRun|\\\\Environment\\\\UserInitMprLogonScript|\\\\Classes\\\\CLSID\\\\[^\\\\]+\\\\InprocServer32|\\\\Policies\\\\Explorer\\\\Run|\\\\Active Setup")
| table _time, host, User, Image, EventID, TargetObject, Details
Sysmon 12 = key created/deleted, 13 = value set, 14 = key/value renamed. TargetObject is the registry path; Details the value written. The regex lists the autostart locations from the Registry keys page.
WMI persistence
index=botsv3 (sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventID IN (19,20,21)) OR (sourcetype="WinEventLog:Microsoft-Windows-WMI-Activity/Operational" EventCode=5861)
| table _time, host, sourcetype, EventID, EventCode, User, EventNamespace, Name, Query, Destination, Consumer, Filter, Message
Sysmon 19/20/21 = WMI event filter / consumer / binding created — all three together = persistence. WMI-Activity 5861 is the OS's own record of a new permanent consumer.
New local users and group changes
index=botsv3 sourcetype=WinEventLog EventCode IN (4720, 4722, 4724, 4728, 4732, 4756)
| eval action=case(EventCode==4720,"user created",EventCode==4722,"user enabled",EventCode==4724,"password reset",EventCode==4728,"added to global group",EventCode==4732,"added to local group",EventCode==4756,"added to universal group")
| table _time, host, Subject_Account_Name, action, Account_Name, Member_Name, Group_Name
| sort 0 _time
4720 create, 4722 enable, 4724 admin password reset, 4728/4732/4756 member added to a security group (global/local/universal). A 4720 followed within seconds by 4732 into Administrators is textbook backdoor account creation.
Lateral movement (target side)¶
PsExec / SMB admin share pattern
index=botsv3 (sourcetype=WinEventLog EventCode IN (4624, 5140, 5145)) OR (sourcetype="WinEventLog:System" EventCode=7045)
| eval share=coalesce(Share_Name, Relative_Target_Name)
| where EventCode!=4624 OR Logon_Type=3
| eval src=coalesce(Source_Network_Address, Source_Address)
| stats values(EventCode) as codes, values(share) as shares, values(Service_Name) as services, values(Account_Name) as accounts, min(_time) as first, max(_time) as last by host, src
| where match(mvjoin(codes,","),"5140|5145") AND match(mvjoin(codes,","),"7045")
| convert ctime(first) ctime(last)
The PsExec chain on the target: 4624 type 3 (network logon) → 5140/5145 (share ADMIN$/IPC$ accessed, file PSEXESVC.exe written) → 7045 (service installed). Grouping everything by target host and source IP, then requiring both a share-access code and a service-install code in the same group, finds the chain even without knowing the tool's name.
WMI / WinRM remote execution
index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventID=1 (ParentImage="*\\wmiprvse.exe" OR ParentImage="*\\wsmprovhost.exe")
| table _time, host, User, ParentImage, Image, CommandLine
On the target, remote WMI runs children under wmiprvse.exe; PowerShell Remoting runs them under wsmprovhost.exe. Neither should normally spawn cmd/powershell.
Pass-the-hash indicators
index=botsv3 sourcetype=WinEventLog EventCode=4624 Logon_Type=9 Logon_Process=seclogo Authentication_Package=Negotiate
| table _time, host, Account_Name, Logon_ID, Process_Name
Mimikatz sekurlsa::pth and similar create a 4624 with Logon Type 9 (NewCredentials), logon process seclogo, package Negotiate on the attacker's machine. On the target, PtH looks like a normal type 3 NTLM logon — so also hunt 4624 Logon_Type=3 Authentication_Package=NTLM from workstation-to-workstation in a Kerberos domain.
Kerberoasting (on DCs)
index=botsv3 sourcetype=WinEventLog EventCode=4769 Ticket_Encryption_Type=0x17 Failure_Code=0x0
| where NOT match(Service_Name, "\\$$|krbtgt")
| bin _time span=10m
| stats dc(Service_Name) as spns, values(Service_Name) as services by _time, Account_Name, Client_Address
| where spns >= 5
4769 = service ticket requested. Encryption type 0x17 = RC4 — what cracking tools request. Excluding computer accounts ($) and krbtgt, one user requesting five or more distinct SPNs in ten minutes is Kerberoasting.
Network side (Zeek / Stream in Splunk)¶
Beacon candidates — see Beaconing & C2 for the full streamstats version.
Rare user agents
index=botsv3 sourcetype=stream:http
| stats dc(src_ip) as hosts, count, values(site) as sites by http_user_agent
| where hosts <= 2
| sort count
dc(src_ip) = how many clients use each UA; one or two = bespoke tool or malware; values(site) shows where it goes.
Executable downloads
index=botsv3 sourcetype=stream:http (http_content_type="application/x-msdownload" OR http_content_type="application/x-dosexec" OR uri_path="*.exe" OR uri_path="*.dll" OR uri_path="*.ps1")
| table _time, src_ip, dest_ip, site, uri_path, http_user_agent, bytes_in
DNS oddities
index=botsv3 sourcetype=stream:dns record_type=A
| eval qlen=len(query), first=mvindex(split(query,"."),0)
| where qlen > 50 OR len(first) > 30 OR record_type IN ("TXT","NULL")
| stats count, dc(query) as uniq by src_ip, dest_ip
| sort - uniq
Large uploads
index=botsv3 sourcetype=stream:tcp
| stats sum(bytes_out) as out, sum(bytes_in) as in by src_ip, dest_ip, dest_port
| eval out_MB=round(out/1048576,1), ratio=round(out/(in+1),1)
| where out_MB > 50 AND ratio > 5
| sort - out_MB
ratio = bytes sent divided by bytes received; a client sending 5× more than it receives, in the tens of megabytes, is uploading.
Log tampering¶
index=botsv3 (sourcetype=WinEventLog EventCode IN (1102, 4719, 4616)) OR (sourcetype="WinEventLog:System" EventCode=104)
| eval what=case(EventCode==1102,"Security log cleared",EventCode==104,"Event log cleared",EventCode==4719,"Audit policy changed",EventCode==4616,"System time changed")
| table _time, host, what, Account_Name, Subject_Account_Name, Message
| sort 0 _time
1102 = Security log cleared (records who), 104 = any other log cleared, 4719 = audit policy changed (logging turned off before the action), 4616 = system time changed (timeline confusion). Also check for hosts whose log volume drops to zero — Data discovery → log gap detection.
Build a host timeline (everything about one machine)¶
index=botsv3 host=FYODOR-L earliest="08/20/2018:00:00:00" latest="08/21/2018:00:00:00"
(sourcetype=WinEventLog EventCode IN (4624,4625,4648,4672,4688,4698,4720,4732,7045,1102))
OR (sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventID IN (1,3,11,12,13,22))
OR (sourcetype="XmlWinEventLog:Microsoft-Windows-PowerShell/Operational" EventCode=4104)
| eval code=coalesce(EventCode, EventID)
| eval who=coalesce(User, mvindex(Account_Name,1), Account_Name)
| eval what=coalesce(CommandLine, Process_Command_Line, ScriptBlockText, TargetFilename, TargetObject, QueryName, DestinationIp." :".DestinationPort, Service_File_Name, Task_Name, Message)
| eval what=substr(what,1,300)
| table _time, sourcetype, code, who, what
| sort 0 _time
One search, three sourcetypes, a handful of event codes; coalesce picks the most descriptive field each event has and substr trims it so the table stays readable. This is the table you paste into a report timeline.