Tools

T1654

Splunk

What Splunk is, how analysts use SPL to search and correlate security data, and how to build detection alerts and dashboards that produce signal, not noise.

View on Graph

What Splunk Is and Why Analysts Live In It

Splunk is a platform for searching, monitoring, and analyzing machine-generated data — logs, metrics, events, and security telemetry — in real time. Along with Elastic Security, it is one of the two dominant SIEM platforms. At its core, Splunk ingests data from any source (Windows Event Logs via Sysmon, syslog, network devices, cloud APIs, custom applications), indexes it for fast retrieval, and provides a search language (SPL) to query, correlate, and visualize that data.

MITRE ATT&CK maps log enumeration to T1654 (Log Enumeration) — attackers query SIEMs and log aggregators to understand what is being monitored, and Splunk is the most commonly targeted SIEM.


Core SPL Commands — The 80/20

You will use these commands in every single search. Master these and you can answer any investigation question.

CommandWhat It DoesExample
searchFilter events by field/valuesearch sourcetype=WinEventLog:Security EventCode=4625 (similar to KQL’s where operator)
indexRestrict search to a data sourceindex=windows or index=network
sourcetypeFilter by log typesourcetype=WinEventLog:Security
statsCalculate aggregate statisticsstats count by src_ip
tableDisplay specific fields in a tabletable _time, src_ip, dest_ip, user
topShow most common valuestop limit=10 src_ip
rareShow least common valuesrare src_ip
sortSort resultssort - count (descending)
whereFilter on calculated valueswhere count > 100
evalCreate calculated fieldseval total_bytes = bytes_in + bytes_out
timechartTime-based charttimechart count by src_ip
dedupRemove duplicate eventsdedup src_ip (first occurrence per IP)
rexExtract data using regexrex field=raw "(?<ip>\d+\.\d+\.\d+\.\d+)"
transactionGroup related events into sessionstransaction src_ip maxspan=5m

Authentication Investigations

1. Failed Logins — Top Sources

Find the source IPs attempting the most failed logins. This is your first query for any brute force investigation.

index=windows EventCode=4625
| stats count by src_ip, Account_Name
| sort - count
| head 20

What it tells you: The top 20 IPs with the most failed login attempts, broken down by targeted account.

2. Brute Force — IPs Crossing Threshold

Identify IPs that have exceeded a threshold of failed logins within a time window.

index=windows EventCode=4625
| bucket _time span=5m
| stats count by src_ip, _time
| where count > 10
| sort - count

What it tells you: IPs that attempted > 10 failed logins within a 5-minute window — classic brute force pattern.

3. Password Spray Detection — One Account, Many Users

Password spray attacks try the same password against many accounts. Detect this by looking for failed logins with the same destination account but from multiple source IPs.

index=windows EventCode=4625
| stats dc(src_ip) as unique_ips, count by Account_Name
| where unique_ips > 5
| sort - unique_ips

What it tells you: Accounts that received failed logins from > 5 different IPs — password spray victims.

4. Successful Logins After Brute Force

Find IPs that transitioned from failed to successful authentication — the attacker found the right password.

index=windows (EventCode=4625 OR EventCode=4624)
| eval auth_type = if(EventCode=4625, "failed", "success")
| stats earliest(_time) as first, latest(_time) as last, values(auth_type) as types by src_ip, Account_Name
| where match(types, "failed") AND match(types, "success")
| eval window = round((last - first)/60)
| table src_ip, Account_Name, first, last, window

What it tells you: IPs that had both failed and successful logins for the same account — the attacker brute-forced and succeeded.


Process and Command-Line Investigations

5. Suspicious PowerShell

Find encoded PowerShell commands — a near-universal malware indicator.

index=windows EventCode=4688
| search CommandLine IN ("*powershell*", "*pwsh*")
| search CommandLine IN ("*EncodedCommand*", "*-enc*", "*iex*", "*DownloadString*", "*Invoke-Expression*")
| stats count by ComputerName, UserName, CommandLine, ParentProcessName
| sort - count

What it tells you: Every host that ran PowerShell with suspicious parameters — the most reliable detection for initial access and lateral movement.

6. LOLBin Execution (Living Off the Land)

Find execution of binaries that attackers commonly abuse for evasion.

index=windows EventCode=4688
| search NewProcessName IN ("*\\rundll32.exe", "*\\regsvr32.exe", "*\\mshta.exe", "*\\cscript.exe", "*\\wscript.exe", "*\\certutil.exe", "*\\bitsadmin.exe", "*\\msbuild.exe", "*\\installutil.exe")
| stats count by ComputerName, NewProcessName, CommandLine, ParentProcessName
| sort - count

What it tells you: Which hosts are running LOLBins and what their parent processes are — distinguish automation scripts from malicious usage.

7. Process Tree — Unusual Parent-Child

Find processes whose parent is unexpected (e.g., winword.exe spawning cmd.exe, which indicates a macro running).

index=windows EventCode=4688
| eval suspicious = case(
    match(ParentProcessName, "WINWORD.EXE"), "Office->Process",
    match(ParentProcessName, "EXCEL.EXE"), "Office->Process",
    match(ParentProcessName, "OUTLOOK.EXE"), "Office->Process",
    match(ParentProcessName, "chrome.exe"), "Browser->Process",
    match(ParentProcessName, "firefox.exe"), "Browser->Process",
    1=1, "Normal"
)
| search suspicious != Normal
| stats count by ParentProcessName, NewProcessName, ComputerName
| sort - count

What it tells you: All instances of Office or browser applications spawning child executables — the classic macro malware and drive-by download pattern.


Network and C2 Investigations

8. Outbound Connections — Volume by Host

Find hosts making the most outbound connections — potential C2 candidates.

