Zeek - Track Port Scans
Hi !
It’s time to talk a bit about the Zeek tool again.
We all see port scans hitting our network equipment and/or servers, hence the idea of using Zeek to identify the IP addresses in question.
At first glance, nothing serious, because these things happen at any time, even everyday occurrences. However, a harmful effect can arise if someone happens to discover an open (and forgotten) network port that, coincidentally, corresponds to something with a security vulnerability.
Let us assume that we first want to identify the counterfeit IP addresses in question, and then, secondly, to block them at the network level.
Let’s see how “Zeek” can come to the rescue and help us identify these kinds of things. I should mention that I will strive to document each line of the script as precisely as possible to make it easier to understand.
Here is the script written in the tool's language:
# This directive loads Zeek's native notice framework. This module is responsible for centralizing alerts and generating entries in the notice.log file.
Load base/frameworks/notice
# This allows the declarations inside the block to be accessible and visible to other Zeek scripts or modules.
export {
# Zeek uses an enumeration (enum) to list all possible alert types. Here, we redefine (redef) this list by appending (+=) a new unique identifier: Port_Scan_S0_Detected. This is the exact string the OSSEC rule will look for.
redef enum Notice::Type += {
Port_Scan_S0_Detected
};
# Declares a global variable as ant hash table (or dictionary) where the key is an IP address (addr). Associated with each IP is a set of unique ports (set[port]).
# "&write_expire = 1 min": this specifies that if an IP address has not triggered a new write operation in the table for 1 minute, its entire entry is automatically deleted. This defines our rolling time window to detect the scan.
global track_s0_scans: table[addr] of set[port] &write_expire = 1 min;
}
# This events is triggered when a connection terminates or times out.
event connection_state_remove(c: connection)
{
# We extract the connection state (the same one that goes in conn.log)
local conn_state = c$conn$conn_state;
local src = c$id$orig_h;
local dst_port = c$id$resp_p;
# We are only targeting state S0 (The origin sent a SYN, no response) and ensure that the source IP is not part of the internal corporate network (defined in Zeek's native global variable Site::local_nets). This prevents blocking your own internal vulnerability scanners or monitoring tools.
if ( conn_state == "S0" && src !in Site::local_nets )
{
# We check if this is the very first time this specific IP has been seen with an S0 state within the last minute. If it is not present in our global table (src !in track_s0_scans), we initialize it by creating an empty port set (set()).
if ( src !in track_s0_scans )
track_s0_scans[src] = set();
# We insert an element into the set discussed above.
add track_s0_scans[src][dst_port];
# We verifiy if the threshold of 5 unique ports is reached.
if ( |track_s0_scans[src]| >= 5 )
{
# This function call raises the alert.
NOTICE([$note=Port_Scan_S0_Detected,
$src=src,
$msg=fmt("IP %s suspected of scanning: 5 distinct ports in S0 state (no response) in less than a minute", src),
$identifier=fmt("%s", src)]);
# We clean up to avoid spamming alerts on the 6th or 7th attempt
delete track_s0_scans[src];
}
}
}
In a future article, we will see how to use “Ossec” to detect these behaviors and, more importantly, block them using the “OpenBSD” firewall (pf).
Cheers.