Skip to content

Latest commit

 

History

History
95 lines (62 loc) · 5.93 KB

File metadata and controls

95 lines (62 loc) · 5.93 KB

Detecting SSH Brute Force (T1110.001) From Scratch

I wanted a detection engineering project that wasn't just converting someone else's advisory into a rule (I'd already done that for a SigmaHQ PR covering a CISA/NSA/FBI advisory). This one is the opposite: pick a technique, generate the attack myself, look at the real logs it produces, and write the detection from there.

The technique is T1110.001, Password Guessing, specifically against SSH. It's about as classic as blue team detections get, but that's kind of the point. If you can't write a solid brute-force detection from first principles, the fancier stuff doesn't matter much either.

The setup

Everything here ran locally on a single Ubuntu box:

  • openssh-server installed and running
  • a throwaway account (labtarget) with a weak, known password
  • a wordlist of common wrong guesses with the real password planted near the end, so the logs would show the realistic pattern: a burst of failures followed by one success

simulate_attack.sh loops through the wordlist with sshpass, attempting a login for each entry. Nothing exotic, this is roughly what a low-effort automated brute-force script looks like.

What the logs actually showed

Pulling from journalctl -u ssh, each failed attempt logs like this:

Jul 28 17:03:21 MSI sshd-session[20384]: pam_unix(sshd:auth): authentication failure; ... user=labtarget
Jul 28 17:03:23 MSI sshd-session[20384]: Failed password for labtarget from 127.0.0.1 port 57058 ssh2
Jul 28 17:03:25 MSI sshd-session[20384]: Connection closed by authenticating user labtarget 127.0.0.1 port 57058 [preauth]

repeated nine times with different source ports, then:

Jul 28 17:04:00 MSI sshd-session[20476]: Accepted password for labtarget from 127.0.0.1 port 38112 ssh2

Nine failures in about 40 seconds from the same source, then a success. Full sample is in logs/auth_sample.log.

The rule

A single Failed password line isn't worth alerting on by itself, people mistype passwords constantly. What matters is the rate from a single source. So this is two rules:

ssh_failed_password_base.yml is the base detection, it just matches on a failed sshd password auth event. Nothing clever, it exists so the correlation rule below has something to count.

ssh_bruteforce_correlation.yml is where the actual logic lives. It uses Sigma's event_count correlation type to group the base rule's matches by src_ip and fire when there are 5 or more within a 2 minute window. I originally reached for value_count, which counts distinct values of a field (useful for something like password spraying across many usernames), but that's the wrong tool for "the same failure repeated a bunch of times from one place." event_count is what you want for a straightforward volume threshold.

I picked 5-in-2-minutes somewhat arbitrarily as a starting point, low enough to catch a scripted attempt, high enough that a person who fat-fingers their password three times in frustration doesn't page anyone. In a real environment I'd tune this against actual auth failure baselines rather than guessing.

Converting and testing

Used sigma-cli (pySigma) to validate the rules and convert them to real backend queries:

sigma check rule/ssh_failed_password_base.yml
# Found 0 errors, 0 condition errors and 0 issues.

Splunk SPL:

Message="*Failed password for*"

| bin _time span=2m
| stats count as event_count by _time src_ip

| search event_count >= 5

Elastic ES|QL:

from * metadata _id, _index, _version | where Message like "*Failed password for*"
| eval timebucket=date_trunc(2minutes, @timestamp) | stats event_count=count() by timebucket, src_ip
| where event_count >= 5

Both converted cleanly with --without-pipeline, meaning the queries are portable but assume the target already normalizes the raw sshd Message field and a src_ip field, which in a real deployment would come from your log source's field extraction (Splunk's CIM authentication model, an Elastic ingest pipeline, whatever). I tried running it through Splunk's built-in splunk_cim pipeline first and it rejected the rule outright (Rule type not yet supported by the Splunk data model CIM pipeline), so for now this is the raw-field version. Worth revisiting if I extend this project.

False positives

  • A shared jump host or NAT gateway where many real users' failed logins collapse onto one source IP. This rule would need a higher threshold or an allowlist for known shared egress points.
  • A misconfigured service or cron job retrying an old credential on a loop. Looks identical to a brute force in volume, different in intent. Worth checking user= in the failed logins, service accounts usually target a fixed, narrow set of usernames rather than trying to log in as random accounts.
  • Password managers or scripts with a stale saved credential doing a handful of rapid retries before failing over. Usually stays under the 5-attempt threshold, which is part of why I picked that number.

What I'd do next

  • Test the rule against a slower, low-and-slow brute force (one attempt every few minutes) to see how easily it evades the 2 minute window, and whether a longer window with a lower threshold catches it without adding noise.
  • Add a second correlation that flags specifically when a failure burst is immediately followed by a success from the same source, that's a much higher-confidence "this probably worked" signal than the failure burst alone.
  • Try the rule against real auth logs instead of my own generated sample, mine only has one very clean attack pattern in it.

Repo layout:

wordlist.txt              guess list used against the test account
simulate_attack.sh        script that ran the simulated attack
logs/auth_sample.log      raw sshd log output from the attack
rule/                     the two Sigma rules
writeup/                  converted Splunk and Elastic queries