index=network sourcetype=netflow
| stats sum(bytes) as total_bytes, dc(dest_ip) as unique_destinations, count as connections by src_ip
| where connections > 1000
| sort - total_bytes

What it tells you: Hosts with high outbound connection counts and bytes transferred — volume anomaly detection.

9. Beaconing Detection — Regular Connection Intervals

Beaconing malware connects to C2 at regular intervals. Find connections with consistent inter-arrival times.

index=proxy dest_ip="*" 
| stats earliest(_time) as first, latest(_time) as last, count as total_connections by src_ip, dest_ip
| eval duration_hours = (last - first) / 3600
| eval connections_per_hour = total_connections / duration_hours
| where connections_per_hour > 1 AND connections_per_hour < 60
| eval interval_seconds = 3600 / connections_per_hour
| table src_ip, dest_ip, total_connections, duration_hours, interval_seconds
| sort interval_seconds

What it tells you: Pairs of src_ip→dest_ip with regular connection patterns — not too frequent (not web browsing), not too rare (not admin tasks). The interval in seconds should be roughly consistent for true beacons.

10. DNS — High TXT Query Volume (Tunneling)

Find hosts making an unusual number of TXT DNS queries, which have the highest DNS tunneling payload capacity.

index=dns query_type=TXT
| stats count by src_ip, query
| where count > 20
| sort - count

What it tells you: Hosts making many TXT queries — potential DNS tunneling for C2 or exfiltration.

11. DNS — Long Subdomain Detection

DNS tunneling encodes data in subdomains. Find unusually long subdomain names.

index=dns
| eval subdomain_length = len(split(query, ".")[0])
| where subdomain_length > 30
| stats count by src_ip, query, query_type
| sort - count

What it tells you: Hosts querying domains with subdomains longer than 30 characters — strong DNS tunneling indicator.

12. Connections to Newly Observed Domains

If you have a feed of domain registration dates, cross-reference connection data.

index=proxy
| lookup domain_age_lookup domain as dest_host output first_seen
| where first_seen > relative_time(now(), "-30d")
| stats count by src_ip, dest_host, first_seen
| sort - count

What it tells you: Connections to domains first seen < 30 days ago — phishing and C2 domains are almost always new.


Data Exfiltration Detection

13. Large Outbound Transfers

Find the top data exporters by volume.

index=network sourcetype=flow
| stats sum(bytes) as total_bytes, sum(packets) as total_packets by src_ip, dest_ip, dest_port
| where total_bytes > 100000000  # > 100MB
| eval total_mb = round(total_bytes/1048576, 2)
| table src_ip, dest_ip, dest_port, total_mb, total_packets
| sort - total_mb

What it tells you: Hosts that transferred > 100MB to external destinations — investigate for data exfiltration.

14. After-Hours Data Transfer

Same as above, but filtered to after-hours time windows.

index=network sourcetype=flow
| eval hour = strftime(_time, "%H")
| where hour >= 18 OR hour <= 6
| stats sum(bytes) as total_bytes by src_ip, dest_ip, dest_port, user
| where total_bytes > 50000000
| eval total_mb = round(total_bytes/1048576, 2)
| sort - total_mb

What it tells you: Data transfers between 6 PM and 6 AM — exfiltration is most common after hours.


Privilege Escalation and Lateral Movement

15. Admin Login Tracking

Find all interactive logons with admin privileges.

index=windows EventCode=4672
| stats count by Account_Name, ComputerName, src_ip
| sort - count

What it tells you: Every time a user received elevated privileges on a system — track admin account usage.

16. Lateral Movement — Explicit Credential Use (4648)

Event 4648 fires when explicit credentials are used (RunAs, scheduled tasks, WMI, PsExec).

index=windows EventCode=4648
| stats count by Account_Name, Target_Server, ProcessName
| sort - count

What it tells you: All instances of explicit credential use — lateral movement via WMI, PsExec, or scheduled tasks.

17. New Scheduled Task Creation

Attackers persist via scheduled tasks. Find recently created tasks.

index=windows EventCode=4698
| table _time, ComputerName, TaskName, UserName, Command
| sort - _time

What it tells you: Every new scheduled task — compare against a baseline of legitimate tasks.


Threat Hunting Queries

18. Unusual Service Installations

index=windows EventCode=7045
| search ServiceName != "TrustedInstaller" AND ServiceType = "own process"
| stats count by ServiceName, ImagePath, ComputerName
| sort - _time

19. Accounts Created Outside IT Processes

index=windows EventCode=4720
| table _time, Account_Name, ComputerName
| sort - _time

20. Domain Admin Group Modifications

index=windows EventCode IN (4728, 4732, 4746, 4751, 4756)
| table _time, Account_Name, TargetUserName, ComputerName
| sort - _time

Building Detections — From Search to Alert

Once you’ve found a useful query, turn it into a real-time alert:

  1. Set a schedule. Most queries should run every 5-15 minutes looking back 15-60 minutes.
  2. Set a threshold. “Alert if count > 5 from a single source IP in 15 minutes” prevents noise from single events.
  3. Add context. The alert message should include: what was detected, affected assets, severity, and a link to the runbook.
  4. Tier the alert. Low → inform, review next shift. Medium → create incident ticket. High → page on-call analyst.

Alert Example

Name: Brute Force - Password Spray Detection
Search: index=windows EventCode=4625
        | stats dc(Account_Name) as unique_accounts, count by src_ip
        | where unique_accounts > 10
Schedule: Every 5 minutes, look back 30 minutes
Threshold: count > 50
Severity: Medium - High (based on unique_accounts count)
Response: Review source IP, check for 4624 (successful login), block at firewall if malicious.

Sources