mirror of
https://github.com/zeek/zeek.git
synced 2025-10-13 12:08:20 +00:00
More extensive base script updating.
* This is basically another checkpoint, but the difference is that in this one all.bro loads just about all of the new scripts and functionality.
This commit is contained in:
parent
11ca973a10
commit
5a868eefda
17 changed files with 583 additions and 406 deletions
|
@ -1,9 +1,16 @@
|
||||||
|
|
||||||
# This script only aims at loading all of the base analysis scripts.
|
# This script only aims at loading all of the base analysis scripts.
|
||||||
|
@load conn
|
||||||
|
@load dns
|
||||||
@load ftp
|
@load ftp
|
||||||
@load http
|
@load http
|
||||||
@load known-services
|
@load irc
|
||||||
@load known-hosts
|
@load known-hosts
|
||||||
@load ssh
|
@load known-services
|
||||||
|
@load smtp
|
||||||
@load ssl
|
@load ssl
|
||||||
|
@load ssh
|
||||||
|
|
||||||
|
@load mime
|
||||||
|
@load software
|
||||||
|
@load weird
|
|
@ -259,12 +259,12 @@ type pcap_packet: record {
|
||||||
|
|
||||||
# GeoIP support.
|
# GeoIP support.
|
||||||
type geo_location: record {
|
type geo_location: record {
|
||||||
country_code: string &default="";
|
country_code: string &optional;
|
||||||
region: string &default="";
|
region: string &optional;
|
||||||
city: string &default="";
|
city: string &optional;
|
||||||
latitude: double &default=0.0;
|
latitude: double &optional;
|
||||||
longitude: double &default=0.0;
|
longitude: double &optional;
|
||||||
};
|
} &log;
|
||||||
|
|
||||||
type entropy_test_result: record {
|
type entropy_test_result: record {
|
||||||
entropy: double;
|
entropy: double;
|
||||||
|
@ -928,7 +928,7 @@ type X509: record {
|
||||||
issuer: string;
|
issuer: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
orig_addr: addr;
|
orig_addr: addr;
|
||||||
};
|
} &log;
|
||||||
|
|
||||||
type http_stats_rec: record {
|
type http_stats_rec: record {
|
||||||
num_requests: count;
|
num_requests: count;
|
||||||
|
|
|
@ -11,6 +11,7 @@ export {
|
||||||
trans_id: count &log &optional;
|
trans_id: count &log &optional;
|
||||||
query: string &log &optional;
|
query: string &log &optional;
|
||||||
qtype: count &log &optional;
|
qtype: count &log &optional;
|
||||||
|
qtype_name: string &log &optional;
|
||||||
qclass: count &log &optional;
|
qclass: count &log &optional;
|
||||||
rcode: count &log &optional;
|
rcode: count &log &optional;
|
||||||
QR: bool &log &default=F;
|
QR: bool &log &default=F;
|
||||||
|
@ -102,9 +103,9 @@ function set_session(c: connection, msg: dns_msg, is_query: bool)
|
||||||
if ( info?$total_answers &&
|
if ( info?$total_answers &&
|
||||||
info$total_answers != msg$num_answers + msg$num_addl + msg$num_auth )
|
info$total_answers != msg$num_answers + msg$num_addl + msg$num_auth )
|
||||||
{
|
{
|
||||||
print "the total number of answers changed midstream on a dns response.";
|
#print "the total number of answers changed midstream on a dns response.";
|
||||||
print info;
|
#print info;
|
||||||
print msg;
|
#print msg;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -122,7 +123,15 @@ event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qcla
|
||||||
c$dns$RD = msg$RD;
|
c$dns$RD = msg$RD;
|
||||||
c$dns$TC = msg$TC;
|
c$dns$TC = msg$TC;
|
||||||
c$dns$qtype = qtype;
|
c$dns$qtype = qtype;
|
||||||
|
c$dns$qtype_name = query_types[qtype];
|
||||||
c$dns$qclass = qclass;
|
c$dns$qclass = qclass;
|
||||||
|
|
||||||
|
# Decode netbios name queries
|
||||||
|
# Note: I'm ignoring the name type for now. Not sure if this should be
|
||||||
|
# worked into the query/response in some fashion.
|
||||||
|
if ( c$id$resp_p == 137/udp )
|
||||||
|
query = decode_netbios_name(query);
|
||||||
|
|
||||||
c$dns$query = query;
|
c$dns$query = query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +165,24 @@ event dns_AAAA_reply(c: connection, msg: dns_msg, ans: dns_answer, a: addr,
|
||||||
add c$dns$replies[fmt("%s", a)];
|
add c$dns$replies[fmt("%s", a)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event dns_NS_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string) &priority=5
|
||||||
|
{
|
||||||
|
set_session(c, msg, F);
|
||||||
|
|
||||||
|
if ( ! c$dns?$replies )
|
||||||
|
c$dns$replies = set();
|
||||||
|
add c$dns$replies[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
event dns_CNAME_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string) &priority=5
|
||||||
|
{
|
||||||
|
set_session(c, msg, F);
|
||||||
|
|
||||||
|
if ( ! c$dns?$replies )
|
||||||
|
c$dns$replies = set();
|
||||||
|
add c$dns$replies[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
event dns_MX_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string,
|
event dns_MX_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string,
|
||||||
preference: count) &priority=5
|
preference: count) &priority=5
|
||||||
|
@ -176,6 +203,43 @@ event dns_PTR_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string)
|
||||||
add c$dns$replies[name];
|
add c$dns$replies[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event dns_SOA_reply(c: connection, msg: dns_msg, ans: dns_answer, soa: dns_soa)
|
||||||
|
{
|
||||||
|
set_session(c, msg, F);
|
||||||
|
|
||||||
|
if ( ! c$dns?$replies )
|
||||||
|
c$dns$replies = set();
|
||||||
|
add c$dns$replies[soa$mname];
|
||||||
|
}
|
||||||
|
|
||||||
|
event dns_WKS_reply(c: connection, msg: dns_msg, ans: dns_answer)
|
||||||
|
{
|
||||||
|
set_session(c, msg, F);
|
||||||
|
}
|
||||||
|
|
||||||
|
event dns_SRV_reply(c: connection, msg: dns_msg, ans: dns_answer)
|
||||||
|
{
|
||||||
|
set_session(c, msg, F);
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: figure out how to handle these
|
||||||
|
#event dns_EDNS(c: connection, msg: dns_msg, ans: dns_answer)
|
||||||
|
# {
|
||||||
|
#
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
#event dns_EDNS_addl(c: connection, msg: dns_msg, ans: dns_edns_additional)
|
||||||
|
# {
|
||||||
|
#
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
#event dns_TSIG_addl(c: connection, msg: dns_msg, ans: dns_tsig_additional)
|
||||||
|
# {
|
||||||
|
#
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
event dns_rejected(c: connection, msg: dns_msg,
|
event dns_rejected(c: connection, msg: dns_msg,
|
||||||
query: string, qtype: count, qclass: count)
|
query: string, qtype: count, qclass: count)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,10 +1,32 @@
|
||||||
##! Activates port-independent protocol detection.
|
##! Activates port-independent protocol detection.
|
||||||
|
|
||||||
|
@load functions
|
||||||
@load signatures
|
@load signatures
|
||||||
|
|
||||||
|
module DPD;
|
||||||
|
|
||||||
|
# Add the DPD signatures.
|
||||||
redef signature_files += "dpd.sig";
|
redef signature_files += "dpd.sig";
|
||||||
|
redef enum Log::ID += { DPD };
|
||||||
|
|
||||||
|
export {
|
||||||
|
type Info: record {
|
||||||
|
ts: time &log;
|
||||||
|
id: conn_id &log;
|
||||||
|
proto: transport_proto &log;
|
||||||
|
analyzer: string &log;
|
||||||
|
failure_reason: string &log;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
redef record connection += {
|
||||||
|
dpd: Info &optional;
|
||||||
|
};
|
||||||
|
|
||||||
event bro_init()
|
event bro_init()
|
||||||
{
|
{
|
||||||
|
Log::create_stream(DPD, [$columns=Info]);
|
||||||
|
|
||||||
for ( a in dpd_config )
|
for ( a in dpd_config )
|
||||||
{
|
{
|
||||||
for ( p in dpd_config[a]$ports )
|
for ( p in dpd_config[a]$ports )
|
||||||
|
@ -16,16 +38,25 @@ event bro_init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event protocol_confirmation(c: connection, atype: count, aid: count)
|
event protocol_confirmation(c: connection, atype: count, aid: count) &priority=10
|
||||||
{
|
{
|
||||||
|
if ( fmt("-%s",analyzer_name(atype)) in c$service )
|
||||||
delete c$service[fmt("-%s",analyzer_name(atype))];
|
delete c$service[fmt("-%s",analyzer_name(atype))];
|
||||||
|
|
||||||
add c$service[analyzer_name(atype)];
|
add c$service[analyzer_name(atype)];
|
||||||
}
|
}
|
||||||
|
|
||||||
event protocol_violation(c: connection, atype: count, aid: count,
|
event protocol_violation(c: connection, atype: count, aid: count,
|
||||||
reason: string) &priority=10
|
reason: string) &priority=10
|
||||||
{
|
{
|
||||||
|
if ( analyzer_name(atype) in c$service )
|
||||||
delete c$service[analyzer_name(atype)];
|
delete c$service[analyzer_name(atype)];
|
||||||
add c$service[fmt("-%s",analyzer_name(atype))];
|
add c$service[fmt("-%s",analyzer_name(atype))];
|
||||||
|
|
||||||
|
Log::write(DPD, [$ts=network_time(),
|
||||||
|
$id=c$id,
|
||||||
|
$proto=get_conn_transport_proto(c$id),
|
||||||
|
$analyzer=analyzer_name(atype),
|
||||||
|
$failure_reason=reason]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
@load http/base
|
@load http/base
|
||||||
@load software
|
@load software
|
||||||
|
|
||||||
|
module HTTP;
|
||||||
|
|
||||||
redef enum Software::Type += {
|
redef enum Software::Type += {
|
||||||
WEB_SERVER,
|
WEB_SERVER,
|
||||||
WEB_BROWSER,
|
WEB_BROWSER,
|
||||||
|
@ -32,7 +34,7 @@ event http_header(c: connection, is_orig: bool, name: string, value: string) &pr
|
||||||
{
|
{
|
||||||
# Flash doesn't include it's name so we'll add it here since it
|
# Flash doesn't include it's name so we'll add it here since it
|
||||||
# simplifies the version parsing.
|
# simplifies the version parsing.
|
||||||
value = cat("Flash ", value);
|
value = cat("Flash/", value);
|
||||||
local flash_version = Software::parse(value, c$id$orig_h, WEB_BROWSER_PLUGIN);
|
local flash_version = Software::parse(value, c$id$orig_h, WEB_BROWSER_PLUGIN);
|
||||||
Software::found(c$id, flash_version);
|
Software::found(c$id, flash_version);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ module IRC;
|
||||||
redef enum Log::ID += { IRC };
|
redef enum Log::ID += { IRC };
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
type Tags: enum { EMPTY };
|
||||||
|
|
||||||
type Info: record {
|
type Info: record {
|
||||||
ts: time &log;
|
ts: time &log;
|
||||||
id: conn_id &log;
|
id: conn_id &log;
|
||||||
|
@ -18,6 +20,7 @@ export {
|
||||||
command: string &log &optional;
|
command: string &log &optional;
|
||||||
value: string &log &optional;
|
value: string &log &optional;
|
||||||
addl: string &log &optional;
|
addl: string &log &optional;
|
||||||
|
tags: set[Tags] &log &default=set();
|
||||||
};
|
};
|
||||||
|
|
||||||
const logged_commands = set("JOIN", "DCC SEND");
|
const logged_commands = set("JOIN", "DCC SEND");
|
||||||
|
@ -148,10 +151,3 @@ event irc_join_message(c: connection, info_list: irc_join_list) &priority=-5
|
||||||
Log::write(IRC, c$irc);
|
Log::write(IRC, c$irc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event expected_connection_seen(c: connection, a: count) &priority=10
|
|
||||||
{
|
|
||||||
local id = c$id;
|
|
||||||
if ( [id$resp_h, id$resp_p] in dcc_expected_transfers )
|
|
||||||
add c$service["ftp-data"];
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,26 +12,75 @@
|
||||||
module IRC;
|
module IRC;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
redef enum Tags += { EXTRACTED_FILE };
|
||||||
|
|
||||||
|
## Pattern of file mime types to extract from IRC DCC file transfers.
|
||||||
|
const extract_file_types = /NO_DEFAULT/ &redef;
|
||||||
|
|
||||||
|
## The on-disk prefix for files to be extracted from IRC DCC file transfers.
|
||||||
|
const extraction_prefix = "irc-dcc-item" &redef;
|
||||||
|
|
||||||
redef record Info += {
|
redef record Info += {
|
||||||
file_name: string &optional;
|
dcc_file_name: string &log &optional;
|
||||||
file_size: count &optional;
|
dcc_file_size: count &log &optional;
|
||||||
|
dcc_mime_type: string &log &optional;
|
||||||
|
|
||||||
|
## The file handle for the file to be extracted
|
||||||
|
extraction_file: file &log &optional;
|
||||||
|
|
||||||
|
## A boolean to indicate if the current file transfer shoudl be transfered.
|
||||||
|
extract_file: bool &default=F;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
global dcc_expected_transfers: table[addr, port] of Info = table();
|
global dcc_expected_transfers: table[addr, port] of Info = table();
|
||||||
|
|
||||||
event file_transferred(c: connection, prefix: string, descr: string,
|
event file_transferred(c: connection, prefix: string, descr: string,
|
||||||
mime_type: string) &priority=5
|
mime_type: string) &priority=3
|
||||||
{
|
{
|
||||||
local id = c$id;
|
local id = c$id;
|
||||||
if ( [id$resp_h, id$resp_p] in dcc_expected_transfers )
|
if ( [id$resp_h, id$resp_p] !in dcc_expected_transfers )
|
||||||
|
return;
|
||||||
|
|
||||||
|
local irc = dcc_expected_transfers[id$resp_h, id$resp_p];
|
||||||
|
|
||||||
|
irc$dcc_mime_type = mime_type;
|
||||||
|
|
||||||
|
if ( extract_file_types in mime_type )
|
||||||
|
irc$extract_file = T;
|
||||||
|
|
||||||
|
if ( irc$extract_file )
|
||||||
{
|
{
|
||||||
|
add irc$tags[EXTRACTED_FILE];
|
||||||
|
irc$extraction_file = open(fmt("%s.%s", extraction_prefix, id_string(c$id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
event file_transferred(c: connection, prefix: string, descr: string,
|
||||||
|
mime_type: string) &priority=-4
|
||||||
|
{
|
||||||
|
local id = c$id;
|
||||||
|
if ( [id$resp_h, id$resp_p] !in dcc_expected_transfers )
|
||||||
|
return;
|
||||||
|
|
||||||
|
local irc = dcc_expected_transfers[id$resp_h, id$resp_p];
|
||||||
|
|
||||||
|
if ( irc$extract_file && irc?$extraction_file )
|
||||||
|
set_contents_file(id, CONTENTS_RESP, irc$extraction_file);
|
||||||
|
|
||||||
|
# Delete these values in case another DCC transfer
|
||||||
|
# happens during the IRC session.
|
||||||
|
# TODO: uncomment these when this operator works
|
||||||
|
# delete irc$extract_file;
|
||||||
|
# delete irc$extraction_file;
|
||||||
|
# delete irc$dcc_file_name;
|
||||||
|
# delete irc$dcc_file_size;
|
||||||
|
# delete irc$dcc_mime_type;
|
||||||
delete dcc_expected_transfers[id$resp_h, id$resp_p];
|
delete dcc_expected_transfers[id$resp_h, id$resp_p];
|
||||||
local fh = open("irc-dcc-item");
|
|
||||||
set_contents_file(id, CONTENTS_RESP, fh);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
event irc_server(c: connection, prefix: string, data: string) &priority=5
|
event irc_server(c: connection, prefix: string, data: string) &priority=5
|
||||||
{
|
{
|
||||||
local parts = split_all(data, / /);
|
local parts = split_all(data, / /);
|
||||||
|
@ -45,11 +94,17 @@ event irc_server(c: connection, prefix: string, data: string) &priority=5
|
||||||
c$irc$command = "DCC SEND";
|
c$irc$command = "DCC SEND";
|
||||||
#local ex_h = count_to_v4_addr(to_count(parts[|parts|-4]));
|
#local ex_h = count_to_v4_addr(to_count(parts[|parts|-4]));
|
||||||
local ex_p = to_port(to_count(parts[|parts|-2]), tcp);
|
local ex_p = to_port(to_count(parts[|parts|-2]), tcp);
|
||||||
c$irc$file_name = parts[|parts|-6];
|
c$irc$dcc_file_name = parts[|parts|-6];
|
||||||
c$irc$file_size = to_count(parts[|parts|]);
|
c$irc$dcc_file_size = to_count(parts[|parts|]);
|
||||||
#print fmt("file! %s->%s:%d", c$id$orig_h, ex_h, ex_p);
|
#print fmt("file! %s->%s:%d", c$id$orig_h, ex_h, ex_p);
|
||||||
#expect_connection(c$id$orig_h, ex_h, ex_p, ANALYZER_FILE, 5 min);
|
#expect_connection(c$id$orig_h, ex_h, ex_p, ANALYZER_FILE, 5 min);
|
||||||
#dcc_expected_transfers[ex_h, ex_p];
|
#dcc_expected_transfers[ex_h, ex_p];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event expected_connection_seen(c: connection, a: count) &priority=10
|
||||||
|
{
|
||||||
|
local id = c$id;
|
||||||
|
if ( [id$resp_h, id$resp_p] in dcc_expected_transfers )
|
||||||
|
add c$service["irc-dcc-data"];
|
||||||
|
}
|
||||||
|
|
|
@ -21,13 +21,15 @@ export {
|
||||||
};
|
};
|
||||||
|
|
||||||
type Info: record {
|
type Info: record {
|
||||||
|
ts: time &log &optional;
|
||||||
|
id: conn_id &log &optional; # connection-ID, if we don't have a connection handy
|
||||||
|
|
||||||
note: Type &log;
|
note: Type &log;
|
||||||
msg: string &default="" &log;
|
msg: string &default="" &log;
|
||||||
sub: string &log &optional; # sub-message
|
sub: string &log &optional; # sub-message
|
||||||
|
|
||||||
conn: connection &log &optional; # connection associated with notice
|
conn: connection &log &optional; # connection associated with notice
|
||||||
iconn: icmp_conn &log &optional; # associated ICMP "connection"
|
iconn: icmp_conn &log &optional; # associated ICMP "connection"
|
||||||
id: conn_id &log &optional; # connection-ID, if we don't have a connection handy
|
|
||||||
src: addr &log &optional; # source address, if we don't have a connection
|
src: addr &log &optional; # source address, if we don't have a connection
|
||||||
dst: addr &log &optional; # destination address
|
dst: addr &log &optional; # destination address
|
||||||
p: port &log &optional; # associated port, if we don't have a conn.
|
p: port &log &optional; # associated port, if we don't have a conn.
|
||||||
|
@ -210,6 +212,8 @@ function execute_with_notice(cmd: string, n: Notice::Info)
|
||||||
function notice(n: Notice::Info)
|
function notice(n: Notice::Info)
|
||||||
{
|
{
|
||||||
# Fill in some defaults.
|
# Fill in some defaults.
|
||||||
|
n$ts = network_time();
|
||||||
|
|
||||||
if ( ! n?$id && n?$conn )
|
if ( ! n?$id && n?$conn )
|
||||||
n$id = n$conn$id;
|
n$id = n$conn$id;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
@load smtp/base
|
||||||
|
|
||||||
|
export {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
|
||||||
|
module SMTP;
|
||||||
|
|
||||||
function find_address_in_smtp_header(header: string): string
|
function find_address_in_smtp_header(header: string): string
|
||||||
{
|
{
|
||||||
local ips = find_ip_addresses(header);
|
local ips = find_ip_addresses(header);
|
||||||
|
|
|
@ -118,6 +118,7 @@ function parse_mozilla(unparsed_version: string,
|
||||||
if ( 2 in parts )
|
if ( 2 in parts )
|
||||||
v = parse(parts[2], host, software_type)$version;
|
v = parse(parts[2], host, software_type)$version;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$ts=network_time(), $host=host, $name=software_name, $version=v,
|
return [$ts=network_time(), $host=host, $name=software_name, $version=v,
|
||||||
$unparsed_version=unparsed_version];
|
$unparsed_version=unparsed_version];
|
||||||
}
|
}
|
||||||
|
|
276
policy/ssh.bro
276
policy/ssh.bro
|
@ -1,274 +1,2 @@
|
||||||
@load functions
|
@load ssh/base
|
||||||
@load notice
|
@load ssh/software
|
||||||
@load software
|
|
||||||
|
|
||||||
module SSH;
|
|
||||||
|
|
||||||
redef enum Notice::Type += {
|
|
||||||
SSH_Login,
|
|
||||||
SSH_PasswordGuessing,
|
|
||||||
SSH_LoginByPasswordGuesser,
|
|
||||||
SSH_Login_From_Interesting_Hostname,
|
|
||||||
SSH_Bytecount_Inconsistency,
|
|
||||||
};
|
|
||||||
|
|
||||||
redef enum Log::ID += { SSH };
|
|
||||||
|
|
||||||
redef enum Software::Type += {
|
|
||||||
SSH_SERVER,
|
|
||||||
SSH_CLIENT,
|
|
||||||
};
|
|
||||||
|
|
||||||
# Configure DPD and the packet filter
|
|
||||||
redef capture_filters += { ["ssh"] = "tcp port 22" };
|
|
||||||
redef dpd_config += { [ANALYZER_SSH] = [$ports = set(22/tcp)] };
|
|
||||||
|
|
||||||
export {
|
|
||||||
type Log: record {
|
|
||||||
ts: time;
|
|
||||||
id: conn_id;
|
|
||||||
status: string &default="";
|
|
||||||
direction: string &default="";
|
|
||||||
remote_location: geo_location &optional;
|
|
||||||
client: string &default="";
|
|
||||||
server: string &default="";
|
|
||||||
resp_size: count &default=0;
|
|
||||||
};
|
|
||||||
# This is the prototype for the event that the logging framework tries
|
|
||||||
# to generate if there is a handler for it.
|
|
||||||
global log: event(rec: Log);
|
|
||||||
|
|
||||||
const password_guesses_limit = 30 &redef;
|
|
||||||
|
|
||||||
# The size in bytes at which the SSH connection is presumed to be
|
|
||||||
# successful.
|
|
||||||
const authentication_data_size = 5500 &redef;
|
|
||||||
|
|
||||||
# The amount of time to remember presumed non-successful logins to build
|
|
||||||
# model of a password guesser.
|
|
||||||
const guessing_timeout = 30 mins &redef;
|
|
||||||
|
|
||||||
# If you want to lookup and log geoip data in the event of a failed login.
|
|
||||||
const log_geodata_on_failure = F &redef;
|
|
||||||
|
|
||||||
# The set of countries for which you'd like to throw notices upon successful login
|
|
||||||
# requires Bro compiled with libGeoIP support
|
|
||||||
const watched_countries: set[string] = {"RO"} &redef;
|
|
||||||
|
|
||||||
# Strange/bad host names to originate successful SSH logins
|
|
||||||
const interesting_hostnames =
|
|
||||||
/^d?ns[0-9]*\./ |
|
|
||||||
/^smtp[0-9]*\./ |
|
|
||||||
/^mail[0-9]*\./ |
|
|
||||||
/^pop[0-9]*\./ |
|
|
||||||
/^imap[0-9]*\./ |
|
|
||||||
/^www[0-9]*\./ |
|
|
||||||
/^ftp[0-9]*\./ &redef;
|
|
||||||
|
|
||||||
# This is a table with orig subnet as the key, and subnet as the value.
|
|
||||||
const ignore_guessers: table[subnet] of subnet &redef;
|
|
||||||
|
|
||||||
# If true, we tell the event engine to not look at further data
|
|
||||||
# packets after the initial SSH handshake. Helps with performance
|
|
||||||
# (especially with large file transfers) but precludes some
|
|
||||||
# kinds of analyses (e.g., tracking connection size).
|
|
||||||
const skip_processing_after_detection = F &redef;
|
|
||||||
|
|
||||||
# Keeps count of how many rejections a host has had
|
|
||||||
global password_rejections: table[addr] of TrackCount
|
|
||||||
&default=default_track_count
|
|
||||||
&write_expire=guessing_timeout;
|
|
||||||
|
|
||||||
# Keeps track of hosts identified as guessing passwords
|
|
||||||
# TODO: guessing_timeout doesn't work correctly here. If a user redefs
|
|
||||||
# the variable, it won't take effect.
|
|
||||||
global password_guessers: set[addr] &read_expire=guessing_timeout+1hr;
|
|
||||||
|
|
||||||
# The list of active SSH connections and the associated session info.
|
|
||||||
global active_conns: table[conn_id] of Log &read_expire=2mins;
|
|
||||||
|
|
||||||
global log_ssh: event(rec: Log);
|
|
||||||
}
|
|
||||||
|
|
||||||
function local_filter(rec: record { id: conn_id; } ): bool
|
|
||||||
{
|
|
||||||
return is_local_addr(rec$id$resp_h);
|
|
||||||
}
|
|
||||||
|
|
||||||
event bro_init()
|
|
||||||
{
|
|
||||||
# Create the stream.
|
|
||||||
# First argument is the ID for the stream.
|
|
||||||
# Second argument is the log record type.
|
|
||||||
Log::create_stream(SSH, [$columns=SSH::Log, $ev=log_ssh]);
|
|
||||||
# Add a default filter that simply logs everything to "ssh.log" using the default writer.
|
|
||||||
Log::add_default_filter(SSH);
|
|
||||||
}
|
|
||||||
|
|
||||||
event check_ssh_connection(c: connection, done: bool)
|
|
||||||
{
|
|
||||||
# If this is no longer a known SSH connection, just return.
|
|
||||||
if ( c$id !in active_conns )
|
|
||||||
return;
|
|
||||||
|
|
||||||
# If this is still a live connection and the byte count has not
|
|
||||||
# crossed the threshold, just return and let the resheduled check happen later.
|
|
||||||
if ( !done && c$resp$size < authentication_data_size )
|
|
||||||
return;
|
|
||||||
|
|
||||||
# Make sure the server has sent back more than 50 bytes to filter out
|
|
||||||
# hosts that are just port scanning. Nothing is ever logged if the server
|
|
||||||
# doesn't send back at least 50 bytes.
|
|
||||||
if ( c$resp$size < 50 )
|
|
||||||
return;
|
|
||||||
|
|
||||||
local ssh_log = active_conns[c$id];
|
|
||||||
local status = "failure";
|
|
||||||
local direction = is_local_addr(c$id$orig_h) ? "to" : "from";
|
|
||||||
local location: geo_location;
|
|
||||||
|
|
||||||
if ( done && c$resp$size < authentication_data_size )
|
|
||||||
{
|
|
||||||
# presumed failure
|
|
||||||
if ( log_geodata_on_failure )
|
|
||||||
location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h);
|
|
||||||
|
|
||||||
if ( c$id$orig_h !in password_rejections )
|
|
||||||
password_rejections[c$id$orig_h] = default_track_count(c$id$orig_h);
|
|
||||||
|
|
||||||
# Track the number of rejections
|
|
||||||
if ( !(c$id$orig_h in ignore_guessers &&
|
|
||||||
c$id$resp_h in ignore_guessers[c$id$orig_h]) )
|
|
||||||
++password_rejections[c$id$orig_h]$n;
|
|
||||||
|
|
||||||
if ( default_check_threshold(password_rejections[c$id$orig_h]) )
|
|
||||||
{
|
|
||||||
add password_guessers[c$id$orig_h];
|
|
||||||
NOTICE([$note=SSH_PasswordGuessing,
|
|
||||||
$conn=c,
|
|
||||||
$msg=fmt("SSH password guessing by %s", c$id$orig_h),
|
|
||||||
$sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n),
|
|
||||||
$n=password_rejections[c$id$orig_h]$n]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# TODO: This is to work around a quasi-bug in Bro which occasionally
|
|
||||||
# causes the byte count to be oversized.
|
|
||||||
# Watch for Gregors work that adds an actual counter of bytes transferred.
|
|
||||||
else if ( c$resp$size < 20000000 )
|
|
||||||
{
|
|
||||||
# presumed successful login
|
|
||||||
status = "success";
|
|
||||||
location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h);
|
|
||||||
|
|
||||||
if ( password_rejections[c$id$orig_h]$n > password_guesses_limit &&
|
|
||||||
c$id$orig_h !in password_guessers)
|
|
||||||
{
|
|
||||||
add password_guessers[c$id$orig_h];
|
|
||||||
NOTICE([$note=SSH_LoginByPasswordGuesser,
|
|
||||||
$conn=c,
|
|
||||||
$n=password_rejections[c$id$orig_h]$n,
|
|
||||||
$msg=fmt("Successful SSH login by password guesser %s", c$id$orig_h),
|
|
||||||
$sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
local message = fmt("SSH login %s %s \"%s\" \"%s\" %f %f %s (triggered with %d bytes)",
|
|
||||||
direction, location$country_code, location$region, location$city,
|
|
||||||
location$latitude, location$longitude,
|
|
||||||
id_string(c$id), c$resp$size);
|
|
||||||
# TODO: rewrite the message once a location variable can be put in notices
|
|
||||||
NOTICE([$note=SSH_Login,
|
|
||||||
$conn=c,
|
|
||||||
$msg=message,
|
|
||||||
$sub=location$country_code]);
|
|
||||||
|
|
||||||
# Check to see if this login came from an interesting hostname
|
|
||||||
when( local hostname = lookup_addr(c$id$orig_h) )
|
|
||||||
{
|
|
||||||
if ( interesting_hostnames in hostname )
|
|
||||||
{
|
|
||||||
NOTICE([$note=SSH_Login_From_Interesting_Hostname,
|
|
||||||
$conn=c,
|
|
||||||
$msg=fmt("Strange login from %s", hostname),
|
|
||||||
$sub=hostname]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ( c$resp$size >= 200000000 )
|
|
||||||
{
|
|
||||||
NOTICE([$note=SSH_Bytecount_Inconsistency,
|
|
||||||
$conn=c,
|
|
||||||
$msg="During byte counting in SSH analysis, an overly large value was seen.",
|
|
||||||
$sub=fmt("%d",c$resp$size)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
ssh_log$remote_location = location;
|
|
||||||
ssh_log$status = status;
|
|
||||||
ssh_log$direction = direction;
|
|
||||||
ssh_log$resp_size = c$resp$size;
|
|
||||||
|
|
||||||
Log::write(SSH, ssh_log);
|
|
||||||
|
|
||||||
delete active_conns[c$id];
|
|
||||||
# Stop watching this connection, we don't care about it anymore.
|
|
||||||
if ( skip_processing_after_detection )
|
|
||||||
{
|
|
||||||
skip_further_processing(c$id);
|
|
||||||
set_record_packets(c$id, F);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event connection_state_remove(c: connection)
|
|
||||||
{
|
|
||||||
event check_ssh_connection(c, T);
|
|
||||||
}
|
|
||||||
|
|
||||||
event ssh_watcher(c: connection)
|
|
||||||
{
|
|
||||||
local id = c$id;
|
|
||||||
# don't go any further if this connection is gone already!
|
|
||||||
if ( !connection_exists(id) )
|
|
||||||
{
|
|
||||||
delete active_conns[id];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event check_ssh_connection(c, F);
|
|
||||||
if ( c$id in active_conns )
|
|
||||||
schedule +15secs { ssh_watcher(c) };
|
|
||||||
}
|
|
||||||
|
|
||||||
event ssh_client_version(c: connection, version: string)
|
|
||||||
{
|
|
||||||
if ( c$id in active_conns )
|
|
||||||
active_conns[c$id]$client = version;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
active_conns[c$id] = [$ts=c$start_time, $id=c$id];
|
|
||||||
schedule +15secs { ssh_watcher(c) };
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get rid of the protocol information when passing to the software framework.
|
|
||||||
local cleaned_version = sub(version, /^SSH[0-9\.\-]+/, "");
|
|
||||||
local si = Software::parse(cleaned_version, c$id$orig_h, SSH_CLIENT);
|
|
||||||
Software::found(c, si);
|
|
||||||
}
|
|
||||||
|
|
||||||
event ssh_server_version(c: connection, version: string)
|
|
||||||
{
|
|
||||||
if ( c$id in active_conns )
|
|
||||||
active_conns[c$id]$server = version;
|
|
||||||
|
|
||||||
# Get rid of the protocol information when passing to the software framework.
|
|
||||||
local cleaned_version = sub(version, /SSH[0-9\.\-]{2,}/, "");
|
|
||||||
local si = Software::parse(cleaned_version, c$id$resp_h, SSH_SERVER);
|
|
||||||
Software::found(c, si);
|
|
||||||
}
|
|
||||||
|
|
||||||
event protocol_confirmation(c: connection, atype: count, aid: count)
|
|
||||||
{
|
|
||||||
if ( atype == ANALYZER_SSH )
|
|
||||||
{
|
|
||||||
active_conns[c$id] = [$ts=c$start_time, $id=c$id];
|
|
||||||
schedule +15secs { ssh_watcher(c) };
|
|
||||||
}
|
|
||||||
}
|
|
263
policy/ssh/base.bro
Normal file
263
policy/ssh/base.bro
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
@load functions
|
||||||
|
@load notice
|
||||||
|
|
||||||
|
module SSH;
|
||||||
|
|
||||||
|
redef enum Notice::Type += {
|
||||||
|
SSH_Login,
|
||||||
|
SSH_PasswordGuessing,
|
||||||
|
SSH_LoginByPasswordGuesser,
|
||||||
|
SSH_Login_From_Interesting_Hostname,
|
||||||
|
SSH_Bytecount_Inconsistency,
|
||||||
|
};
|
||||||
|
|
||||||
|
redef enum Log::ID += { SSH };
|
||||||
|
|
||||||
|
# Configure DPD and the packet filter
|
||||||
|
redef capture_filters += { ["ssh"] = "tcp port 22" };
|
||||||
|
redef dpd_config += { [ANALYZER_SSH] = [$ports = set(22/tcp)] };
|
||||||
|
|
||||||
|
export {
|
||||||
|
type Info: record {
|
||||||
|
ts: time &log;
|
||||||
|
id: conn_id &log;
|
||||||
|
status: string &log &optional;
|
||||||
|
direction: string &log &optional;
|
||||||
|
remote_location: geo_location &log &optional;
|
||||||
|
client: string &log &optional;
|
||||||
|
server: string &log &optional;
|
||||||
|
resp_size: count &log &default=0;
|
||||||
|
|
||||||
|
## Indicate if the SSH session is done being watched.
|
||||||
|
done: bool &default=F;
|
||||||
|
};
|
||||||
|
|
||||||
|
const password_guesses_limit = 30 &redef;
|
||||||
|
|
||||||
|
# The size in bytes at which the SSH connection is presumed to be
|
||||||
|
# successful.
|
||||||
|
const authentication_data_size = 5500 &redef;
|
||||||
|
|
||||||
|
# The amount of time to remember presumed non-successful logins to build
|
||||||
|
# model of a password guesser.
|
||||||
|
const guessing_timeout = 30 mins &redef;
|
||||||
|
|
||||||
|
# If you want to lookup and log geoip data in the event of a failed login.
|
||||||
|
const log_geodata_on_failure = F &redef;
|
||||||
|
|
||||||
|
# The set of countries for which you'd like to throw notices upon successful login
|
||||||
|
# requires Bro compiled with libGeoIP support
|
||||||
|
const watched_countries: set[string] = {"RO"} &redef;
|
||||||
|
|
||||||
|
# Strange/bad host names to originate successful SSH logins
|
||||||
|
const interesting_hostnames =
|
||||||
|
/^d?ns[0-9]*\./ |
|
||||||
|
/^smtp[0-9]*\./ |
|
||||||
|
/^mail[0-9]*\./ |
|
||||||
|
/^pop[0-9]*\./ |
|
||||||
|
/^imap[0-9]*\./ |
|
||||||
|
/^www[0-9]*\./ |
|
||||||
|
/^ftp[0-9]*\./ &redef;
|
||||||
|
|
||||||
|
# This is a table with orig subnet as the key, and subnet as the value.
|
||||||
|
const ignore_guessers: table[subnet] of subnet &redef;
|
||||||
|
|
||||||
|
# If true, we tell the event engine to not look at further data
|
||||||
|
# packets after the initial SSH handshake. Helps with performance
|
||||||
|
# (especially with large file transfers) but precludes some
|
||||||
|
# kinds of analyses (e.g., tracking connection size).
|
||||||
|
const skip_processing_after_detection = F &redef;
|
||||||
|
|
||||||
|
# Keeps count of how many rejections a host has had
|
||||||
|
global password_rejections: table[addr] of TrackCount
|
||||||
|
&default=default_track_count
|
||||||
|
&write_expire=guessing_timeout;
|
||||||
|
|
||||||
|
# Keeps track of hosts identified as guessing passwords
|
||||||
|
# TODO: guessing_timeout doesn't work correctly here. If a user redefs
|
||||||
|
# the variable, it won't take effect.
|
||||||
|
global password_guessers: set[addr] &read_expire=guessing_timeout+1hr;
|
||||||
|
|
||||||
|
global log_ssh: event(rec: Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: move this elsewhere
|
||||||
|
function local_filter(rec: record { id: conn_id; } ): bool
|
||||||
|
{
|
||||||
|
return is_local_addr(rec$id$resp_h);
|
||||||
|
}
|
||||||
|
|
||||||
|
redef record connection += {
|
||||||
|
ssh: Info &optional;
|
||||||
|
};
|
||||||
|
|
||||||
|
event bro_init()
|
||||||
|
{
|
||||||
|
Log::create_stream(SSH, [$columns=Info, $ev=log_ssh]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_session(c: connection)
|
||||||
|
{
|
||||||
|
if ( ! c?$ssh )
|
||||||
|
{
|
||||||
|
local info: Info;
|
||||||
|
info$ts=network_time();
|
||||||
|
info$id=c$id;
|
||||||
|
c$ssh = info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_ssh_connection(c: connection, done: bool)
|
||||||
|
{
|
||||||
|
# If done watching this connection, just return.
|
||||||
|
if ( c$ssh$done )
|
||||||
|
return;
|
||||||
|
|
||||||
|
# If this is still a live connection and the byte count has not
|
||||||
|
# crossed the threshold, just return and let the resheduled check happen later.
|
||||||
|
if ( !done && c$resp$size < authentication_data_size )
|
||||||
|
return;
|
||||||
|
|
||||||
|
# Make sure the server has sent back more than 50 bytes to filter out
|
||||||
|
# hosts that are just port scanning. Nothing is ever logged if the server
|
||||||
|
# doesn't send back at least 50 bytes.
|
||||||
|
if ( c$resp$size < 50 )
|
||||||
|
return;
|
||||||
|
|
||||||
|
local status = "failure";
|
||||||
|
local direction = is_local_addr(c$id$orig_h) ? "to" : "from";
|
||||||
|
local location: geo_location;
|
||||||
|
|
||||||
|
if ( done && c$resp$size < authentication_data_size )
|
||||||
|
{
|
||||||
|
# presumed failure
|
||||||
|
if ( log_geodata_on_failure )
|
||||||
|
location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h);
|
||||||
|
|
||||||
|
if ( c$id$orig_h !in password_rejections )
|
||||||
|
password_rejections[c$id$orig_h] = default_track_count(c$id$orig_h);
|
||||||
|
|
||||||
|
# Track the number of rejections
|
||||||
|
if ( !(c$id$orig_h in ignore_guessers &&
|
||||||
|
c$id$resp_h in ignore_guessers[c$id$orig_h]) )
|
||||||
|
++password_rejections[c$id$orig_h]$n;
|
||||||
|
|
||||||
|
if ( default_check_threshold(password_rejections[c$id$orig_h]) )
|
||||||
|
{
|
||||||
|
add password_guessers[c$id$orig_h];
|
||||||
|
NOTICE([$note=SSH_PasswordGuessing,
|
||||||
|
$conn=c,
|
||||||
|
$msg=fmt("SSH password guessing by %s", c$id$orig_h),
|
||||||
|
$sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n),
|
||||||
|
$n=password_rejections[c$id$orig_h]$n]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# TODO: This is to work around a quasi-bug in Bro which occasionally
|
||||||
|
# causes the byte count to be oversized.
|
||||||
|
# Watch for Gregors work that adds an actual counter of bytes transferred.
|
||||||
|
else if ( c$resp$size < 20000000 )
|
||||||
|
{
|
||||||
|
# presumed successful login
|
||||||
|
status = "success";
|
||||||
|
location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h);
|
||||||
|
|
||||||
|
if ( password_rejections[c$id$orig_h]$n > password_guesses_limit &&
|
||||||
|
c$id$orig_h !in password_guessers)
|
||||||
|
{
|
||||||
|
add password_guessers[c$id$orig_h];
|
||||||
|
NOTICE([$note=SSH_LoginByPasswordGuesser,
|
||||||
|
$conn=c,
|
||||||
|
$n=password_rejections[c$id$orig_h]$n,
|
||||||
|
$msg=fmt("Successful SSH login by password guesser %s", c$id$orig_h),
|
||||||
|
$sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
local message = fmt("SSH login %s %s \"%s\" \"%s\" %f %f %s (triggered with %d bytes)",
|
||||||
|
direction, location$country_code, location$region, location$city,
|
||||||
|
location$latitude, location$longitude,
|
||||||
|
id_string(c$id), c$resp$size);
|
||||||
|
# TODO: rewrite the message once a location variable can be put in notices
|
||||||
|
NOTICE([$note=SSH_Login,
|
||||||
|
$conn=c,
|
||||||
|
$msg=message,
|
||||||
|
$sub=location$country_code]);
|
||||||
|
|
||||||
|
# Check to see if this login came from an interesting hostname
|
||||||
|
when( local hostname = lookup_addr(c$id$orig_h) )
|
||||||
|
{
|
||||||
|
if ( interesting_hostnames in hostname )
|
||||||
|
{
|
||||||
|
NOTICE([$note=SSH_Login_From_Interesting_Hostname,
|
||||||
|
$conn=c,
|
||||||
|
$msg=fmt("Strange login from %s", hostname),
|
||||||
|
$sub=hostname]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ( c$resp$size >= 200000000 )
|
||||||
|
{
|
||||||
|
NOTICE([$note=SSH_Bytecount_Inconsistency,
|
||||||
|
$conn=c,
|
||||||
|
$msg="During byte counting in SSH analysis, an overly large value was seen.",
|
||||||
|
$sub=fmt("%d",c$resp$size)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
c$ssh$remote_location = location;
|
||||||
|
c$ssh$status = status;
|
||||||
|
c$ssh$direction = direction;
|
||||||
|
c$ssh$resp_size = c$resp$size;
|
||||||
|
|
||||||
|
Log::write(SSH, c$ssh);
|
||||||
|
|
||||||
|
# Set the "done" flag to prevent the watching event from rescheduling
|
||||||
|
# after detection is done.
|
||||||
|
c$ssh$done;
|
||||||
|
|
||||||
|
# Stop watching this connection, we don't care about it anymore.
|
||||||
|
if ( skip_processing_after_detection )
|
||||||
|
{
|
||||||
|
skip_further_processing(c$id);
|
||||||
|
set_record_packets(c$id, F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event connection_state_remove(c: connection)
|
||||||
|
{
|
||||||
|
if ( c?$ssh )
|
||||||
|
check_ssh_connection(c, T);
|
||||||
|
}
|
||||||
|
|
||||||
|
event ssh_watcher(c: connection)
|
||||||
|
{
|
||||||
|
local id = c$id;
|
||||||
|
# don't go any further if this connection is gone already!
|
||||||
|
if ( !connection_exists(id) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
check_ssh_connection(c, F);
|
||||||
|
if ( ! c$ssh$done )
|
||||||
|
schedule +15secs { ssh_watcher(c) };
|
||||||
|
}
|
||||||
|
|
||||||
|
event ssh_server_version(c: connection, version: string) &priority=5
|
||||||
|
{
|
||||||
|
set_session(c);
|
||||||
|
c$ssh$server = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
event ssh_client_version(c: connection, version: string) &priority=5
|
||||||
|
{
|
||||||
|
set_session(c);
|
||||||
|
c$ssh$client = version;
|
||||||
|
schedule +15secs { ssh_watcher(c) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#event protocol_confirmation(c: connection, atype: count, aid: count) &priority=5
|
||||||
|
# {
|
||||||
|
# if ( atype == ANALYZER_SSH )
|
||||||
|
# {
|
||||||
|
# if ( ! c?$ssh )
|
||||||
|
# schedule +15secs { ssh_watcher(c) };
|
||||||
|
# set_session(c);
|
||||||
|
# }
|
||||||
|
# }
|
25
policy/ssh/software.bro
Normal file
25
policy/ssh/software.bro
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
@load ssh/base
|
||||||
|
@load software
|
||||||
|
|
||||||
|
module SSH;
|
||||||
|
|
||||||
|
redef enum Software::Type += {
|
||||||
|
SSH_SERVER,
|
||||||
|
SSH_CLIENT,
|
||||||
|
};
|
||||||
|
|
||||||
|
event ssh_client_version(c: connection, version: string) &priority=4
|
||||||
|
{
|
||||||
|
# Get rid of the protocol information when passing to the software framework.
|
||||||
|
local cleaned_version = sub(version, /^SSH[0-9\.\-]+/, "");
|
||||||
|
local si = Software::parse(cleaned_version, c$id$orig_h, SSH_CLIENT);
|
||||||
|
Software::found(c$id, si);
|
||||||
|
}
|
||||||
|
|
||||||
|
event ssh_server_version(c: connection, version: string) &priority=4
|
||||||
|
{
|
||||||
|
# Get rid of the protocol information when passing to the software framework.
|
||||||
|
local cleaned_version = sub(version, /SSH[0-9\.\-]{2,}/, "");
|
||||||
|
local si = Software::parse(cleaned_version, c$id$resp_h, SSH_SERVER);
|
||||||
|
Software::found(c$id, si);
|
||||||
|
}
|
|
@ -465,7 +465,7 @@ const ssl_cipher_desc: table[count] of string = {
|
||||||
[SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA] = "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA",
|
[SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA] = "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA",
|
||||||
[SSL_RSA_FIPS_WITH_DES_CBC_SHA_2] = "SSL_RSA_FIPS_WITH_DES_CBC_SHA_2",
|
[SSL_RSA_FIPS_WITH_DES_CBC_SHA_2] = "SSL_RSA_FIPS_WITH_DES_CBC_SHA_2",
|
||||||
[SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA_2] = "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA_2",
|
[SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA_2] = "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA_2",
|
||||||
};
|
} &default="UNKNOWN";
|
||||||
|
|
||||||
|
|
||||||
# --- the following sets are provided for convenience
|
# --- the following sets are provided for convenience
|
||||||
|
|
147
policy/ssl.bro
147
policy/ssl.bro
|
@ -9,7 +9,7 @@
|
||||||
module SSL;
|
module SSL;
|
||||||
|
|
||||||
redef enum Notice::Type += {
|
redef enum Notice::Type += {
|
||||||
# Blanket X509 error
|
## Blanket X509 error
|
||||||
SSL_X509Violation,
|
SSL_X509Violation,
|
||||||
## Session data not consistent with connection
|
## Session data not consistent with connection
|
||||||
SSL_SessConIncon,
|
SSL_SessConIncon,
|
||||||
|
@ -18,33 +18,31 @@ redef enum Notice::Type += {
|
||||||
redef enum Log::ID += { SSL };
|
redef enum Log::ID += { SSL };
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type Log: record {
|
type Tags: enum {
|
||||||
ts: time;
|
WEAK_CLIENT_CIPHER,
|
||||||
id: conn_id;
|
WEAK_SERVER_CIPHER,
|
||||||
## This is the session ID. It's optional because SSLv2 doesn't have it.
|
WEAK_CIPHER_AGREED
|
||||||
sid: string &optional;
|
|
||||||
# TODO: dga 3/11 The following 2 fields are not yet picked up
|
|
||||||
#not_valid_before: time; # certificate valid time constraint
|
|
||||||
#not_valid_after: time; # certificate valid time constraint
|
|
||||||
version: string &default="UNKNOWN"; # version number
|
|
||||||
weak_client_cipher: bool &default = F; # true if client offered insecure ciphers
|
|
||||||
weak_server_cipher: bool &default = F; # true if server offered insecure ciphers
|
|
||||||
weak_cipher_agreed: bool &default = F; # true if insecure cipher agreed upon for use
|
|
||||||
|
|
||||||
version: string &default=""; # version associated with connection
|
|
||||||
client_cert: X509 &optional; # client certificate
|
|
||||||
server_cert: X509 &optional; # server certificate
|
|
||||||
handshake_cipher: string &default=""; # agreed-upon cipher for session/conn.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConnectionInfo: record {
|
type Info: record {
|
||||||
log: Log;
|
ts: time &log;
|
||||||
|
id: conn_id &log;
|
||||||
|
## This is the session ID. It's optional because SSLv2 doesn't have it.
|
||||||
|
sid: string &log &optional;
|
||||||
|
# TODO: dga 3/11 The following 2 fields are not yet picked up
|
||||||
|
#not_valid_before: time &log &optional; ##< certificate valid time constraint
|
||||||
|
#not_valid_after: time &log &optional; ##< certificate valid time constraint
|
||||||
|
version: string &log &default="UNKNOWN"; ##< SSL/TLS version number
|
||||||
|
|
||||||
|
client_cert: X509 &log &optional; ##< client certificate
|
||||||
|
server_cert: X509 &log &optional; ##< server certificate
|
||||||
|
handshake_cipher: string &log &optional; ##< agreed-upon cipher for session/conn.
|
||||||
|
tags: set[Tags] &log;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SessionInfo: record {
|
type SessionInfo: record {
|
||||||
## This tracks the number of times this session has been reused.
|
## This tracks the number of times this session has been used.
|
||||||
num_reuse: count &default=1;
|
num_use: count &default=1;
|
||||||
|
|
||||||
version: string &default=""; # version associated with connection
|
version: string &default=""; # version associated with connection
|
||||||
client_cert: X509 &optional; # client certificate
|
client_cert: X509 &optional; # client certificate
|
||||||
|
@ -89,13 +87,10 @@ export {
|
||||||
## The list of all detected X509 certs.
|
## The list of all detected X509 certs.
|
||||||
global certs: set[addr, port, string] &create_expire=1day &synchronized;
|
global certs: set[addr, port, string] &create_expire=1day &synchronized;
|
||||||
|
|
||||||
## All active SSL/TLS connections
|
|
||||||
global active_conns: table[conn_id] of ConnectionInfo &read_expire=1hr;
|
|
||||||
|
|
||||||
## Recent TLS session IDs
|
## Recent TLS session IDs
|
||||||
global recent_sessions: table[string] of SessionInfo &read_expire=1hr;
|
global recent_sessions: table[string] of SessionInfo &read_expire=1hr;
|
||||||
|
|
||||||
global log_ssl: event(rec: Log);
|
global log_ssl: event(rec: Info);
|
||||||
|
|
||||||
## This is the set of SSL/TLS ciphers are are seen as weak to attack.
|
## This is the set of SSL/TLS ciphers are are seen as weak to attack.
|
||||||
const weak_ciphers: set[count] = {
|
const weak_ciphers: set[count] = {
|
||||||
|
@ -140,6 +135,10 @@ export {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redef record connection += {
|
||||||
|
ssl: Info &optional;
|
||||||
|
};
|
||||||
|
|
||||||
# NOTE: this is a 'local' port format for your site
|
# NOTE: this is a 'local' port format for your site
|
||||||
# --- well-known ports for ssl ---------
|
# --- well-known ports for ssl ---------
|
||||||
redef capture_filters += {
|
redef capture_filters += {
|
||||||
|
@ -167,8 +166,7 @@ redef dpd_config += {
|
||||||
|
|
||||||
event bro_init()
|
event bro_init()
|
||||||
{
|
{
|
||||||
Log::create_stream(SSL, [$columns=SSL::Log, $ev=log_ssl] );
|
Log::create_stream(SSL, [$columns=Info, $ev=log_ssl] );
|
||||||
Log::add_default_filter(SSL);
|
|
||||||
|
|
||||||
# The event engine will generate a run-time if this fails for
|
# The event engine will generate a run-time if this fails for
|
||||||
# reasons other than that the directory already exists.
|
# reasons other than that the directory already exists.
|
||||||
|
@ -199,24 +197,16 @@ const x509_hot_errors: set[int] = {
|
||||||
};
|
};
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
function ssl_get_cipher_name(cipherSuite: count): string
|
function set_session(c: connection)
|
||||||
{
|
|
||||||
return cipherSuite in ssl_cipher_desc ?
|
|
||||||
ssl_cipher_desc[cipherSuite] : "UNKNOWN";
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_connection_info(c: connection): ConnectionInfo
|
|
||||||
{
|
{
|
||||||
local id = c$id;
|
local id = c$id;
|
||||||
|
|
||||||
if ( id in active_conns )
|
if ( ! c?$ssl )
|
||||||
return active_conns[id];
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
local log: Log = [$ts=network_time(), $id=id];
|
local info: Info;
|
||||||
local conn_info: ConnectionInfo = [$log=log];
|
info$ts=network_time();
|
||||||
active_conns[id] = conn_info;
|
info$id=id;
|
||||||
return conn_info;
|
c$ssl = info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,44 +221,43 @@ function get_session_info(s: SSL_sessionID): SessionInfo
|
||||||
|
|
||||||
event ssl_certificate(c: connection, cert: X509, is_server: bool)
|
event ssl_certificate(c: connection, cert: X509, is_server: bool)
|
||||||
{
|
{
|
||||||
#if ( is_server )
|
print "hello?";
|
||||||
# event protocol_confirmation(c, ANALYZER_SSL, 0);
|
set_session(c);
|
||||||
local conn = get_connection_info(c);
|
|
||||||
|
|
||||||
if ( [c$id$resp_h, c$id$resp_p, cert$subject] !in certs )
|
if ( [c$id$resp_h, c$id$resp_p, cert$subject] !in certs )
|
||||||
add certs[c$id$resp_h, c$id$resp_p, cert$subject];
|
add certs[c$id$resp_h, c$id$resp_p, cert$subject];
|
||||||
|
|
||||||
if( is_server )
|
if( is_server )
|
||||||
{
|
{
|
||||||
conn$log$server_cert = cert;
|
c$ssl$server_cert = cert;
|
||||||
|
|
||||||
# We have not filled in the field for the master session
|
# We have not filled in the field for the master session
|
||||||
# for this connection. Do it now, but only if this is not a
|
# for this connection. Do it now, but only if this is not a
|
||||||
# SSLv2 connection (no session information in that case).
|
# SSLv2 connection (no session information in that case).
|
||||||
if ( conn$log$sid in recent_sessions &&
|
if ( c$ssl$sid in recent_sessions &&
|
||||||
recent_sessions[conn$log$sid]?$server_cert )
|
recent_sessions[c$ssl$sid]?$server_cert )
|
||||||
recent_sessions[conn$log$sid]$server_cert$subject = cert$subject;
|
recent_sessions[c$ssl$sid]$server_cert$subject = cert$subject;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
conn$log$client_cert = cert;
|
c$ssl$client_cert = cert;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event ssl_conn_attempt(c: connection, version: count, ciphers: cipher_suites_list)
|
event ssl_conn_attempt(c: connection, version: count, ciphers: cipher_suites_list)
|
||||||
{
|
{
|
||||||
local conn = get_connection_info(c);
|
set_session(c);
|
||||||
|
|
||||||
conn$log$version = version_strings[version];
|
c$ssl$version = version_strings[version];
|
||||||
|
|
||||||
for ( cs in ciphers )
|
for ( cs in ciphers )
|
||||||
{
|
{
|
||||||
if ( cs in weak_ciphers )
|
if ( cs in weak_ciphers )
|
||||||
{
|
{
|
||||||
conn$log$weak_client_cipher = T;
|
add c$ssl$tags[WEAK_CLIENT_CIPHER];
|
||||||
#event ssl_conn_weak(
|
#event ssl_conn_weak(
|
||||||
# fmt("SSL client supports weak cipher: %s (0x%x)",
|
# fmt("SSL client supports weak cipher: %s (0x%x)",
|
||||||
# ssl_get_cipher_name(cs), cs), c);
|
# ssl_cipher_desc[cs], cs), c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,7 +265,7 @@ event ssl_conn_attempt(c: connection, version: count, ciphers: cipher_suites_lis
|
||||||
event ssl_conn_server_reply(c: connection, version: count,
|
event ssl_conn_server_reply(c: connection, version: count,
|
||||||
ciphers: cipher_suites_list)
|
ciphers: cipher_suites_list)
|
||||||
{
|
{
|
||||||
local conn = get_connection_info(c);
|
set_session(c);
|
||||||
|
|
||||||
#conn$log$version = version_strings[version];
|
#conn$log$version = version_strings[version];
|
||||||
|
|
||||||
|
@ -284,31 +273,27 @@ event ssl_conn_server_reply(c: connection, version: count,
|
||||||
{
|
{
|
||||||
if ( cs in weak_ciphers )
|
if ( cs in weak_ciphers )
|
||||||
{
|
{
|
||||||
conn$log$weak_server_cipher = T;
|
add c$ssl$tags[WEAK_SERVER_CIPHER];
|
||||||
#event ssl_conn_weak(
|
|
||||||
# fmt("SSLv2 server supports weak cipher: %s (0x%x)",
|
|
||||||
# ssl_get_cipher_name(cs), cs), c);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event ssl_conn_established(c: connection, version: count, cipher_suite: count) &priority=1
|
event ssl_conn_established(c: connection, version: count, cipher_suite: count) &priority=1
|
||||||
{
|
{
|
||||||
local conn = get_connection_info(c);
|
set_session(c);
|
||||||
|
|
||||||
conn$log$ts = network_time();
|
c$ssl$version = version_strings[version];
|
||||||
#conn$log$version = version_strings[version];
|
|
||||||
|
|
||||||
if ( cipher_suite in weak_ciphers )
|
if ( cipher_suite in weak_ciphers )
|
||||||
conn$log$weak_cipher_agreed = T;
|
add c$ssl$tags[WEAK_CIPHER_AGREED];
|
||||||
|
|
||||||
# log the connection
|
# log the connection
|
||||||
Log::write(SSL, conn$log);
|
Log::write(SSL, c$ssl);
|
||||||
}
|
}
|
||||||
|
|
||||||
event process_X509_extensions(c: connection, ex: X509_extension)
|
event process_X509_extensions(c: connection, ex: X509_extension)
|
||||||
{
|
{
|
||||||
local conn = get_connection_info(c);
|
set_session(c);
|
||||||
|
|
||||||
#local msg = fmt( "%.6f X.509 extensions: ", network_time() );
|
#local msg = fmt( "%.6f X.509 extensions: ", network_time() );
|
||||||
#for ( i in ex )
|
#for ( i in ex )
|
||||||
|
@ -317,33 +302,34 @@ event process_X509_extensions(c: connection, ex: X509_extension)
|
||||||
|
|
||||||
event ssl_session_insertion(c: connection, id: SSL_sessionID)
|
event ssl_session_insertion(c: connection, id: SSL_sessionID)
|
||||||
{
|
{
|
||||||
|
set_session(c);
|
||||||
|
|
||||||
local cid = c$id;
|
local cid = c$id;
|
||||||
local conn = get_connection_info(c);
|
c$ssl$sid=md5_hash(id);
|
||||||
conn$log$sid=md5_hash(id);
|
|
||||||
|
|
||||||
# This will create a new session if one doesn't already exist.
|
# This will create a new session if one doesn't already exist.
|
||||||
local session = get_session_info(id);
|
local session = get_session_info(id);
|
||||||
session$version=conn$log$version;
|
session$version=c$ssl$version;
|
||||||
if ( conn$log?$client_cert ) session$client_cert=conn$log$client_cert;
|
if ( c$ssl?$client_cert ) session$client_cert=c$ssl$client_cert;
|
||||||
if ( conn$log?$server_cert ) session$server_cert=conn$log$server_cert;
|
if ( c$ssl?$server_cert ) session$server_cert=c$ssl$server_cert;
|
||||||
session$handshake_cipher=conn$log$handshake_cipher;
|
if ( c$ssl?$handshake_cipher )session$handshake_cipher=c$ssl$handshake_cipher;
|
||||||
}
|
}
|
||||||
|
|
||||||
event ssl_conn_reused(c: connection, session_id: SSL_sessionID)
|
event ssl_conn_reused(c: connection, session_id: SSL_sessionID)
|
||||||
{
|
{
|
||||||
local conn = get_connection_info(c);
|
set_session(c);
|
||||||
|
|
||||||
# We cannot track sessions with SSLv2.
|
# We cannot track sessions with SSLv2.
|
||||||
if ( conn$log$version == version_strings[SSLv2] )
|
if ( c$ssl$version == version_strings[SSLv2] )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
local session = get_session_info(session_id);
|
local session = get_session_info(session_id);
|
||||||
++session$num_reuse;
|
++session$num_use;
|
||||||
|
|
||||||
# At this point, the connection values have been set. We can then
|
# At this point, the connection values have been set. We can then
|
||||||
# compare session and connection values with some confidence.
|
# compare session and connection values with some confidence.
|
||||||
if ( session$version != conn$log$version ||
|
if ( session$version != c$ssl$version ||
|
||||||
session$handshake_cipher != conn$log$handshake_cipher )
|
session$handshake_cipher != c$ssl$handshake_cipher )
|
||||||
{
|
{
|
||||||
NOTICE([$note=SSL_SessConIncon, $conn=c, $msg="session violation"]);
|
NOTICE([$note=SSL_SessConIncon, $conn=c, $msg="session violation"]);
|
||||||
}
|
}
|
||||||
|
@ -354,7 +340,8 @@ event ssl_X509_error(c: connection, err: int, err_string: string)
|
||||||
if ( err in x509_ignore_errors )
|
if ( err in x509_ignore_errors )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
local conn = get_connection_info(c);
|
set_session(c);
|
||||||
|
|
||||||
local error =
|
local error =
|
||||||
err in x509_errors ? x509_errors[err] : "unknown X.509 error";
|
err in x509_errors ? x509_errors[err] : "unknown X.509 error";
|
||||||
|
|
||||||
|
@ -362,13 +349,7 @@ event ssl_X509_error(c: connection, err: int, err_string: string)
|
||||||
if ( err in x509_hot_errors )
|
if ( err in x509_hot_errors )
|
||||||
{
|
{
|
||||||
NOTICE([$note=SSL_X509Violation, $conn=c, $msg=error]);
|
NOTICE([$note=SSL_X509Violation, $conn=c, $msg=error]);
|
||||||
++c$hot;
|
|
||||||
severity = "error";
|
severity = "error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event connection_state_remove(c: connection)
|
|
||||||
{
|
|
||||||
delete active_conns[c$id];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,16 @@ global matched_software: table[string] of Software::Info = {
|
||||||
[$name="Zope/(Zope", $version=[$major=2,$minor=7,$minor2=8,$addl="final"], $host=0.0.0.0, $ts=ts],
|
[$name="Zope/(Zope", $version=[$major=2,$minor=7,$minor2=8,$addl="final"], $host=0.0.0.0, $ts=ts],
|
||||||
["The Bat! (v2.00.9) Personal"] =
|
["The Bat! (v2.00.9) Personal"] =
|
||||||
[$name="The Bat!", $version=[$major=2,$minor=0,$minor2=9,$addl="Personal"], $host=0.0.0.0, $ts=ts],
|
[$name="The Bat!", $version=[$major=2,$minor=0,$minor2=9,$addl="Personal"], $host=0.0.0.0, $ts=ts],
|
||||||
|
["Flash/10,2,153,1"] =
|
||||||
|
[$name="Flash", $version=[$major=10,$minor=2,$minor2=153,$addl="1"], $host=0.0.0.0, $ts=ts],
|
||||||
|
|
||||||
|
["Apache/2.0.46 (Win32) mod_ssl/2.0.46 OpenSSL/0.9.7b mod_jk2/2.0.4"] =
|
||||||
|
[$name="Apache", $version=[$major=2,$minor=0,$minor2=46,$addl="Win32"], $host=0.0.0.0, $ts=ts],
|
||||||
|
|
||||||
|
["Apple iPhone v4.3.1 Weather v1.0.0.8G4"] =
|
||||||
|
[$name="Apple iPhone", $version=[$major=4,$minor=3,$minor2=1,$addl="Weather"], $host=0.0.0.0, $ts=ts],
|
||||||
|
["Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5"] =
|
||||||
|
[$name="Safari", $version=[$major=5,$minor=0,$minor2=2,$addl="Mobile"], $host=0.0.0.0, $ts=ts],
|
||||||
};
|
};
|
||||||
|
|
||||||
event bro_init()
|
event bro_init()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue