Module 6: Skill Assessment

The queries on this page are from the Module 6 skill assessment exercises. I kept the walkthrough in order so I could see what I tried at each step without re-running the labs.

Back to the main module notes: Module 6: Detecting Windows Attacks with Splunk

Assessment Navigation

Key Takeaway

Questions

Related

Key Takeaway

These three questions all came down to the same habit from the main module: start from the chapter query or the attack mechanics, check one raw event when something returns zero rows, then adjust fields or thresholds against what the data actually shows.

Beaconing needed Zeek field names (id.orig_h, not src) and a looser read on the jitter percentage. PrintNightmare was endpoint/operation hunting on bro:dce_rpc:json. BloodHound was breadth across RPC endpoints, with the operation list doing the real confirming when the dataset only had one target host.

Assessment Notes

Three questions: Empire beacon TimeInterval, PrintNightmare source IP, and BloodHound collector source IP.

1. Detecting Beaconing Malware (empire)

Question: Using the empire index and the bro:http:json sourcetype, identify beaconing activity by modifying the Splunk search from the “Detecting Beaconing Malware” section, and enter the value of the TimeInterval field as the answer.

What I did, in order:

  1. Took the beaconing query from section 12 as the starting point (src, dest, dest_port fields) and swapped the index/sourcetype to index="empire" sourcetype="bro:http:json". Ran it.
index="empire" sourcetype="bro:http:json"
| sort 0 _time
| streamstats current=f last(_time) as prevtime by src, dest, dest_port
| eval timedelta = _time - prevtime
| eventstats avg(timedelta) as avg, count as total by src, dest, dest_port
| eval upper=avg*1.1
| eval lower=avg*0.9
| where timedelta > lower AND timedelta < upper
| stats count, values(avg) as TimeInterval by src, dest, dest_port, total
| eval prcnt = (count/total)*100
| where prcnt > 90 AND total > 10

Got 0 results.

  1. Didn’t guess - ran index="empire" sourcetype="bro:http:json" | head 1 to look at one raw event. src/dest/dest_port don’t exist in this data. The real fields are id.orig_h, id.orig_p, id.resp_h, id.resp_p - same convention as every other bro:*:json sourcetype in this module.

  2. Swapped the field names and re-ran:

index="empire" sourcetype="bro:http:json"
| sort 0 _time
| streamstats current=f last(_time) as prevtime by id.orig_h, id.resp_h, id.resp_p
| eval timedelta = _time - prevtime
| eventstats avg(timedelta) as avg, count as total by id.orig_h, id.resp_h, id.resp_p
| eval upper=avg*1.1
| eval lower=avg*0.9
| where timedelta > lower AND timedelta < upper
| stats count, values(avg) as TimeInterval by id.orig_h, id.resp_h, id.resp_p, total
| eval prcnt = (count/total)*100
| where prcnt > 90 AND total > 10

Still 0 results. Field names were fixed, so the problem moved to the last line.

  1. Removed where prcnt > 90 AND total > 10 and sorted by prcnt descending instead, to see the actual numbers before any threshold cut them:
index="empire" sourcetype="bro:http:json"
| sort 0 _time
| streamstats current=f last(_time) as prevtime by id.orig_h, id.resp_h, id.resp_p
| eval timedelta = _time - prevtime
| eventstats avg(timedelta) as avg, count as total by id.orig_h, id.resp_h, id.resp_p
| eval upper=avg*1.1
| eval lower=avg*0.9
| where timedelta > lower AND timedelta < upper
| stats count, values(avg) as TimeInterval by id.orig_h, id.resp_h, id.resp_p, total
| eval prcnt = (count/total)*100
| sort - prcnt

Got:

id.orig_h id.resp_h id.resp_p total count TimeInterval prcnt
10.0.10.100 192.168.151.181 80 48 41 4.680851063829787 85.42
10.0.10.30 34.122.121.32 80 2 1 600 50
10.0.10.30 35.224.170.84 80 2 1 600 50
  1. Read the table. The prcnt > 90 cutoff was a few points too strict for this dataset - that’s why the filtered query gave 0. Top row has 48 total connections with 85% landing in the jitter band: real volume, real consistency, that’s the beacon. The other two rows only have 2 total connections each, so their 50% is just 1-out-of-2 - not enough data to mean anything, ignored them.

