Splunk Lab for Beginners: Find an SSH Brute Force in Real Logs
0 / 7 steps
The goal
Load real network logs into Splunk, pull the fields out yourself, and find an SSH brute-force attack, a machine that failed hundreds of logins and then got in, and a DNS “red flag” that turns out to be harmless. Every step shows the number you should get, so you know you’re on track.
Public practice data. The logs come from the 2012 Mid-Atlantic Collegiate Cyber Defense Competition (MACCDC), where student blue teams defended networks against a red team, captured by Zeek and shared on SecRepo. The addresses are private competition ranges. Nothing here touches a live network.
Why these logs. Toy datasets are clean; real ones are noisy. Half of learning a SIEM is telling an attack from background noise, and this capture has plenty of both: 428,000 DNS queries and 7,000 SSH connections over two days.
Credit where it’s due. The idea comes from Rajneesh Gupta’s Splunk Projects for Beginners, which covers seven log types from the same capture. This lab goes deeper on two of them, with real field extraction and checked results.
Steps
01Get Splunk running
You need Splunk Enterprise in its free mode. If you don’t have it yet, the Splunk Free with Docker lab gets you there in about 15 minutes. Then log in at http://localhost:8000 and open Search & Reporting.
Splunk Free’s limit is 500 MB of new data a day. These two files are about 7 MB together, so you’re nowhere near it.
02Download the two logs
Download both files with your browser. Keep them as .gz; Splunk can read compressed files directly. If your upload ever refuses one, unzip it first with 7-Zip or gunzip.
https://www.secrepo.com/maccdc2012/dns.log.gz
https://www.secrepo.com/maccdc2012/ssh.log.gz
What’s inside: one event per line, fields separated by tabs, no header row. The first field is a Unix timestamp (seconds since 1970). Splunk needs two small settings to read it, which you add in the next step.
03Upload them with their own source types
Go to Settings → Add Data → Upload and pick dns.log.gz. On Set Source Type, make three changes: open Event Breaks and choose Every Line; open Timestamp, choose Advanced and set Timestamp format to %s.%6N; then open Advanced, click New setting and add MAX_DAYS_AGO with the value 10951. Then click Save As and name the source type zeek:dns. Leave the index as main, click through Review and Submit. Repeat for ssh.log.gz with the name zeek:ssh.
Now check both arrived. Set the time picker to All time: these events are from March 2012, so the default “Last 24 hours” shows nothing.
sourcetype="zeek:*" | stats count by sourcetype
sourcetype count zeek:dns 427935 zeek:ssh 7143
The 2,000-day trap. By default Splunk ignores any timestamp more than 2,000 days old and quietly stamps the event with the time you uploaded it. These logs are from 2012, so without MAX_DAYS_AGO every event lands on today’s date and the five-minute alert in step 07 gives the wrong answer. Check the Time column after uploading: it should say 3/16/12 or 3/17/12. Old practice datasets hit this all the time.
04Pull the DNS fields out yourself
Splunk doesn’t know Zeek’s column order, so you name the fields with rex: each [^\t]* grabs everything up to the next tab. Run this to see which kinds of lookups the network made.
sourcetype="zeek:dns" | rex "^(?<ts>[^\t]*)\t(?<uid>[^\t]*)\t(?<src_ip>[^\t]*)\t(?<src_port>[^\t]*)\t(?<dest_ip>[^\t]*)\t(?<dest_port>[^\t]*)\t(?<proto>[^\t]*)\t(?<trans_id>[^\t]*)\t(?<query>[^\t]*)\t(?<qclass>[^\t]*)\t(?<qclass_name>[^\t]*)\t(?<qtype>[^\t]*)\t(?<qtype_name>[^\t]*)\t(?<rcode>[^\t]*)\t(?<rcode_name>[^\t]*)" | stats count by qtype_name | sort - count | head 5
qtype_name count A 212477 NB 77199 AAAA 54519 PTR 53017 TXT 12870
Read it like an analyst: A and AAAA are normal web lookups. NB is NetBIOS name chatter from Windows machines, a big part of the noise. PTR lookups (IP back to name) in bulk can mean someone mapping the network.
05DNS: separate signal from noise
Failed lookups (NXDOMAIN) are worth a look: malware probing for its control server often fails a lot. Find who fails most.
sourcetype="zeek:dns" | rex "^(?<ts>[^\t]*)\t(?<uid>[^\t]*)\t(?<src_ip>[^\t]*)\t(?<src_port>[^\t]*)\t(?<dest_ip>[^\t]*)\t(?<dest_port>[^\t]*)\t(?<proto>[^\t]*)\t(?<trans_id>[^\t]*)\t(?<query>[^\t]*)\t(?<qclass>[^\t]*)\t(?<qclass_name>[^\t]*)\t(?<qtype>[^\t]*)\t(?<qtype_name>[^\t]*)\t(?<rcode>[^\t]*)\t(?<rcode_name>[^\t]*)" | where rcode_name="NXDOMAIN" | stats count by src_ip | sort - count | head 3
src_ip count 192.168.202.103 7471 192.168.202.138 4762 192.168.202.79 4711
Now try a classic “DNS tunnelling” check: very long query names.
sourcetype="zeek:dns" | rex "^(?<ts>[^\t]*)\t(?<uid>[^\t]*)\t(?<src_ip>[^\t]*)\t(?<src_port>[^\t]*)\t(?<dest_ip>[^\t]*)\t(?<dest_port>[^\t]*)\t(?<proto>[^\t]*)\t(?<trans_id>[^\t]*)\t(?<query>[^\t]*)\t(?<qclass>[^\t]*)\t(?<qclass_name>[^\t]*)\t(?<qtype>[^\t]*)\t(?<qtype_name>[^\t]*)\t(?<rcode>[^\t]*)\t(?<rcode_name>[^\t]*)" | eval qlen=len(query) | where qlen>60 | stats count by query | sort - count
775 events in total. Click through the results: almost all of them (750) end in ip6.arpa.
The lesson is the false positive. Those long names are IPv6 reverse lookups, which are long by design. A rule that fires on length alone would bury you. Good detections filter out the known-harmless pattern first, for example NOT query="*.ip6.arpa".
06SSH: find the brute force
Same trick for SSH: name the fields, then see how connections ended.
sourcetype="zeek:ssh" | rex "^(?<ts>[^\t]*)\t(?<uid>[^\t]*)\t(?<src_ip>[^\t]*)\t(?<src_port>[^\t]*)\t(?<dest_ip>[^\t]*)\t(?<dest_port>[^\t]*)\t(?<status>[^\t]*)\t(?<direction>[^\t]*)\t(?<client>[^\t]*)\t(?<server>[^\t]*)" | stats count by status
status count failure 5069 undetermined 1773 success 301
Now rank the sources by failed logins, and count how many different servers each one hit.
sourcetype="zeek:ssh" | rex "^(?<ts>[^\t]*)\t(?<uid>[^\t]*)\t(?<src_ip>[^\t]*)\t(?<src_port>[^\t]*)\t(?<dest_ip>[^\t]*)\t(?<dest_port>[^\t]*)\t(?<status>[^\t]*)\t(?<direction>[^\t]*)\t(?<client>[^\t]*)\t(?<server>[^\t]*)" | where status="failure" | stats count as failures dc(dest_ip) as servers by src_ip | sort - failures | head 3
src_ip failures servers 192.168.202.141 2365 1 192.168.202.110 613 35 192.168.204.45 574 48
Two different attacks. 192.168.202.141 hammered one server 2,365 times in about eight minutes on 17 March: a classic brute force. The next two spread their attempts over dozens of servers, which looks more like password spraying. Run | stats count by client on the SSH data and you’ll even spot Nmap announcing itself as SSH-2.0-Nmap-SSH2-Hostkey.
07SSH: who failed, then got in?
The question that matters: did any of those failures turn into a login? Count failures and successes per source in one pass.
sourcetype="zeek:ssh" | rex "^(?<ts>[^\t]*)\t(?<uid>[^\t]*)\t(?<src_ip>[^\t]*)\t(?<src_port>[^\t]*)\t(?<dest_ip>[^\t]*)\t(?<dest_port>[^\t]*)\t(?<status>[^\t]*)\t(?<direction>[^\t]*)\t(?<client>[^\t]*)\t(?<server>[^\t]*)" | stats count(eval(status="failure")) as failures count(eval(status="success")) as successes dc(dest_ip) as servers by src_ip | where failures>0 AND successes>0 | sort - failures | head 3
src_ip failures successes servers 192.168.202.110 613 6 38 192.168.204.45 574 2 48 192.168.202.68 161 6 48
23 sources have both. Finally, turn the brute-force pattern into a rule: more than 20 failures from one source in five minutes.
sourcetype="zeek:ssh" | rex "^(?<ts>[^\t]*)\t(?<uid>[^\t]*)\t(?<src_ip>[^\t]*)\t(?<src_port>[^\t]*)\t(?<dest_ip>[^\t]*)\t(?<dest_port>[^\t]*)\t(?<status>[^\t]*)\t(?<direction>[^\t]*)\t(?<client>[^\t]*)\t(?<server>[^\t]*)" | where status="failure" | bin _time span=5m | stats count by _time src_ip | where count>20
38 five-minute windows from 9 sources. The busiest: 2363 failures from 192.168.202.141.
Treat “success” as a lead, not proof. Zeek’s SSH success flag in this 2012 capture is an educated guess from the traffic, not a confirmed login. In a real SOC, failures-then-success is where you open a case and check the server’s own auth log. To keep the rule, click Save As → Report. Splunk Free can’t schedule alerts; during the 60-day Enterprise trial (or on a paid licence), Save As → Alert turns it into a rule that fires within minutes of an attack on live data.
You loaded raw logs, extracted fields by hand, found a brute force, flagged the logins worth investigating, and saw a scary-looking DNS pattern turn out to be harmless. That’s a real slice of a SOC analyst’s day.
Before you leave — prove it
Without scrolling back: which source brute-forced 192.168.229.101, roughly how many times, and why wouldn’t you alert on long DNS names alone? Then pick one of the other five logs from the same capture (FTP, HTTP, SMTP, DHCP or tunnel), write its rex yourself, and find one thing worth reporting.
What you learned
- Data on-boarding — custom source types, line breaking, and the All time trap with old data.
- Field extraction with rex — naming columns in raw, header-less logs.
- Brute force vs. spraying — one target hammered vs. many targets tried.
- Failures then success — the pattern that turns noise into an investigation.
- False positives — why a detection needs the harmless pattern filtered out first.
Put it on your resume. Investigated real network logs in Splunk: extracted fields and identified an SSH brute force that ended in a successful login. Change it to match what you actually did.
NextSplunk, Wazuh or Elastic: which SIEM should you learn?→
Go deeper
- Splunk Projects for Beginners — Rajneesh Gupta’s seven log projects from the same capture.
- SecRepo — more free security datasets, including the full MACCDC 2012 Zeek logs.
- Splunk rex reference — everything the extraction command can do.
- Zeek log docs — what every column means.
Original walkthrough. Tested in Splunk 10.6 (the Docker image from the setup lab) on 6 October 2026: every expected result above came from running these exact searches. The logs were loaded from the command line with the same source-type settings as step 03, rather than through the Add Data screens, so menu wording there may differ slightly by version. Spot something wrong? tell me and I will fix it.
Nyx P.S. Tested. Steven’s Splunk container started first try. I checked the logs. Twice. 😏