- Published on
Analyzing Zeek's SSH Brute Force Detection Script
- Authors

- Name
- Morphy Chan
The script is located at /opt/zeek/share/zeek/policy/protocols/ssh/detect-bruteforcing.zeek. It was last updated in 2015 — quite dated.
$ ls -lh
total 16K
-rw-r--r-- 1 root root 3.2K Jan 28 2015 detect-bruteforcing.zeek
-rw-r--r-- 1 root root 1.5K Jan 28 2015 geo-data.zeek
-rw-r--r-- 1 root root 1.6K Jan 28 2015 interesting-hostnames.zeek
-rw-r--r-- 1 root root 1008 Jan 28 2015 software.zeek
1. Parameter Definitions
The script starts with a series of parameter definitions:
export {
redef enum Notice::Type += {
## ...
Password_Guessing,
## ...
Login_By_Password_Guesser,
};
redef enum Intel::Where += {
## An indicator of the login for the intel framework.
SSH::SUCCESSFUL_LOGIN,
};
## The number of failed SSH connections before a host is designated as
## guessing passwords.
const password_guesses_limit: double = 30 &redef;
## The amount of time to remember presumed non-successful logins to
## build a model of a password guesser.
const guessing_timeout = 30 mins &redef;
## ...
const ignore_guessers: table[subnet] of subnet &redef;
}
- Two new
Noticetypes are defined, as the detection results are ultimately written tonotice.log. The names are self-explanatory:Password_Guessingfor brute force events, andLogin_By_Password_Guesserfor successful logins after brute forcing. - A common industry approach to detecting brute force attacks is checking whether the number of attempts within a time window exceeds a preset threshold. This method is simple and efficient, though it may miss attempts spanning across time windows. This script follows the same logic.
password_guesses_limitdefines the attempt frequency threshold (default: 30);guessing_timeoutdefines the time window (default: 30 minutes). ignore_guessersdefines an IP whitelist.
2. Detection Logic Using the SumStats Framework
Zeek provides a statistics framework called SumStats, primarily used for aggregating various network data metrics. It's well-suited for implementing frequency-based detection scripts. The SumStats framework 1 involves three steps:
- Observation: Collecting data — in this script, collecting each SSH login attempt.
- Reducer: Processing the collected data — e.g., summation, aggregation.
- SumStat: Responding to processing results — i.e., the action, such as generating a notice when SSH login attempt frequency exceeds the threshold.
Note that Zeek's architecture is distributed, so the SumStats framework likely draws on Map-Reduce principles.
Let's examine how the detection script implements these three steps.
2.1 Observing ssh_auth_failed Events
ssh_auth_failed(c: connection) is a Zeek event triggered when an SSH login failure is detected:
event ssh_auth_failed(c: connection)
{
local id = c$id;
# Add data to the FAILED_LOGIN metric unless this connection should
# be ignored.
if ( ! (id$orig_h in ignore_guessers &&
id$resp_h in ignore_guessers[id$orig_h]) )
SumStats::observe("ssh.login.failure", [$host=id$orig_h], [$str=cat(id$resp_h)]);
}
When triggered, the observe method is called for IPs not in the whitelist. The observe method takes three parameters:
- Id: A string identifier for the observed data. Since
observemay be called in multiple scripts for different data, each data stream needs a unique identifier — here"ssh.login.failure"identifies SSH login failure events. - Key: A
SumStats::Keyrecord containingstrandhostfields. Here$hostis used with the source IP of the failed SSH login as the key. - Obs: A
SumStats::Observationrecord withnum,dbl, andstrfields. Here the$strtype stores the destination IP of the failed SSH login.
2.2 Reduce and SumStat
Data reduction and the sumstat action are handled in the zeek_init() event.
2.2.1 Reducer
First, a SumStats::Reducer is defined:
event zeek_init()
{
local r1: SumStats::Reducer = [$stream="ssh.login.failure", $apply=set(SumStats::SUM, SumStats::SAMPLE), $num_samples=5];
...
}
The Reducer record includes:
stream: The data stream identifier, matching the corresponding
observemethod'sId.apply: A set of
SumStats::Calculationvalues defining what processing to apply. Here two methods are used — SUM and SAMPLE:Calculate the sum of the values. For string values, this will be the number of strings.
Since the observation phase stores destination IPs as strings, SUM here counts the number of strings — i.e., the number of failed SSH attempts.
Get uniquely distributed random samples from the observation stream.
Samples the observed SSH login destination IPs, with the sample count based on
num_samples.
2.2.2 SumStat
Next comes the action processing after reduction, using the sumstat method:
event zeek_init()
{
...
SumStats::create([$name="detect-ssh-bruteforcing",
$epoch=guessing_timeout,
$reducers=set(r1),
$threshold_val(key: SumStats::Key, result: SumStats::Result) =
{
return result["ssh.login.failure"]$sum;
},
$threshold=password_guesses_limit,
$threshold_crossed(key: SumStats::Key, result: SumStats::Result) =
{
local r = result["ssh.login.failure"];
local sub_msg = fmt("Sampled servers: ");
local samples = r$samples;
for ( i in samples )
{
if ( samples[i]?$str )
sub_msg = fmt("%s%s %s", sub_msg, i==0 ? "":",", samples[i]$str);
}
# Generate the notice.
NOTICE([$note=Password_Guessing,
$msg=fmt("%s appears to be guessing SSH passwords (seen in %d connections).", key$host, r$num),
$sub=sub_msg,
$src=key$host,
$identifier=cat(key$host)]);
}]);
}
SumStats::create includes:
- name: The name of this statistical analysis — here, SSH brute force detection.
- epoch: The time period — the detection time window defined in the parameters.
- threshold_val: Since we need to compare against the preset threshold, we need the count of observed SSH login failures within the time window. The
SUMmethod from the reducer provides this. - threshold: The preset threshold defined in the parameters.
- threshold_crossed: A Zeek-provided callback triggered when the statistical result exceeds the threshold:
- Results are stored in
result["ssh.login.failure"] - Since the reducer also uses
SAMPLE, the sampled data is formatted intosub_msg - The relevant information is output to
notice.log
- Results are stored in
3. Script Verification
Running this script against a pcap file containing SSH brute force traffic successfully generates an alert in notice.log:
$ cat notice.log | jq
{
"ts": 1617095241.708715,
"note": "SSH::Password_Guessing",
"msg": "192.168.153.6 appears to be guessing SSH passwords (seen in 30 connections).",
"sub": "Sampled servers: 192.168.153.18, 192.168.153.18, 192.168.153.18, 192.168.153.18, 192.168.153.18",
"src": "192.168.153.6",
"actions": [
"Notice::ACTION_LOG"
],
"suppress_for": 3600
}