Answer: TimeInterval = 4.680851063829787 (~4.68 seconds), from 10.0.10.100 → 192.168.151.181:80.

2. Detecting PrintNightmare

Question: Using the printnightmare index and the bro:dce_rpc:json sourcetype, create a Splunk search that will detect possible exploitation of the PrintNightmare vulnerability. Enter the IP included in the id.orig_h field as the answer.

What I did, in order:

  1. No chapter query to start from this time, so reasoned from the vulnerability itself: PrintNightmare (CVE-2021-1675 / CVE-2021-34527) abuses the print spooler RPC interface (endpoint="spoolss") by calling RpcAddPrinterDriverEx (or the older RpcAddPrinterDriver) to push an unsigned driver DLL with SYSTEM privileges. Same endpoint/operation field pattern already used for zerologon’s endpoint="netlogon" detection, just a different endpoint/operation pair.

  2. Built the query directly off that:

index="printnightmare" sourcetype="bro:dce_rpc:json"
| where endpoint="spoolss" AND (operation="RpcAddPrinterDriverEx" OR operation="RpcAddPrinterDriver")
| stats count by id.orig_h, id.resp_h, operation
  1. Ran it. Got one row back:
id.orig_h id.resp_h operation count
192.168.1.149 192.168.1.157 RpcAddPrinterDriverEx 3
  1. Read it: one source (192.168.1.149) called RpcAddPrinterDriverEx against one print server (192.168.1.157) three times. A single non-print-server client calling this specific operation at all is the signal, since legitimate driver installs come from the print server or admin tooling, not an arbitrary workstation.

Answer: id.orig_h = 192.168.1.149

3. Detecting BloodHound activity

Question: Using the bloodhound_all_no_kerberos_sign index and the bro:dce_rpc:json sourcetype, create a Splunk search that will detect possible BloodHound activity. Enter the IP included in the id.orig_h field as the answer.

What I did, in order:

  1. Reasoned from how BloodHound/SharpHound’s network collection actually works: it calls specific DCE-RPC interfaces against hosts to map sessions and local group membership - srvsvc (NetSessionEnum/NetrSessionEnum), samr (SamrEnumerateUsersInDomain, SamrGetMembersInAlias, etc.), lsarpc (SID lookups). Built a breadth-based query first, since BloodHound’s whole point is sweeping many hosts fast:
index="bloodhound_all_no_kerberos_sign" sourcetype="bro:dce_rpc:json"
| where endpoint IN ("srvsvc", "samr", "lsarpc")
| stats count, dc(id.resp_h) as distinct_hosts, values(endpoint) as endpoints_used, values(operation) as operations by id.orig_h
| sort - distinct_hosts
  1. Ran it. Got one row back:
id.orig_h count distinct_hosts endpoints_used operations
192.168.109.105 29 1 samr, srvsvc NetrSessionEnum, SamrCloseHandle, SamrConnect5, SamrGetMembersInAlias, SamrOpenAlias, SamrOpenDomain
  1. distinct_hosts=1 looked like it broke the breadth assumption the query was built on. Checked the rest of the row instead of dismissing it: this dataset only has one (id.orig_h,id.resp_h) pair total, same structural situation as the RDP brute-force and HTTP-exfil chapters - with only one target in the data, breadth-across-hosts can’t show up regardless of whether it’s a real attack. The operation list itself is the confirming signal: NetrSessionEnum + SamrGetMembersInAlias/SamrOpenAlias/SamrOpenDomain is exactly BloodHound’s Session/LocalAdmin collection method, not normal admin traffic.

Answer: id.orig_h = 192.168.109.105

References

[HTB Academy] Certified Defensive Security Analyst (CDSA) - Module 6: Detecting Windows Attacks with Splunk Skill Assessment. https://academy.hackthebox.com/