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:
- Took the beaconing query from section 12 as the starting point (
src,dest,dest_portfields) and swapped the index/sourcetype toindex="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.
-
Didn’t guess - ran
index="empire" sourcetype="bro:http:json" | head 1to look at one raw event.src/dest/dest_portdon’t exist in this data. The real fields areid.orig_h,id.orig_p,id.resp_h,id.resp_p- same convention as every otherbro:*:jsonsourcetype in this module. -
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.
- Removed
where prcnt > 90 AND total > 10and sorted byprcntdescending 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 |
- Read the table. The
prcnt > 90cutoff 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:
-
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 callingRpcAddPrinterDriverEx(or the olderRpcAddPrinterDriver) to push an unsigned driver DLL with SYSTEM privileges. Sameendpoint/operationfield pattern already used for zerologon’sendpoint="netlogon"detection, just a different endpoint/operation pair. -
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
- Ran it. Got one row back:
| id.orig_h | id.resp_h | operation | count |
|---|---|---|---|
| 192.168.1.149 | 192.168.1.157 | RpcAddPrinterDriverEx | 3 |
- Read it: one source (
192.168.1.149) calledRpcAddPrinterDriverExagainst 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:
- 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
- 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 |
distinct_hosts=1looked 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/SamrOpenDomainis 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/