Triage an SSH brute force with TB

Someone has been guessing passwords on a Linux server. Using the sample auth.log that comes with TB, you'll find the attacker, see which accounts they tried, and answer the question that matters: did they get in? Every step runs here in your browser.

1.Look at the raw log

Start by reading a few lines. fs.lines gives back a list, one item per line.

import fs

let lines = fs.lines("apps/data/auth.log")
say(len(lines), "lines")
for line in lines.slice(20, 26) {
  say(line)
}

2.Turn lines into records

Lines are hard to count. logs.read parses each one into a map with fields like user, ip, action and success.

import logs

let records = logs.read("apps/data/auth.log", "auth")
let failed = records.filter(fn(r) { return r.get("action") == "failed_login" })
say(failed[0])

3.Count failures by address

logs.top counts how often each value of a field appears. One address stands out.

import logs
import report

let records = logs.read("apps/data/auth.log", "auth")
let failed = records.filter(fn(r) { return r.get("action") == "failed_login" })
say(report.table(logs.top(failed, "ip", 5)))
say(report.table(logs.top(failed, "user", 5)))

4.Let a rule find it

Counting by hand works once. A detection rule works every time. detect.brute_force looks for five or more failures from one source within ten minutes, and checks what happened next.

import logs
import detect

let records = logs.read("apps/data/auth.log", "auth")
for alert in detect.brute_force(records) {
  say(f"{alert.level}: {alert.rule} from {alert.by}")
  say(f"  {alert.count} failures between {alert.first_when} and {alert.last_when}")
  say(f"  accounts tried: {alert.users}")
  say(f"  logged in afterwards: {alert.success_after} as {alert.success_user}")
}

5.What did they do once inside?

The attacker guessed the deploy account's password. Follow that account from the moment it logged in.

import logs

let records = logs.read("apps/data/auth.log", "auth")
let start = records.filter(fn(r) {
  return r.get("user") == "deploy" and r.get("success") == true
}).first().time

for r in records {
  if r.time >= start and (r.get("user") == "deploy" or r.get("target_user") == "deploy") {
    say(r.when, r.action, r.message)
  }
}

A new account, a sudo session: this is an incident, not just noise.

6.Write it up

Alerts are maps, so report can turn them into a table for a ticket, Markdown, CSV or a full HTML page.

import logs
import detect
import report

let records = logs.read("apps/data/auth.log", "auth")
let alerts = detect.brute_force(records)
say(report.markdown(alerts, ["level", "rule", "by", "count", "success_user", "success_when"]))

Where next

The Brute-force hunter app does all of this for Linux and Windows logs from the command line, and the Log triage desktop app shows alerts in a window:

The Log triage desktop app listing four alerts from auth.log
apps/log-triage-gui running on the same auth.log.

See also the logs, detect and report references.