Module 4: Additional Splunk Hunting Queries
These queries are from extra practical exercises I worked through after the main module sections. I consolidated them on this page so they are easier to reference later.
Back to the main module notes: Module 4: Understanding Log Sources & Investigating with Splunk
Skill assessment walkthrough: Module 4: Skill Assessment
Navigation
Queries on this page
- Remote thread outlier baseline
- PsExec password in command line
- C2 inbound port pivot
- Kerberos TGT volume
- SYSTEM distinct host count
- Burst login window
Related
These are extra searches I ran after the module, mostly to practice the same pivot habit on different question types. Not part of the core walkthrough, but they stuck because each one changed how I thought about the next search.
Which process is injecting threads everywhere?
-
The Pivot: Instead of guessing a process name, baseline CreateRemoteThread volume per source image and keep outliers above two standard deviations.
-
The Query:
index=* EventCode=8
| stats count as threads_created by SourceImage
| eventstats avg(threads_created) as avg stdev(threads_created) as stdev
| eval threshold = avg + 2*stdev
| where threads_created > threshold
| table SourceImage, threads_created, avg, stdev, threshold
| sort - threads_created
- The Discovery: EventCode 8 is CreateRemoteThread, one process spinning up a thread inside another’s memory. The
2*stdevis your sensitivity dial; drop it to catch more, raise it to quiet things down. If you run AV or EDR that legitimately injects, exclude those tools before the baseline or they’ll drag the average up and hide the real outlier.
What password did the attacker use with PsExec?
Different angle on lateral movement from the PsExec scenarios in the main notes. Here the answer was sitting in the command line.
-
The Pivot: PsExec passes the password on the command line, and Sysmon EventCode 1 logs the full string.
-
The Query:
index=* EventCode=1 CommandLine="*psexec*"
| table _time, ComputerName, User, CommandLine, ParentImage
| sort _time
- The Discovery: No need to get clever and grep for
-p. Just pull every psexec process creation and read the command line. Bonus:ParentImagetells you what kicked off the psexec call, which is usually the next thread to pull on.
What port did the C2 server come back in on?
This one followed the beaconing searches from Spot the Unusual. Outbound traffic found the C2 IP; flipping source and destination found how it came back in.
-
The Pivot: After finding outbound beaconing to the C2 IP, flip the direction and treat the C2 IP as the source to see inbound connections.
-
The Query:
index=* EventCode=3
(SourceIp="<c2_ip_1>" OR SourceIp="<c2_ip_2>")
| table _time, ComputerName, SourceIp, SourcePort, DestinationIp, DestinationPort, Image
| sort _time
- The Discovery:
DestinationPortis the answer, the port on the victim the attacker reached for.Imageshows which Windows service caught the connection, which usually gives away the protocol. Lesson worth keeping: always look at a C2 IP both ways. Outbound shows you the beacon; inbound shows you how they walked back in.
Which account requested the most Kerberos tickets?
After network pivots, I practiced simpler auth-side counting. Sometimes the fastest search is just stats and sort.
-
The Pivot: Count Kerberos TGT requests in EventCode 4768 per account.
-
The Query:
index=* EventCode=4768
| stats count as ticket_requests by Account_Name
| sort - ticket_requests
| head 10
- The Discovery: The top account by ticket volume is your starting point for credential abuse or excessive authentication activity.
How many different machines did SYSTEM touch?
-
The Pivot: Use distinct count on
ComputerNamefor a chosen account login stream. -
The Query:
index=* EventCode=4624 Account_Name="SYSTEM"
| stats dc(ComputerName) as distinct_computers
- The Discovery:
dc()counts unique hosts, so ten logins to the same box still count as one. Swap the account name for whoever you’re chasing.
Which accounts did all their logging-in inside a tight window?
-
The Pivot: Find accounts whose entire login history spans less than ten minutes, then exclude machine accounts.
-
The Query:
index=* EventCode=4624 Account_Name="*"
| search NOT Account_Name="*$*"
| stats count AS logins, range(_time) AS span by Account_Name
| where span < 600
| sort - logins
- The Discovery: The
NOT Account_Name="*$*"is the part I almost forgot. Without it the machine accounts (all those names ending in$) flood the results. One thing to watch: an account with a single login has a span of 0, so it always slips past the< 600filter. If those show up and muddy things, look at them on their own.
References
[HTB Academy] Certified Defensive Security Analyst (CDSA) - Module 4: Understanding Log Sources & Investigating with Splunk. https://academy.hackthebox.com/
[spl-threat-hunting-library] Splunk SPL threat-hunting queries from CDSA Module 4. https://github.com/kismatkunwar89/spl-threat-hunting-library