mirror of
https://github.com/zeek/zeek.git
synced 2025-10-12 03:28:19 +00:00
Merge remote-tracking branch 'origin/master' into topic/seth/analyzer-framework
Conflicts: scripts/base/init-default.bro scripts/base/protocols/dns/main.bro scripts/base/protocols/ftp/main.bro scripts/base/protocols/http/main.bro scripts/base/protocols/irc/main.bro scripts/base/protocols/smtp/main.bro scripts/base/protocols/ssh/main.bro scripts/base/protocols/ssl/main.bro scripts/base/protocols/syslog/main.bro src/main.cc testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log
This commit is contained in:
commit
5f8ee93ef0
1249 changed files with 267087 additions and 176962 deletions
|
@ -6,9 +6,9 @@ module Conn;
|
|||
export {
|
||||
## Define inactivity timeouts by the service detected being used over
|
||||
## the connection.
|
||||
const analyzer_inactivity_timeouts: table[AnalyzerTag] of interval = {
|
||||
const analyzer_inactivity_timeouts: table[Analyzer::Tag] of interval = {
|
||||
# For interactive services, allow longer periods of inactivity.
|
||||
[[ANALYZER_SSH, ANALYZER_FTP]] = 1 hrs,
|
||||
[[Analyzer::ANALYZER_SSH, Analyzer::ANALYZER_FTP]] = 1 hrs,
|
||||
} &redef;
|
||||
|
||||
## Define inactivity timeouts based on common protocol ports.
|
||||
|
@ -18,7 +18,7 @@ export {
|
|||
|
||||
}
|
||||
|
||||
event protocol_confirmation(c: connection, atype: count, aid: count)
|
||||
event protocol_confirmation(c: connection, atype: Analyzer::Tag, aid: count)
|
||||
{
|
||||
if ( atype in analyzer_inactivity_timeouts )
|
||||
set_inactivity_timeout(c$id, analyzer_inactivity_timeouts[atype]);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
##! Base DNS analysis script which tracks and logs DNS queries along with
|
||||
##! their responses.
|
||||
|
||||
@load base/frameworks/protocols
|
||||
@load base/utils/queue
|
||||
@load ./consts
|
||||
|
||||
module DNS;
|
||||
|
@ -68,25 +68,12 @@ export {
|
|||
ready: bool &default=F;
|
||||
## The total number of resource records in a reply message's answer
|
||||
## section.
|
||||
total_answers: count &default=0;
|
||||
total_answers: count &optional;
|
||||
## The total number of resource records in a reply message's answer,
|
||||
## authority, and additional sections.
|
||||
total_replies: count &optional;
|
||||
};
|
||||
|
||||
## A record type which tracks the status of DNS queries for a given
|
||||
## :bro:type:`connection`.
|
||||
type State: record {
|
||||
## Indexed by query id, returns Info record corresponding to
|
||||
## query/response which haven't completed yet.
|
||||
pending: table[count] of Info &optional;
|
||||
|
||||
## This is the list of DNS responses that have completed based on the
|
||||
## number of responses declared and the number received. The contents
|
||||
## of the set are transaction IDs.
|
||||
finished_answers: set[count] &optional;
|
||||
};
|
||||
|
||||
## An event that can be handled to access the :bro:type:`DNS::Info`
|
||||
## record as it is sent to the logging framework.
|
||||
global log_dns: event(rec: Info);
|
||||
|
@ -103,34 +90,57 @@ export {
|
|||
##
|
||||
## reply: The specific response information according to RR type/class.
|
||||
global do_reply: event(c: connection, msg: dns_msg, ans: dns_answer, reply: string);
|
||||
|
||||
## A hook that is called whenever a session is being set.
|
||||
## This can be used if additional initialization logic needs to happen
|
||||
## when creating a new session value.
|
||||
##
|
||||
## c: The connection involved in the new session
|
||||
##
|
||||
## msg: The DNS message header information.
|
||||
##
|
||||
## is_query: Indicator for if this is being called for a query or a response.
|
||||
global set_session: hook(c: connection, msg: dns_msg, is_query: bool);
|
||||
|
||||
## A record type which tracks the status of DNS queries for a given
|
||||
## :bro:type:`connection`.
|
||||
type State: record {
|
||||
## Indexed by query id, returns Info record corresponding to
|
||||
## query/response which haven't completed yet.
|
||||
pending: table[count] of Queue::Queue;
|
||||
|
||||
## This is the list of DNS responses that have completed based on the
|
||||
## number of responses declared and the number received. The contents
|
||||
## of the set are transaction IDs.
|
||||
finished_answers: set[count];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
redef record connection += {
|
||||
dns: Info &optional;
|
||||
dns_state: State &optional;
|
||||
};
|
||||
|
||||
# Not attaching ANALYZER_DNS_UDP_BINPAC and ANALYZER_DNS_TCP_BINPAC right now.
|
||||
global analyzers = { ANALYZER_DNS };
|
||||
redef Protocols::analyzer_map += { ["DNS"] = analyzers };
|
||||
global ports = { 53/udp, 53/tcp, 137/udp, 5353/udp, 5355/udp };
|
||||
redef Protocols::common_ports += { ["DNS"] = ports };
|
||||
# DPD configuration.
|
||||
redef capture_filters += {
|
||||
["dns"] = "port 53",
|
||||
["mdns"] = "udp and port 5353",
|
||||
["llmns"] = "udp and port 5355",
|
||||
["netbios-ns"] = "udp port 137",
|
||||
};
|
||||
|
||||
const ports = { 53/udp, 53/tcp, 137/udp, 5353/udp, 5355/udp };
|
||||
redef likely_server_ports += { ports };
|
||||
|
||||
event bro_init() &priority=5
|
||||
{
|
||||
Log::create_stream(DNS::LOG, [$columns=Info, $ev=log_dns]);
|
||||
Analyzer::register_for_ports(Analyzer::ANALYZER_DNS, ports);
|
||||
}
|
||||
|
||||
function new_session(c: connection, trans_id: count): Info
|
||||
{
|
||||
if ( ! c?$dns_state )
|
||||
{
|
||||
local state: State;
|
||||
state$pending=table();
|
||||
state$finished_answers=set();
|
||||
c$dns_state = state;
|
||||
}
|
||||
|
||||
local info: Info;
|
||||
info$ts = network_time();
|
||||
info$id = c$id;
|
||||
|
@ -140,18 +150,37 @@ function new_session(c: connection, trans_id: count): Info
|
|||
return info;
|
||||
}
|
||||
|
||||
function set_session(c: connection, msg: dns_msg, is_query: bool)
|
||||
hook set_session(c: connection, msg: dns_msg, is_query: bool) &priority=5
|
||||
{
|
||||
if ( ! c?$dns_state || msg$id !in c$dns_state$pending )
|
||||
if ( ! c?$dns_state )
|
||||
{
|
||||
c$dns_state$pending[msg$id] = new_session(c, msg$id);
|
||||
# Try deleting this transaction id from the set of finished answers.
|
||||
# Sometimes hosts will reuse ports and transaction ids and this should
|
||||
# be considered to be a legit scenario (although bad practice).
|
||||
delete c$dns_state$finished_answers[msg$id];
|
||||
local state: State;
|
||||
c$dns_state = state;
|
||||
}
|
||||
|
||||
c$dns = c$dns_state$pending[msg$id];
|
||||
if ( msg$id !in c$dns_state$pending )
|
||||
c$dns_state$pending[msg$id] = Queue::init();
|
||||
|
||||
local info: Info;
|
||||
# If this is either a query or this is the reply but
|
||||
# no Info records are in the queue (we missed the query?)
|
||||
# we need to create an Info record and put it in the queue.
|
||||
if ( is_query ||
|
||||
Queue::len(c$dns_state$pending[msg$id]) == 0 )
|
||||
{
|
||||
info = new_session(c, msg$id);
|
||||
Queue::put(c$dns_state$pending[msg$id], info);
|
||||
}
|
||||
|
||||
if ( is_query )
|
||||
# If this is a query, assign the newly created info variable
|
||||
# so that the world looks correct to anything else handling
|
||||
# this query.
|
||||
c$dns = info;
|
||||
else
|
||||
# Peek at the next item in the queue for this trans_id and
|
||||
# assign it to c$dns since this is a response.
|
||||
c$dns = Queue::peek(c$dns_state$pending[msg$id]);
|
||||
|
||||
if ( ! is_query )
|
||||
{
|
||||
|
@ -179,7 +208,7 @@ function set_session(c: connection, msg: dns_msg, is_query: bool)
|
|||
|
||||
event dns_message(c: connection, is_orig: bool, msg: dns_msg, len: count) &priority=5
|
||||
{
|
||||
set_session(c, msg, is_orig);
|
||||
hook set_session(c, msg, is_orig);
|
||||
}
|
||||
|
||||
event DNS::do_reply(c: connection, msg: dns_msg, ans: dns_answer, reply: string) &priority=5
|
||||
|
@ -189,9 +218,6 @@ event DNS::do_reply(c: connection, msg: dns_msg, ans: dns_answer, reply: string)
|
|||
c$dns$AA = msg$AA;
|
||||
c$dns$RA = msg$RA;
|
||||
|
||||
if ( msg$id in c$dns_state$finished_answers )
|
||||
event conn_weird("dns_reply_seen_after_done", c, "");
|
||||
|
||||
if ( reply != "" )
|
||||
{
|
||||
if ( ! c$dns?$answers )
|
||||
|
@ -206,7 +232,6 @@ event DNS::do_reply(c: connection, msg: dns_msg, ans: dns_answer, reply: string)
|
|||
if ( c$dns?$answers && c$dns?$total_answers &&
|
||||
|c$dns$answers| == c$dns$total_answers )
|
||||
{
|
||||
add c$dns_state$finished_answers[c$dns$trans_id];
|
||||
# Indicate this request/reply pair is ready to be logged.
|
||||
c$dns$ready = T;
|
||||
}
|
||||
|
@ -219,7 +244,8 @@ event DNS::do_reply(c: connection, msg: dns_msg, ans: dns_answer, reply: string)
|
|||
{
|
||||
Log::write(DNS::LOG, c$dns);
|
||||
# This record is logged and no longer pending.
|
||||
delete c$dns_state$pending[c$dns$trans_id];
|
||||
Queue::get(c$dns_state$pending[c$dns$trans_id]);
|
||||
delete c$dns;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,15 +257,14 @@ event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qcla
|
|||
c$dns$qclass_name = classes[qclass];
|
||||
c$dns$qtype = qtype;
|
||||
c$dns$qtype_name = query_types[qtype];
|
||||
c$dns$Z = msg$Z;
|
||||
|
||||
# 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$Z = msg$Z;
|
||||
c$dns$query = query;
|
||||
}
|
||||
|
||||
event dns_A_reply(c: connection, msg: dns_msg, ans: dns_answer, a: addr) &priority=5
|
||||
|
@ -327,6 +352,13 @@ event connection_state_remove(c: connection) &priority=-5
|
|||
# If Bro is expiring state, we should go ahead and log all unlogged
|
||||
# request/response pairs now.
|
||||
for ( trans_id in c$dns_state$pending )
|
||||
Log::write(DNS::LOG, c$dns_state$pending[trans_id]);
|
||||
{
|
||||
local infos: vector of Info;
|
||||
Queue::get_vector(c$dns_state$pending[trans_id], infos);
|
||||
for ( i in infos )
|
||||
{
|
||||
Log::write(DNS::LOG, infos[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@load ./utils-commands
|
||||
@load ./main
|
||||
@load ./file-analysis
|
||||
@load ./file-extract
|
||||
@load ./gridftp
|
||||
|
|
48
scripts/base/protocols/ftp/file-analysis.bro
Normal file
48
scripts/base/protocols/ftp/file-analysis.bro
Normal file
|
@ -0,0 +1,48 @@
|
|||
@load ./main
|
||||
@load base/utils/conn-ids
|
||||
@load base/frameworks/file-analysis/main
|
||||
|
||||
module FTP;
|
||||
|
||||
export {
|
||||
## Default file handle provider for FTP.
|
||||
global get_file_handle: function(c: connection, is_orig: bool): string;
|
||||
}
|
||||
|
||||
function get_handle_string(c: connection): string
|
||||
{
|
||||
return cat(Analyzer::ANALYZER_FTP_DATA, " ", c$start_time, " ", id_string(c$id));
|
||||
}
|
||||
|
||||
function get_file_handle(c: connection, is_orig: bool): string
|
||||
{
|
||||
if ( [c$id$resp_h, c$id$resp_p] !in ftp_data_expected ) return "";
|
||||
|
||||
local info: FTP::Info = ftp_data_expected[c$id$resp_h, c$id$resp_p];
|
||||
|
||||
if ( info$passive )
|
||||
# FTP client initiates data channel.
|
||||
if ( is_orig )
|
||||
# Don't care about FTP client data.
|
||||
return "";
|
||||
else
|
||||
# Do care about FTP server data.
|
||||
return get_handle_string(c);
|
||||
else
|
||||
# FTP server initiates dta channel.
|
||||
if ( is_orig )
|
||||
# Do care about FTP server data.
|
||||
return get_handle_string(c);
|
||||
else
|
||||
# Don't care about FTP client data.
|
||||
return "";
|
||||
}
|
||||
|
||||
module GLOBAL;
|
||||
|
||||
event get_file_handle(tag: Analyzer::Tag, c: connection, is_orig: bool)
|
||||
&priority=5
|
||||
{
|
||||
if ( tag != Analyzer::ANALYZER_FTP_DATA ) return;
|
||||
set_file_handle(FTP::get_file_handle(c, is_orig));
|
||||
}
|
|
@ -15,51 +15,71 @@ export {
|
|||
|
||||
redef record Info += {
|
||||
## On disk file where it was extracted to.
|
||||
extraction_file: file &log &optional;
|
||||
extraction_file: string &log &optional;
|
||||
|
||||
## Indicates if the current command/response pair should attempt to
|
||||
## extract the file if a file was transferred.
|
||||
extract_file: bool &default=F;
|
||||
|
||||
## Internal tracking of the total number of files extracted during this
|
||||
## session.
|
||||
num_extracted_files: count &default=0;
|
||||
};
|
||||
|
||||
event file_transferred(c: connection, prefix: string, descr: string,
|
||||
mime_type: string) &priority=3
|
||||
function get_extraction_name(f: fa_file): string
|
||||
{
|
||||
local id = c$id;
|
||||
if ( [id$resp_h, id$resp_p] !in ftp_data_expected )
|
||||
return;
|
||||
|
||||
local s = ftp_data_expected[id$resp_h, id$resp_p];
|
||||
local r = fmt("%s-%s.dat", extraction_prefix, f$id);
|
||||
return r;
|
||||
}
|
||||
|
||||
if ( extract_file_types in s$mime_type )
|
||||
event file_new(f: fa_file) &priority=5
|
||||
{
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "FTP_DATA" ) return;
|
||||
|
||||
if ( f?$mime_type && extract_file_types in f$mime_type )
|
||||
{
|
||||
s$extract_file = T;
|
||||
++s$num_extracted_files;
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_EXTRACT,
|
||||
$extract_filename=get_extraction_name(f)]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( [cid$resp_h, cid$resp_p] !in ftp_data_expected ) next;
|
||||
|
||||
local s = ftp_data_expected[cid$resp_h, cid$resp_p];
|
||||
|
||||
if ( ! s$extract_file ) next;
|
||||
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_EXTRACT,
|
||||
$extract_filename=get_extraction_name(f)]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
event file_transferred(c: connection, prefix: string, descr: string,
|
||||
mime_type: string) &priority=-4
|
||||
event file_state_remove(f: fa_file) &priority=4
|
||||
{
|
||||
local id = c$id;
|
||||
if ( [id$resp_h, id$resp_p] !in ftp_data_expected )
|
||||
return;
|
||||
|
||||
local s = ftp_data_expected[id$resp_h, id$resp_p];
|
||||
|
||||
if ( s$extract_file )
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "FTP_DATA" ) return;
|
||||
if ( ! f?$info ) return;
|
||||
|
||||
for ( filename in f$info$extracted_files )
|
||||
{
|
||||
local suffix = fmt("%d.dat", s$num_extracted_files);
|
||||
local fname = generate_extraction_filename(extraction_prefix, c, suffix);
|
||||
s$extraction_file = open(fname);
|
||||
if ( s$passive )
|
||||
set_contents_file(id, CONTENTS_RESP, s$extraction_file);
|
||||
else
|
||||
set_contents_file(id, CONTENTS_ORIG, s$extraction_file);
|
||||
local s: FTP::Info;
|
||||
s$ts = network_time();
|
||||
s$tags = set();
|
||||
s$user = "<ftp-data>";
|
||||
s$extraction_file = filename;
|
||||
|
||||
if ( f?$conns )
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
s$uid = f$conns[cid]$uid;
|
||||
s$id = cid;
|
||||
}
|
||||
|
||||
Log::write(FTP::LOG, s);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
##! The logging this script does is primarily focused on logging FTP commands
|
||||
##! along with metadata. For example, if files are transferred, the argument
|
||||
##! will take on the full path that the client is at along with the requested
|
||||
##! will take on the full path that the client is at along with the requested
|
||||
##! file name.
|
||||
|
||||
@load base/frameworks/protocols
|
||||
@load ./utils-commands
|
||||
@load base/utils/paths
|
||||
@load base/utils/numbers
|
||||
|
@ -15,18 +14,31 @@ module FTP;
|
|||
export {
|
||||
## The FTP protocol logging stream identifier.
|
||||
redef enum Log::ID += { LOG };
|
||||
|
||||
|
||||
## List of commands that should have their command/response pairs logged.
|
||||
const logged_commands = {
|
||||
"APPE", "DELE", "RETR", "STOR", "STOU", "ACCT"
|
||||
"APPE", "DELE", "RETR", "STOR", "STOU", "ACCT", "PORT", "PASV", "EPRT",
|
||||
"EPSV"
|
||||
} &redef;
|
||||
|
||||
|
||||
## This setting changes if passwords used in FTP sessions are captured or not.
|
||||
const default_capture_password = F &redef;
|
||||
|
||||
|
||||
## User IDs that can be considered "anonymous".
|
||||
const guest_ids = { "anonymous", "ftp", "ftpuser", "guest" } &redef;
|
||||
|
||||
|
||||
## The expected endpoints of an FTP data channel.
|
||||
type ExpectedDataChannel: record {
|
||||
## Whether PASV mode is toggled for control channel.
|
||||
passive: bool &log;
|
||||
## The host that will be initiating the data connection.
|
||||
orig_h: addr &log;
|
||||
## The host that will be accepting the data connection.
|
||||
resp_h: addr &log;
|
||||
## The port at which the acceptor is listening for the data connection.
|
||||
resp_p: port &log;
|
||||
};
|
||||
|
||||
type Info: record {
|
||||
## Time when the command was sent.
|
||||
ts: time &log;
|
||||
|
@ -42,51 +54,52 @@ export {
|
|||
command: string &log &optional;
|
||||
## Argument for the command if one is given.
|
||||
arg: string &log &optional;
|
||||
|
||||
|
||||
## Libmagic "sniffed" file type if the command indicates a file transfer.
|
||||
mime_type: string &log &optional;
|
||||
## Libmagic "sniffed" file description if the command indicates a file transfer.
|
||||
mime_desc: string &log &optional;
|
||||
## Size of the file if the command indicates a file transfer.
|
||||
file_size: count &log &optional;
|
||||
|
||||
|
||||
## Reply code from the server in response to the command.
|
||||
reply_code: count &log &optional;
|
||||
## Reply message from the server in response to the command.
|
||||
reply_msg: string &log &optional;
|
||||
## Arbitrary tags that may indicate a particular attribute of this command.
|
||||
tags: set[string] &log &default=set();
|
||||
|
||||
tags: set[string] &log;
|
||||
|
||||
## Expected FTP data channel.
|
||||
data_channel: ExpectedDataChannel &log &optional;
|
||||
|
||||
## Current working directory that this session is in. By making
|
||||
## the default value '/.', we can indicate that unless something
|
||||
## the default value '.', we can indicate that unless something
|
||||
## more concrete is discovered that the existing but unknown
|
||||
## directory is ok to use.
|
||||
cwd: string &default="/.";
|
||||
|
||||
cwd: string &default=".";
|
||||
|
||||
## Command that is currently waiting for a response.
|
||||
cmdarg: CmdArg &optional;
|
||||
## Queue for commands that have been sent but not yet responded to
|
||||
## Queue for commands that have been sent but not yet responded to
|
||||
## are tracked here.
|
||||
pending_commands: PendingCmds;
|
||||
|
||||
|
||||
## Indicates if the session is in active or passive mode.
|
||||
passive: bool &default=F;
|
||||
|
||||
|
||||
## Determines if the password will be captured for this request.
|
||||
capture_password: bool &default=default_capture_password;
|
||||
};
|
||||
|
||||
## This record is to hold a parsed FTP reply code. For example, for the
|
||||
## This record is to hold a parsed FTP reply code. For example, for the
|
||||
## 201 status code, the digits would be parsed as: x->2, y->0, z=>1.
|
||||
type ReplyCode: record {
|
||||
x: count;
|
||||
y: count;
|
||||
z: count;
|
||||
};
|
||||
|
||||
|
||||
## Parse FTP reply codes into the three constituent single digit values.
|
||||
global parse_ftp_reply_code: function(code: count): ReplyCode;
|
||||
|
||||
|
||||
## Event that can be handled to access the :bro:type:`FTP::Info`
|
||||
## record as it is sent on to the logging framework.
|
||||
global log_ftp: event(rec: Info);
|
||||
|
@ -95,19 +108,22 @@ export {
|
|||
# Add the state tracking information variable to the connection record
|
||||
redef record connection += {
|
||||
ftp: Info &optional;
|
||||
ftp_data_reuse: bool &default=F;
|
||||
};
|
||||
|
||||
global analyzers = { ANALYZER_FTP };
|
||||
redef Protocols::analyzer_map += { ["FTP"] = analyzers };
|
||||
global ports = { 21/tcp };
|
||||
redef Protocols::common_ports += { ["FTP"] = ports };
|
||||
# Configure DPD
|
||||
redef capture_filters += { ["ftp"] = "port 21 and port 2811" };
|
||||
|
||||
const ports = { 21/tcp, 2811/tcp };
|
||||
redef likely_server_ports += { ports };
|
||||
|
||||
# Establish the variable for tracking expected connections.
|
||||
global ftp_data_expected: table[addr, port] of Info &create_expire=5mins;
|
||||
global ftp_data_expected: table[addr, port] of Info &read_expire=5mins;
|
||||
|
||||
event bro_init() &priority=5
|
||||
{
|
||||
Log::create_stream(FTP::LOG, [$columns=Info, $ev=log_ftp]);
|
||||
Analyzer::register_for_ports(Analyzer::ANALYZER_FTP, ports);
|
||||
}
|
||||
|
||||
## A set of commands where the argument can be expected to refer
|
||||
|
@ -151,7 +167,7 @@ function set_ftp_session(c: connection)
|
|||
s$uid=c$uid;
|
||||
s$id=c$id;
|
||||
c$ftp=s;
|
||||
|
||||
|
||||
# Add a shim command so the server can respond with some init response.
|
||||
add_pending_cmd(c$ftp$pending_commands, "<init>", "");
|
||||
}
|
||||
|
@ -163,35 +179,51 @@ function ftp_message(s: Info)
|
|||
# or it's a deliberately logged command.
|
||||
if ( |s$tags| > 0 || (s?$cmdarg && s$cmdarg$cmd in logged_commands) )
|
||||
{
|
||||
if ( s?$password &&
|
||||
! s$capture_password &&
|
||||
if ( s?$password &&
|
||||
! s$capture_password &&
|
||||
to_lower(s$user) !in guest_ids )
|
||||
{
|
||||
s$password = "<hidden>";
|
||||
}
|
||||
|
||||
|
||||
local arg = s$cmdarg$arg;
|
||||
if ( s$cmdarg$cmd in file_cmds )
|
||||
arg = fmt("ftp://%s%s", addr_to_uri(s$id$resp_h), build_path_compressed(s$cwd, arg));
|
||||
|
||||
{
|
||||
local comp_path = build_path_compressed(s$cwd, arg);
|
||||
if ( comp_path[0] != "/" )
|
||||
comp_path = cat("/", comp_path);
|
||||
|
||||
arg = fmt("ftp://%s%s", addr_to_uri(s$id$resp_h), comp_path);
|
||||
}
|
||||
|
||||
s$ts=s$cmdarg$ts;
|
||||
s$command=s$cmdarg$cmd;
|
||||
if ( arg == "" )
|
||||
delete s$arg;
|
||||
else
|
||||
s$arg=arg;
|
||||
|
||||
|
||||
Log::write(FTP::LOG, s);
|
||||
}
|
||||
|
||||
# The MIME and file_size fields are specific to file transfer commands
|
||||
# and may not be used in all commands so they need reset to "blank"
|
||||
|
||||
# The MIME and file_size fields are specific to file transfer commands
|
||||
# and may not be used in all commands so they need reset to "blank"
|
||||
# values after logging.
|
||||
delete s$mime_type;
|
||||
delete s$mime_desc;
|
||||
delete s$file_size;
|
||||
# Same with data channel.
|
||||
delete s$data_channel;
|
||||
# Tags are cleared everytime too.
|
||||
delete s$tags;
|
||||
s$tags = set();
|
||||
}
|
||||
|
||||
function add_expected_data_channel(s: Info, chan: ExpectedDataChannel)
|
||||
{
|
||||
s$passive = chan$passive;
|
||||
s$data_channel = chan;
|
||||
ftp_data_expected[chan$resp_h, chan$resp_p] = s;
|
||||
Analyzer::schedule_analyzer(chan$orig_h, chan$resp_h, chan$resp_p, Analyzer::ANALYZER_FTP_DATA,
|
||||
5mins);
|
||||
}
|
||||
|
||||
event ftp_request(c: connection, command: string, arg: string) &priority=5
|
||||
|
@ -206,19 +238,19 @@ event ftp_request(c: connection, command: string, arg: string) &priority=5
|
|||
remove_pending_cmd(c$ftp$pending_commands, c$ftp$cmdarg);
|
||||
ftp_message(c$ftp);
|
||||
}
|
||||
|
||||
|
||||
local id = c$id;
|
||||
set_ftp_session(c);
|
||||
|
||||
|
||||
# Queue up the new command and argument
|
||||
add_pending_cmd(c$ftp$pending_commands, command, arg);
|
||||
|
||||
|
||||
if ( command == "USER" )
|
||||
c$ftp$user = arg;
|
||||
|
||||
|
||||
else if ( command == "PASS" )
|
||||
c$ftp$password = arg;
|
||||
|
||||
|
||||
else if ( command == "PORT" || command == "EPRT" )
|
||||
{
|
||||
local data = (command == "PORT") ?
|
||||
|
@ -226,9 +258,8 @@ event ftp_request(c: connection, command: string, arg: string) &priority=5
|
|||
|
||||
if ( data$valid )
|
||||
{
|
||||
c$ftp$passive=F;
|
||||
ftp_data_expected[data$h, data$p] = c$ftp;
|
||||
expect_connection(id$resp_h, data$h, data$p, ANALYZER_FILE, 5mins);
|
||||
add_expected_data_channel(c$ftp, [$passive=F, $orig_h=id$resp_h,
|
||||
$resp_h=data$h, $resp_p=data$p]);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -240,17 +271,14 @@ event ftp_request(c: connection, command: string, arg: string) &priority=5
|
|||
|
||||
event ftp_reply(c: connection, code: count, msg: string, cont_resp: bool) &priority=5
|
||||
{
|
||||
# TODO: figure out what to do with continued FTP response (not used much)
|
||||
#if ( cont_resp ) return;
|
||||
|
||||
local id = c$id;
|
||||
set_ftp_session(c);
|
||||
|
||||
c$ftp$cmdarg = get_pending_cmd(c$ftp$pending_commands, code, msg);
|
||||
|
||||
c$ftp$reply_code = code;
|
||||
c$ftp$reply_msg = msg;
|
||||
|
||||
|
||||
# TODO: figure out what to do with continued FTP response (not used much)
|
||||
if ( cont_resp ) return;
|
||||
|
||||
# TODO: do some sort of generic clear text login processing here.
|
||||
local response_xyz = parse_ftp_reply_code(code);
|
||||
#if ( response_xyz$x == 2 && # successful
|
||||
|
@ -266,22 +294,22 @@ event ftp_reply(c: connection, code: count, msg: string, cont_resp: bool) &prior
|
|||
# if that's given as well which would be more correct.
|
||||
c$ftp$file_size = extract_count(msg);
|
||||
}
|
||||
|
||||
|
||||
# PASV and EPSV processing
|
||||
else if ( (code == 227 || code == 229) &&
|
||||
(c$ftp$cmdarg$cmd == "PASV" || c$ftp$cmdarg$cmd == "EPSV") )
|
||||
{
|
||||
local data = (code == 227) ? parse_ftp_pasv(msg) : parse_ftp_epsv(msg);
|
||||
|
||||
|
||||
if ( data$valid )
|
||||
{
|
||||
c$ftp$passive=T;
|
||||
|
||||
|
||||
if ( code == 229 && data$h == [::] )
|
||||
data$h = id$resp_h;
|
||||
|
||||
ftp_data_expected[data$h, data$p] = c$ftp;
|
||||
expect_connection(id$orig_h, data$h, data$p, ANALYZER_FILE, 5mins);
|
||||
data$h = c$id$resp_h;
|
||||
|
||||
add_expected_data_channel(c$ftp, [$passive=T, $orig_h=c$id$orig_h,
|
||||
$resp_h=data$h, $resp_p=data$p]);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -300,9 +328,9 @@ event ftp_reply(c: connection, code: count, msg: string, cont_resp: bool) &prior
|
|||
else if ( c$ftp$cmdarg$cmd == "PWD" || c$ftp$cmdarg$cmd == "XPWD" )
|
||||
c$ftp$cwd = extract_path(msg);
|
||||
}
|
||||
|
||||
|
||||
# In case there are multiple commands queued, go ahead and remove the
|
||||
# command here and log because we can't do the normal processing pipeline
|
||||
# command here and log because we can't do the normal processing pipeline
|
||||
# to wait for a new command before logging the command/response pair.
|
||||
if ( |c$ftp$pending_commands| > 1 )
|
||||
{
|
||||
|
@ -311,8 +339,7 @@ event ftp_reply(c: connection, code: count, msg: string, cont_resp: bool) &prior
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
event expected_connection_seen(c: connection, a: count) &priority=10
|
||||
event scheduled_analyzer_applied(c: connection, a: Analyzer::Tag) &priority=10
|
||||
{
|
||||
local id = c$id;
|
||||
if ( [id$resp_h, id$resp_p] in ftp_data_expected )
|
||||
|
@ -327,18 +354,21 @@ event file_transferred(c: connection, prefix: string, descr: string,
|
|||
{
|
||||
local s = ftp_data_expected[id$resp_h, id$resp_p];
|
||||
s$mime_type = split1(mime_type, /;/)[1];
|
||||
s$mime_desc = descr;
|
||||
}
|
||||
}
|
||||
|
||||
event file_transferred(c: connection, prefix: string, descr: string,
|
||||
mime_type: string) &priority=-5
|
||||
|
||||
event connection_reused(c: connection) &priority=5
|
||||
{
|
||||
local id = c$id;
|
||||
if ( [id$resp_h, id$resp_p] in ftp_data_expected )
|
||||
delete ftp_data_expected[id$resp_h, id$resp_p];
|
||||
if ( "ftp-data" in c$service )
|
||||
c$ftp_data_reuse = T;
|
||||
}
|
||||
|
||||
|
||||
event connection_state_remove(c: connection) &priority=-5
|
||||
{
|
||||
if ( c$ftp_data_reuse ) return;
|
||||
delete ftp_data_expected[c$id$resp_h, c$id$resp_p];
|
||||
}
|
||||
|
||||
# Use state remove event to cover connections terminated by RST.
|
||||
event connection_state_remove(c: connection) &priority=-5
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@load ./main
|
||||
@load ./utils
|
||||
@load ./file-analysis
|
||||
@load ./file-ident
|
||||
@load ./file-hash
|
||||
@load ./file-extract
|
||||
|
|
54
scripts/base/protocols/http/file-analysis.bro
Normal file
54
scripts/base/protocols/http/file-analysis.bro
Normal file
|
@ -0,0 +1,54 @@
|
|||
@load ./main
|
||||
@load ./utils
|
||||
@load base/utils/conn-ids
|
||||
@load base/frameworks/file-analysis/main
|
||||
|
||||
module HTTP;
|
||||
|
||||
export {
|
||||
redef record HTTP::Info += {
|
||||
## Number of MIME entities in the HTTP request message body so far.
|
||||
request_mime_level: count &default=0;
|
||||
## Number of MIME entities in the HTTP response message body so far.
|
||||
response_mime_level: count &default=0;
|
||||
};
|
||||
|
||||
## Default file handle provider for HTTP.
|
||||
global get_file_handle: function(c: connection, is_orig: bool): string;
|
||||
}
|
||||
|
||||
event http_begin_entity(c: connection, is_orig: bool) &priority=5
|
||||
{
|
||||
if ( ! c?$http )
|
||||
return;
|
||||
|
||||
if ( is_orig )
|
||||
++c$http$request_mime_level;
|
||||
else
|
||||
++c$http$response_mime_level;
|
||||
}
|
||||
|
||||
function get_file_handle(c: connection, is_orig: bool): string
|
||||
{
|
||||
if ( ! c?$http ) return "";
|
||||
|
||||
local mime_level: count =
|
||||
is_orig ? c$http$request_mime_level : c$http$response_mime_level;
|
||||
local mime_level_str: string = mime_level > 1 ? cat(mime_level) : "";
|
||||
|
||||
if ( c$http$range_request )
|
||||
return cat(Analyzer::ANALYZER_HTTP, " ", is_orig, " ", c$id$orig_h, " ",
|
||||
build_url(c$http));
|
||||
|
||||
return cat(Analyzer::ANALYZER_HTTP, " ", c$start_time, " ", is_orig, " ",
|
||||
c$http$trans_depth, mime_level_str, " ", id_string(c$id));
|
||||
}
|
||||
|
||||
module GLOBAL;
|
||||
|
||||
event get_file_handle(tag: Analyzer::Tag, c: connection, is_orig: bool)
|
||||
&priority=5
|
||||
{
|
||||
if ( tag != Analyzer::ANALYZER_HTTP ) return;
|
||||
set_file_handle(HTTP::get_file_handle(c, is_orig));
|
||||
}
|
|
@ -2,8 +2,7 @@
|
|||
##! the message body from the server can be extracted with this script.
|
||||
|
||||
@load ./main
|
||||
@load ./file-ident
|
||||
@load base/utils/files
|
||||
@load ./file-analysis
|
||||
|
||||
module HTTP;
|
||||
|
||||
|
@ -15,46 +14,87 @@ export {
|
|||
const extraction_prefix = "http-item" &redef;
|
||||
|
||||
redef record Info += {
|
||||
## On-disk file where the response body was extracted to.
|
||||
extraction_file: file &log &optional;
|
||||
## On-disk location where files in request body were extracted.
|
||||
extracted_request_files: vector of string &log &optional;
|
||||
|
||||
## On-disk location where files in response body were extracted.
|
||||
extracted_response_files: vector of string &log &optional;
|
||||
|
||||
## Indicates if the response body is to be extracted or not. Must be
|
||||
## set before or by the first :bro:id:`http_entity_data` event for the
|
||||
## content.
|
||||
## set before or by the first :bro:see:`file_new` for the file content.
|
||||
extract_file: bool &default=F;
|
||||
};
|
||||
}
|
||||
|
||||
event http_entity_data(c: connection, is_orig: bool, length: count, data: string) &priority=-5
|
||||
function get_extraction_name(f: fa_file): string
|
||||
{
|
||||
local r = fmt("%s-%s.dat", extraction_prefix, f$id);
|
||||
return r;
|
||||
}
|
||||
|
||||
function add_extraction_file(c: connection, is_orig: bool, fn: string)
|
||||
{
|
||||
# Client body extraction is not currently supported in this script.
|
||||
if ( is_orig )
|
||||
return;
|
||||
|
||||
if ( c$http$first_chunk )
|
||||
{
|
||||
if ( c$http?$mime_type &&
|
||||
extract_file_types in c$http$mime_type )
|
||||
if ( ! c$http?$extracted_request_files )
|
||||
c$http$extracted_request_files = vector();
|
||||
c$http$extracted_request_files[|c$http$extracted_request_files|] = fn;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( ! c$http?$extracted_response_files )
|
||||
c$http$extracted_response_files = vector();
|
||||
c$http$extracted_response_files[|c$http$extracted_response_files|] = fn;
|
||||
}
|
||||
}
|
||||
|
||||
event file_new(f: fa_file) &priority=5
|
||||
{
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "HTTP" ) return;
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
local fname: string;
|
||||
local c: connection;
|
||||
|
||||
if ( f?$mime_type && extract_file_types in f$mime_type )
|
||||
{
|
||||
fname = get_extraction_name(f);
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_EXTRACT,
|
||||
$extract_filename=fname]);
|
||||
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
c$http$extract_file = T;
|
||||
}
|
||||
|
||||
if ( c$http$extract_file )
|
||||
{
|
||||
local suffix = fmt("%s_%d.dat", is_orig ? "orig" : "resp", c$http_state$current_response);
|
||||
local fname = generate_extraction_filename(extraction_prefix, c, suffix);
|
||||
|
||||
c$http$extraction_file = open(fname);
|
||||
enable_raw_output(c$http$extraction_file);
|
||||
c = f$conns[cid];
|
||||
if ( ! c?$http ) next;
|
||||
add_extraction_file(c, f$is_orig, fname);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( c$http?$extraction_file )
|
||||
print c$http$extraction_file, data;
|
||||
}
|
||||
local extracting: bool = F;
|
||||
|
||||
event http_end_entity(c: connection, is_orig: bool)
|
||||
{
|
||||
if ( c$http?$extraction_file )
|
||||
close(c$http$extraction_file);
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
c = f$conns[cid];
|
||||
|
||||
if ( ! c?$http ) next;
|
||||
|
||||
if ( ! c$http$extract_file ) next;
|
||||
|
||||
fname = get_extraction_name(f);
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_EXTRACT,
|
||||
$extract_filename=fname]);
|
||||
extracting = T;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( extracting )
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
c = f$conns[cid];
|
||||
if ( ! c?$http ) next;
|
||||
add_extraction_file(c, f$is_orig, fname);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,20 @@
|
|||
##! Calculate hashes for HTTP body transfers.
|
||||
|
||||
@load ./file-ident
|
||||
@load ./main
|
||||
@load ./file-analysis
|
||||
|
||||
module HTTP;
|
||||
|
||||
export {
|
||||
redef enum Notice::Type += {
|
||||
## Indicates that an MD5 sum was calculated for an HTTP response body.
|
||||
MD5,
|
||||
};
|
||||
|
||||
redef record Info += {
|
||||
## MD5 sum for a file transferred over HTTP calculated from the
|
||||
## response body.
|
||||
md5: string &log &optional;
|
||||
md5: string &log &optional;
|
||||
|
||||
## This value can be set per-transfer to determine per request
|
||||
## if a file should have an MD5 sum generated. It must be
|
||||
## set to T at the time of or before the first chunk of body data.
|
||||
calc_md5: bool &default=F;
|
||||
|
||||
## Indicates if an MD5 sum is being calculated for the current
|
||||
## request/response pair.
|
||||
calculating_md5: bool &default=F;
|
||||
calc_md5: bool &default=F;
|
||||
};
|
||||
|
||||
## Generate MD5 sums for these filetypes.
|
||||
|
@ -31,62 +23,46 @@ export {
|
|||
&redef;
|
||||
}
|
||||
|
||||
## Initialize and calculate the hash.
|
||||
event http_entity_data(c: connection, is_orig: bool, length: count, data: string) &priority=5
|
||||
event file_new(f: fa_file) &priority=5
|
||||
{
|
||||
if ( is_orig || ! c?$http ) return;
|
||||
|
||||
if ( c$http$first_chunk )
|
||||
{
|
||||
if ( c$http$calc_md5 ||
|
||||
(c$http?$mime_type && generate_md5 in c$http$mime_type) )
|
||||
{
|
||||
c$http$calculating_md5 = T;
|
||||
md5_hash_init(c$id);
|
||||
}
|
||||
}
|
||||
|
||||
if ( c$http$calculating_md5 )
|
||||
md5_hash_update(c$id, data);
|
||||
}
|
||||
|
||||
## In the event of a content gap during a file transfer, detect the state for
|
||||
## the MD5 sum calculation and stop calculating the MD5 since it would be
|
||||
## incorrect anyway.
|
||||
event content_gap(c: connection, is_orig: bool, seq: count, length: count) &priority=5
|
||||
{
|
||||
if ( is_orig || ! c?$http || ! c$http$calculating_md5 ) return;
|
||||
|
||||
set_state(c, F, is_orig);
|
||||
c$http$calculating_md5 = F;
|
||||
md5_hash_finish(c$id);
|
||||
}
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "HTTP" ) return;
|
||||
|
||||
## When the file finishes downloading, finish the hash and generate a notice.
|
||||
event http_message_done(c: connection, is_orig: bool, stat: http_message_stat) &priority=-3
|
||||
{
|
||||
if ( is_orig || ! c?$http ) return;
|
||||
|
||||
if ( c$http$calculating_md5 )
|
||||
if ( f?$mime_type && generate_md5 in f$mime_type )
|
||||
{
|
||||
local url = build_url_http(c$http);
|
||||
c$http$calculating_md5 = F;
|
||||
c$http$md5 = md5_hash_finish(c$id);
|
||||
|
||||
NOTICE([$note=MD5, $msg=fmt("%s %s %s", c$id$orig_h, c$http$md5, url),
|
||||
$sub=c$http$md5, $conn=c, $URL=url]);
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_MD5]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( ! c?$http ) next;
|
||||
|
||||
if ( ! c$http$calc_md5 ) next;
|
||||
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_MD5]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
event connection_state_remove(c: connection) &priority=-5
|
||||
event file_state_remove(f: fa_file) &priority=4
|
||||
{
|
||||
if ( c?$http_state &&
|
||||
c$http_state$current_response in c$http_state$pending &&
|
||||
c$http_state$pending[c$http_state$current_response]$calculating_md5 )
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "HTTP" ) return;
|
||||
if ( ! f?$conns ) return;
|
||||
if ( ! f?$info ) return;
|
||||
if ( ! f$info?$md5 ) return;
|
||||
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
# The MD5 sum isn't going to be saved anywhere since the entire
|
||||
# body wouldn't have been seen anyway and we'd just be giving an
|
||||
# incorrect MD5 sum.
|
||||
md5_hash_finish(c$id);
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( ! c?$http ) next;
|
||||
|
||||
c$http$md5 = f$info$md5;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +1,28 @@
|
|||
##! Identification of file types in HTTP response bodies with file content sniffing.
|
||||
|
||||
@load base/frameworks/signatures
|
||||
@load base/frameworks/notice
|
||||
@load ./main
|
||||
@load ./utils
|
||||
|
||||
# Add the magic number signatures to the core signature set.
|
||||
@load-sigs ./file-ident.sig
|
||||
|
||||
# Ignore the signatures used to match files
|
||||
redef Signatures::ignored_ids += /^matchfile-/;
|
||||
@load ./file-analysis
|
||||
|
||||
module HTTP;
|
||||
|
||||
export {
|
||||
redef enum Notice::Type += {
|
||||
## Indicates when the file extension doesn't seem to match the file contents.
|
||||
## Indicates when the file extension doesn't seem to match the file
|
||||
## contents.
|
||||
Incorrect_File_Type,
|
||||
};
|
||||
|
||||
redef record Info += {
|
||||
## Mime type of response body identified by content sniffing.
|
||||
mime_type: string &log &optional;
|
||||
|
||||
## Indicates that no data of the current file transfer has been
|
||||
## seen yet. After the first :bro:id:`http_entity_data` event, it
|
||||
## will be set to F.
|
||||
first_chunk: bool &default=T;
|
||||
};
|
||||
|
||||
## Mapping between mime types and regular expressions for URLs
|
||||
## The :bro:enum:`HTTP::Incorrect_File_Type` notice is generated if the pattern
|
||||
## doesn't match the mime type that was discovered.
|
||||
## Mapping between mime type strings (without character set) and
|
||||
## regular expressions for URLs.
|
||||
## The :bro:enum:`HTTP::Incorrect_File_Type` notice is generated if the
|
||||
## pattern doesn't match the mime type that was discovered.
|
||||
const mime_types_extensions: table[string] of pattern = {
|
||||
["application/x-dosexec"] = /\.([eE][xX][eE]|[dD][lL][lL])/,
|
||||
} &redef;
|
||||
|
@ -43,45 +34,72 @@ export {
|
|||
const ignored_incorrect_file_type_urls = /^$/ &redef;
|
||||
}
|
||||
|
||||
event signature_match(state: signature_state, msg: string, data: string) &priority=5
|
||||
event file_new(f: fa_file) &priority=5
|
||||
{
|
||||
# Only signatures matching file types are dealt with here.
|
||||
if ( /^matchfile-/ !in state$sig_id ) return;
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "HTTP" ) return;
|
||||
if ( ! f?$mime_type ) return;
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
local c = state$conn;
|
||||
set_state(c, F, F);
|
||||
|
||||
# Not much point in any of this if we don't know about the HTTP session.
|
||||
if ( ! c?$http ) return;
|
||||
|
||||
# Set the mime type that was detected.
|
||||
c$http$mime_type = msg;
|
||||
|
||||
if ( msg in mime_types_extensions &&
|
||||
c$http?$uri && mime_types_extensions[msg] !in c$http$uri )
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( ! c?$http ) next;
|
||||
|
||||
c$http$mime_type = f$mime_type;
|
||||
|
||||
local mime_str: string = c$http$mime_type;
|
||||
|
||||
if ( mime_str !in mime_types_extensions ) next;
|
||||
if ( ! c$http?$uri ) next;
|
||||
if ( mime_types_extensions[mime_str] in c$http$uri ) next;
|
||||
|
||||
local url = build_url_http(c$http);
|
||||
|
||||
if ( url == ignored_incorrect_file_type_urls )
|
||||
return;
|
||||
|
||||
local message = fmt("%s %s %s", msg, c$http$method, url);
|
||||
|
||||
if ( url == ignored_incorrect_file_type_urls ) next;
|
||||
|
||||
local message = fmt("%s %s %s", mime_str, c$http$method, url);
|
||||
NOTICE([$note=Incorrect_File_Type,
|
||||
$msg=message,
|
||||
$conn=c,
|
||||
$method=c$http$method,
|
||||
$URL=url]);
|
||||
$conn=c]);
|
||||
}
|
||||
}
|
||||
|
||||
event http_entity_data(c: connection, is_orig: bool, length: count, data: string) &priority=5
|
||||
event file_over_new_connection(f: fa_file, c: connection) &priority=5
|
||||
{
|
||||
if ( c$http$first_chunk && ! c$http?$mime_type )
|
||||
c$http$mime_type = split1(identify_data(data, T), /;/)[1];
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "HTTP" ) return;
|
||||
if ( ! f?$mime_type ) return;
|
||||
if ( ! c?$http ) return;
|
||||
|
||||
# Spread the mime around (e.g. for partial content, file_type event only
|
||||
# happens once for the first connection, but if there's subsequent
|
||||
# connections to transfer the same file, they'll be lacking the mime_type
|
||||
# field if we don't do this).
|
||||
c$http$mime_type = f$mime_type;
|
||||
}
|
||||
|
||||
event http_entity_data(c: connection, is_orig: bool, length: count, data: string) &priority=-10
|
||||
|
||||
# Tracks byte-range request / partial content response mime types, indexed
|
||||
# by [connection, uri] pairs. This is needed because a person can pipeline
|
||||
# byte-range requests over multiple connections to the same uri. Without
|
||||
# the tracking, only the first request in the pipeline for each connection
|
||||
# would get a mime_type field assigned to it (by the FileAnalysis policy hooks).
|
||||
global partial_types: table[conn_id, string] of string &read_expire=5mins;
|
||||
|
||||
# Priority 4 so that it runs before the handler that will write to http.log.
|
||||
event http_message_done(c: connection, is_orig: bool, stat: http_message_stat)
|
||||
&priority=4
|
||||
{
|
||||
if ( c$http$first_chunk )
|
||||
c$http$first_chunk=F;
|
||||
if ( ! c$http$range_request ) return;
|
||||
if ( ! c$http?$uri ) return;
|
||||
|
||||
if ( c$http?$mime_type )
|
||||
{
|
||||
partial_types[c$id, c$http$uri] = c$http$mime_type;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( [c$id, c$http$uri] in partial_types )
|
||||
c$http$mime_type = partial_types[c$id, c$http$uri];
|
||||
}
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
# These signatures are used as a replacement for libmagic. The signature
|
||||
# name needs to start with "matchfile" and the "event" directive takes
|
||||
# the mime type of the file matched by the http-reply-body pattern.
|
||||
#
|
||||
# Signatures from: http://www.garykessler.net/library/file_sigs.html
|
||||
|
||||
signature matchfile-exe {
|
||||
http-reply-body /\x4D\x5A/
|
||||
event "application/x-dosexec"
|
||||
}
|
||||
|
||||
signature matchfile-elf {
|
||||
http-reply-body /\x7F\x45\x4C\x46/
|
||||
event "application/x-executable"
|
||||
}
|
||||
|
||||
signature matchfile-script {
|
||||
# This is meant to match the interpreter declaration at the top of many
|
||||
# interpreted scripts.
|
||||
http-reply-body /\#\![[:blank:]]?\//
|
||||
event "application/x-script"
|
||||
}
|
||||
|
||||
signature matchfile-wmv {
|
||||
http-reply-body /\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C/
|
||||
event "video/x-ms-wmv"
|
||||
}
|
||||
|
||||
signature matchfile-flv {
|
||||
http-reply-body /\x46\x4C\x56\x01/
|
||||
event "video/x-flv"
|
||||
}
|
||||
|
||||
signature matchfile-swf {
|
||||
http-reply-body /[\x46\x43]\x57\x53/
|
||||
event "application/x-shockwave-flash"
|
||||
}
|
||||
|
||||
signature matchfile-jar {
|
||||
http-reply-body /\x5F\x27\xA8\x89/
|
||||
event "application/java-archive"
|
||||
}
|
||||
|
||||
signature matchfile-class {
|
||||
http-reply-body /\xCA\xFE\xBA\xBE/
|
||||
event "application/java-byte-code"
|
||||
}
|
||||
|
||||
signature matchfile-msoffice-2007 {
|
||||
# MS Office 2007 XML documents
|
||||
http-reply-body /\x50\x4B\x03\x04\x14\x00\x06\x00/
|
||||
event "application/msoffice"
|
||||
}
|
||||
|
||||
signature matchfile-msoffice {
|
||||
# Older MS Office files
|
||||
http-reply-body /\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1/
|
||||
event "application/msoffice"
|
||||
}
|
||||
|
||||
signature matchfile-rtf {
|
||||
http-reply-body /\x7B\x5C\x72\x74\x66\x31/
|
||||
event "application/rtf"
|
||||
}
|
||||
|
||||
signature matchfile-lnk {
|
||||
http-reply-body /\x4C\x00\x00\x00\x01\x14\x02\x00\x00\x00\x00\x00\xC0\x00\x00\x00\x00\x00\x00\x46/
|
||||
event "application/x-ms-shortcut"
|
||||
}
|
||||
|
||||
signature matchfile-torrent {
|
||||
http-reply-body /\x64\x38\x3A\x61\x6E\x6E\x6F\x75\x6E\x63\x65/
|
||||
event "application/x-bittorrent"
|
||||
}
|
||||
|
||||
signature matchfile-pdf {
|
||||
http-reply-body /\x25\x50\x44\x46/
|
||||
event "application/pdf"
|
||||
}
|
||||
|
||||
signature matchfile-html {
|
||||
http-reply-body /<[hH][tT][mM][lL]/
|
||||
event "text/html"
|
||||
}
|
||||
|
||||
signature matchfile-html2 {
|
||||
http-reply-body /<![dD][oO][cC][tT][yY][pP][eE][[:blank:]][hH][tT][mM][lL]/
|
||||
event "text/html"
|
||||
}
|
||||
|
||||
signature matchfile-xml {
|
||||
http-reply-body /<\??[xX][mM][lL]/
|
||||
event "text/xml"
|
||||
}
|
||||
|
||||
signature matchfile-gif {
|
||||
http-reply-body /\x47\x49\x46\x38[\x37\x39]\x61/
|
||||
event "image/gif"
|
||||
}
|
||||
|
||||
signature matchfile-jpg {
|
||||
http-reply-body /\xFF\xD8\xFF[\xDB\xE0\xE1\xE2\xE3\xE8]..[\x4A\x45\x53][\x46\x78\x50][\x49\x69][\x46\x66]/
|
||||
event "image/jpeg"
|
||||
}
|
||||
|
||||
signature matchfile-tiff {
|
||||
http-reply-body /\x4D\x4D\x00[\x2A\x2B]/
|
||||
event "image/tiff"
|
||||
}
|
||||
|
||||
signature matchfile-png {
|
||||
http-reply-body /\x89\x50\x4e\x47/
|
||||
event "image/png"
|
||||
}
|
||||
|
||||
signature matchfile-zip {
|
||||
http-reply-body /\x50\x4B\x03\x04/
|
||||
event "application/zip"
|
||||
}
|
||||
|
||||
signature matchfile-bzip {
|
||||
http-reply-body /\x42\x5A\x68/
|
||||
event "application/bzip2"
|
||||
}
|
||||
|
||||
signature matchfile-gzip {
|
||||
http-reply-body /\x1F\x8B\x08/
|
||||
event "application/x-gzip"
|
||||
}
|
||||
|
||||
signature matchfile-cab {
|
||||
http-reply-body /\x4D\x53\x43\x46/
|
||||
event "application/vnd.ms-cab-compressed"
|
||||
}
|
||||
|
||||
signature matchfile-rar {
|
||||
http-reply-body /\x52\x61\x72\x21\x1A\x07\x00/
|
||||
event "application/x-rar-compressed"
|
||||
}
|
||||
|
||||
signature matchfile-7z {
|
||||
http-reply-body /\x37\x7A\xBC\xAF\x27\x1C/
|
||||
event "application/x-7z-compressed"
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
##! to log request/response pairs and all relevant metadata together in
|
||||
##! a single record.
|
||||
|
||||
@load base/frameworks/protocols
|
||||
@load base/utils/numbers
|
||||
@load base/utils/files
|
||||
|
||||
|
@ -72,6 +71,10 @@ export {
|
|||
|
||||
## All of the headers that may indicate if the request was proxied.
|
||||
proxied: set[string] &log &optional;
|
||||
|
||||
## Indicates if this request can assume 206 partial content in
|
||||
## response.
|
||||
range_request: bool &default=F;
|
||||
};
|
||||
|
||||
## Structure to maintain state for an HTTP connection with multiple
|
||||
|
@ -95,6 +98,19 @@ export {
|
|||
"XROXY-CONNECTION",
|
||||
"PROXY-CONNECTION",
|
||||
} &redef;
|
||||
|
||||
## A list of HTTP methods. Other methods will generate a weird. Note
|
||||
## that the HTTP analyzer will only accept methods consisting solely
|
||||
## of letters ``[A-Za-z]``.
|
||||
const http_methods: set[string] = {
|
||||
"GET", "POST", "HEAD", "OPTIONS",
|
||||
"PUT", "DELETE", "TRACE", "CONNECT",
|
||||
# HTTP methods for distributed authoring:
|
||||
"PROPFIND", "PROPPATCH", "MKCOL",
|
||||
"COPY", "MOVE", "LOCK", "UNLOCK",
|
||||
"POLL", "REPORT", "SUBSCRIBE", "BMOVE",
|
||||
"SEARCH"
|
||||
} &redef;
|
||||
|
||||
## Event that can be handled to access the HTTP record as it is sent on
|
||||
## to the logging framework.
|
||||
|
@ -107,23 +123,26 @@ redef record connection += {
|
|||
http_state: State &optional;
|
||||
};
|
||||
|
||||
# Initialize the HTTP logging stream.
|
||||
# DPD configuration.
|
||||
redef capture_filters += {
|
||||
["http"] = "tcp and port (80 or 81 or 631 or 1080 or 3138 or 8000 or 8080 or 8888)"
|
||||
};
|
||||
|
||||
const ports = {
|
||||
80/tcp, 81/tcp, 631/tcp, 1080/tcp, 3128/tcp,
|
||||
8000/tcp, 8080/tcp, 8888/tcp,
|
||||
};
|
||||
|
||||
redef likely_server_ports += { ports };
|
||||
|
||||
|
||||
# Initialize the HTTP logging stream and ports.
|
||||
event bro_init() &priority=5
|
||||
{
|
||||
Log::create_stream(HTTP::LOG, [$columns=Info, $ev=log_http]);
|
||||
Analyzer::register_for_ports(Analyzer::ANALYZER_HTTP, ports);
|
||||
}
|
||||
|
||||
|
||||
global analyzers = { ANALYZER_HTTP, ANALYZER_HTTP_BINPAC };
|
||||
redef Protocols::analyzer_map += { ["HTTP"] = analyzers };
|
||||
global ports = { 80/tcp, 81/tcp, 631/tcp, 1080/tcp, 3138/tcp, 8000/tcp, 8080/tcp, 8888/tcp };
|
||||
redef Protocols::common_ports += { ["HTTP"] = ports };
|
||||
|
||||
redef likely_server_ports += {
|
||||
80/tcp, 81/tcp, 631/tcp, 1080/tcp, 3138/tcp,
|
||||
8000/tcp, 8080/tcp, 8888/tcp,
|
||||
};
|
||||
|
||||
function code_in_range(c: count, min: count, max: count) : bool
|
||||
{
|
||||
return c >= min && c <= max;
|
||||
|
@ -175,6 +194,9 @@ event http_request(c: connection, method: string, original_URI: string,
|
|||
|
||||
c$http$method = method;
|
||||
c$http$uri = unescaped_URI;
|
||||
|
||||
if ( method !in http_methods )
|
||||
event conn_weird("unknown_HTTP_method", c, method);
|
||||
}
|
||||
|
||||
event http_reply(c: connection, version: string, code: count, reason: string) &priority=5
|
||||
|
@ -214,6 +236,9 @@ event http_header(c: connection, is_orig: bool, name: string, value: string) &pr
|
|||
else if ( name == "HOST" )
|
||||
# The split is done to remove the occasional port value that shows up here.
|
||||
c$http$host = split1(value, /:/)[1];
|
||||
|
||||
else if ( name == "RANGE" )
|
||||
c$http$range_request = T;
|
||||
|
||||
else if ( name == "USER-AGENT" )
|
||||
c$http$user_agent = value;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
@load ./main
|
||||
@load ./dcc-send
|
||||
@load ./dcc-send
|
||||
@load ./file-analysis
|
||||
|
|
|
@ -28,69 +28,137 @@ export {
|
|||
dcc_file_size: count &log &optional;
|
||||
## Sniffed mime type of the file.
|
||||
dcc_mime_type: string &log &optional;
|
||||
|
||||
|
||||
## The file handle for the file to be extracted
|
||||
extraction_file: file &log &optional;
|
||||
|
||||
extraction_file: string &log &optional;
|
||||
|
||||
## A boolean to indicate if the current file transfer should be extracted.
|
||||
extract_file: bool &default=F;
|
||||
|
||||
## The count of the number of file that have been extracted during the session.
|
||||
num_extracted_files: count &default=0;
|
||||
};
|
||||
}
|
||||
|
||||
global dcc_expected_transfers: table[addr, port] of Info = table();
|
||||
global dcc_expected_transfers: table[addr, port] of Info &read_expire=5mins;
|
||||
|
||||
event file_transferred(c: connection, prefix: string, descr: string,
|
||||
mime_type: string) &priority=3
|
||||
function set_dcc_mime(f: fa_file)
|
||||
{
|
||||
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];
|
||||
|
||||
irc$dcc_mime_type = split1(mime_type, /;/)[1];
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
if ( extract_file_types == irc$dcc_mime_type )
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
irc$extract_file = T;
|
||||
}
|
||||
|
||||
if ( irc$extract_file )
|
||||
{
|
||||
local suffix = fmt("%d.dat", ++irc$num_extracted_files);
|
||||
local fname = generate_extraction_filename(extraction_prefix, c, suffix);
|
||||
irc$extraction_file = open(fname);
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( [cid$resp_h, cid$resp_p] !in dcc_expected_transfers ) next;
|
||||
|
||||
local s = dcc_expected_transfers[cid$resp_h, cid$resp_p];
|
||||
|
||||
s$dcc_mime_type = f$mime_type;
|
||||
}
|
||||
}
|
||||
|
||||
event file_transferred(c: connection, prefix: string, descr: string,
|
||||
mime_type: string) &priority=-4
|
||||
function set_dcc_extraction_file(f: fa_file, filename: string)
|
||||
{
|
||||
local id = c$id;
|
||||
if ( [id$resp_h, id$resp_p] !in dcc_expected_transfers )
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( [cid$resp_h, cid$resp_p] !in dcc_expected_transfers ) next;
|
||||
|
||||
local s = dcc_expected_transfers[cid$resp_h, cid$resp_p];
|
||||
|
||||
s$extraction_file = filename;
|
||||
}
|
||||
}
|
||||
|
||||
function get_extraction_name(f: fa_file): string
|
||||
{
|
||||
local r = fmt("%s-%s.dat", extraction_prefix, f$id);
|
||||
return r;
|
||||
}
|
||||
|
||||
# this handler sets the IRC::Info mime type
|
||||
event file_new(f: fa_file) &priority=5
|
||||
{
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "IRC_DATA" ) return;
|
||||
if ( ! f?$mime_type ) return;
|
||||
|
||||
set_dcc_mime(f);
|
||||
}
|
||||
|
||||
# this handler check if file extraction is desired
|
||||
event file_new(f: fa_file) &priority=5
|
||||
{
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "IRC_DATA" ) return;
|
||||
|
||||
local fname: string;
|
||||
|
||||
if ( f?$mime_type && extract_file_types in f$mime_type )
|
||||
{
|
||||
fname = get_extraction_name(f);
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_EXTRACT,
|
||||
$extract_filename=fname]);
|
||||
set_dcc_extraction_file(f, fname);
|
||||
return;
|
||||
}
|
||||
|
||||
local irc = dcc_expected_transfers[id$resp_h, id$resp_p];
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
local tmp = irc$command;
|
||||
irc$command = "DCC";
|
||||
Log::write(IRC::LOG, irc);
|
||||
irc$command = tmp;
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( irc?$extraction_file )
|
||||
set_contents_file(id, CONTENTS_RESP, irc$extraction_file);
|
||||
if ( [cid$resp_h, cid$resp_p] !in dcc_expected_transfers ) next;
|
||||
|
||||
# Delete these values in case another DCC transfer
|
||||
# happens during the IRC session.
|
||||
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];
|
||||
local s = dcc_expected_transfers[cid$resp_h, cid$resp_p];
|
||||
|
||||
if ( ! s$extract_file ) next;
|
||||
|
||||
fname = get_extraction_name(f);
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_EXTRACT,
|
||||
$extract_filename=fname]);
|
||||
s$extraction_file = fname;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function log_dcc(f: fa_file)
|
||||
{
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( [cid$resp_h, cid$resp_p] !in dcc_expected_transfers ) next;
|
||||
|
||||
local irc = dcc_expected_transfers[cid$resp_h, cid$resp_p];
|
||||
|
||||
local tmp = irc$command;
|
||||
irc$command = "DCC";
|
||||
Log::write(IRC::LOG, irc);
|
||||
irc$command = tmp;
|
||||
|
||||
# Delete these values in case another DCC transfer
|
||||
# happens during the IRC session.
|
||||
delete irc$extract_file;
|
||||
delete irc$extraction_file;
|
||||
delete irc$dcc_file_name;
|
||||
delete irc$dcc_file_size;
|
||||
delete irc$dcc_mime_type;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
event file_new(f: fa_file) &priority=-5
|
||||
{
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "IRC_DATA" ) return;
|
||||
|
||||
log_dcc(f);
|
||||
}
|
||||
|
||||
event irc_dcc_message(c: connection, is_orig: bool,
|
||||
|
@ -100,17 +168,22 @@ event irc_dcc_message(c: connection, is_orig: bool,
|
|||
{
|
||||
set_session(c);
|
||||
if ( dcc_type != "SEND" )
|
||||
return;
|
||||
return;
|
||||
c$irc$dcc_file_name = argument;
|
||||
c$irc$dcc_file_size = size;
|
||||
local p = count_to_port(dest_port, tcp);
|
||||
expect_connection(to_addr("0.0.0.0"), address, p, ANALYZER_FILE, 5 min);
|
||||
Analyzer::schedule_analyzer(0.0.0.0, address, p, Analyzer::ANALYZER_IRC_DATA, 5 min);
|
||||
dcc_expected_transfers[address, p] = c$irc;
|
||||
}
|
||||
|
||||
event expected_connection_seen(c: connection, a: count) &priority=10
|
||||
event expected_connection_seen(c: connection, a: Analyzer::Tag) &priority=10
|
||||
{
|
||||
local id = c$id;
|
||||
if ( [id$resp_h, id$resp_p] in dcc_expected_transfers )
|
||||
add c$service["irc-dcc-data"];
|
||||
}
|
||||
|
||||
event connection_state_remove(c: connection) &priority=-5
|
||||
{
|
||||
delete dcc_expected_transfers[c$id$resp_h, c$id$resp_p];
|
||||
}
|
||||
|
|
25
scripts/base/protocols/irc/file-analysis.bro
Normal file
25
scripts/base/protocols/irc/file-analysis.bro
Normal file
|
@ -0,0 +1,25 @@
|
|||
@load ./dcc-send.bro
|
||||
@load base/utils/conn-ids
|
||||
@load base/frameworks/file-analysis/main
|
||||
|
||||
module IRC;
|
||||
|
||||
export {
|
||||
## Default file handle provider for IRC.
|
||||
global get_file_handle: function(c: connection, is_orig: bool): string;
|
||||
}
|
||||
|
||||
function get_file_handle(c: connection, is_orig: bool): string
|
||||
{
|
||||
if ( is_orig ) return "";
|
||||
return cat(Analyzer::ANALYZER_IRC_DATA, " ", c$start_time, " ", id_string(c$id));
|
||||
}
|
||||
|
||||
module GLOBAL;
|
||||
|
||||
event get_file_handle(tag: Analyzer::Tag, c: connection, is_orig: bool)
|
||||
&priority=5
|
||||
{
|
||||
if ( tag != Analyzer::ANALYZER_IRC_DATA ) return;
|
||||
set_file_handle(IRC::get_file_handle(c, is_orig));
|
||||
}
|
|
@ -2,8 +2,6 @@
|
|||
##! IRC commands along with the associated response and some additional
|
||||
##! metadata about the connection if it's available.
|
||||
|
||||
@load base/frameworks/protocols
|
||||
|
||||
module IRC;
|
||||
|
||||
export {
|
||||
|
@ -40,14 +38,20 @@ redef record connection += {
|
|||
irc: Info &optional;
|
||||
};
|
||||
|
||||
global analyzers = { ANALYZER_IRC };
|
||||
redef Protocols::analyzer_map += { ["IRC"] = analyzers };
|
||||
global ports = { 6666/tcp, 6667/tcp, 6668/tcp, 6669/tcp, 7000/tcp };
|
||||
redef Protocols::common_ports += { ["IRC"] = ports };
|
||||
# Some common IRC ports.
|
||||
redef capture_filters += { ["irc-6666"] = "port 6666" };
|
||||
redef capture_filters += { ["irc-6667"] = "port 6667" };
|
||||
redef capture_filters += { ["irc-6668"] = "port 6668" };
|
||||
redef capture_filters += { ["irc-6669"] = "port 6669" };
|
||||
|
||||
# DPD configuration.
|
||||
const ports = { 6666/tcp, 6667/tcp, 6668/tcp, 6669/tcp };
|
||||
redef likely_server_ports += { ports };
|
||||
|
||||
event bro_init() &priority=5
|
||||
{
|
||||
Log::create_stream(IRC::LOG, [$columns=Info, $ev=irc_log]);
|
||||
Analyzer::register_for_ports(Analyzer::ANALYZER_IRC, ports);
|
||||
}
|
||||
|
||||
function new_session(c: connection): Info
|
||||
|
|
|
@ -31,12 +31,14 @@ redef record connection += {
|
|||
|
||||
# Configure DPD and the packet filter.
|
||||
redef capture_filters += { ["modbus"] = "tcp port 502" };
|
||||
redef dpd_config += { [ANALYZER_MODBUS] = [$ports = set(502/tcp)] };
|
||||
redef likely_server_ports += { 502/tcp };
|
||||
|
||||
const ports = { 502/tcp };
|
||||
redef likely_server_ports += { ports };
|
||||
|
||||
event bro_init() &priority=5
|
||||
{
|
||||
Log::create_stream(Modbus::LOG, [$columns=Info, $ev=log_modbus]);
|
||||
Analyzer::register_for_ports(Analyzer::ANALYZER_MODBUS, ports);
|
||||
}
|
||||
|
||||
event modbus_message(c: connection, headers: ModbusHeaders, is_orig: bool) &priority=5
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@load ./main
|
||||
@load ./entities
|
||||
@load ./entities-excerpt
|
||||
@load ./entities-excerpt
|
||||
@load ./file-analysis
|
||||
|
|
|
@ -9,44 +9,29 @@ export {
|
|||
redef record SMTP::EntityInfo += {
|
||||
## The entity body excerpt.
|
||||
excerpt: string &log &default="";
|
||||
|
||||
## Internal tracking to know how much of the body should be included
|
||||
## in the excerpt.
|
||||
excerpt_len: count &optional;
|
||||
};
|
||||
|
||||
## This is the default value for how much of the entity body should be
|
||||
## included for all MIME entities.
|
||||
## included for all MIME entities. The lesser of this value and
|
||||
## :bro:see:`default_file_bof_buffer_size` will be used.
|
||||
const default_entity_excerpt_len = 0 &redef;
|
||||
|
||||
## This table defines how much of various entity bodies should be
|
||||
## included in excerpts.
|
||||
const entity_excerpt_len: table[string] of count = {}
|
||||
&redef
|
||||
&default = default_entity_excerpt_len;
|
||||
}
|
||||
|
||||
event mime_segment_data(c: connection, length: count, data: string) &priority=-1
|
||||
event file_new(f: fa_file) &priority=5
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
|
||||
if ( c$smtp$current_entity$content_len == 0 )
|
||||
c$smtp$current_entity$excerpt_len = entity_excerpt_len[c$smtp$current_entity$mime_type];
|
||||
}
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "SMTP" ) return;
|
||||
if ( ! f?$bof_buffer ) return;
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
event mime_segment_data(c: connection, length: count, data: string) &priority=-2
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
|
||||
local ent = c$smtp$current_entity;
|
||||
if ( ent$content_len < ent$excerpt_len )
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
if ( ent$content_len + length < ent$excerpt_len )
|
||||
ent$excerpt = cat(ent$excerpt, data);
|
||||
else
|
||||
{
|
||||
local x_bytes = ent$excerpt_len - ent$content_len;
|
||||
ent$excerpt = cat(ent$excerpt, sub_bytes(data, 1, x_bytes));
|
||||
}
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( ! c?$smtp ) next;
|
||||
|
||||
if ( default_entity_excerpt_len > 0 )
|
||||
c$smtp$current_entity$excerpt =
|
||||
f$bof_buffer[0:default_entity_excerpt_len];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,42 +7,34 @@
|
|||
module SMTP;
|
||||
|
||||
export {
|
||||
redef enum Notice::Type += {
|
||||
## Indicates that an MD5 sum was calculated for a MIME message.
|
||||
MD5,
|
||||
};
|
||||
|
||||
redef enum Log::ID += { ENTITIES_LOG };
|
||||
|
||||
type EntityInfo: record {
|
||||
## This is the timestamp of when the MIME content transfer began.
|
||||
ts: time &log;
|
||||
uid: string &log;
|
||||
id: conn_id &log;
|
||||
ts: time &log;
|
||||
uid: string &log;
|
||||
id: conn_id &log;
|
||||
## A count to represent the depth of this message transaction in a
|
||||
## single connection where multiple messages were transferred.
|
||||
trans_depth: count &log;
|
||||
trans_depth: count &log;
|
||||
## The filename seen in the Content-Disposition header.
|
||||
filename: string &log &optional;
|
||||
filename: string &log &optional;
|
||||
## Track how many bytes of the MIME encoded file have been seen.
|
||||
content_len: count &log &default=0;
|
||||
content_len: count &log &default=0;
|
||||
## The mime type of the entity discovered through magic bytes identification.
|
||||
mime_type: string &log &optional;
|
||||
mime_type: string &log &optional;
|
||||
|
||||
## The calculated MD5 sum for the MIME entity.
|
||||
md5: string &log &optional;
|
||||
md5: string &log &optional;
|
||||
## Optionally calculate the file's MD5 sum. Must be set prior to the
|
||||
## first data chunk being see in an event.
|
||||
calc_md5: bool &default=F;
|
||||
## This boolean value indicates if an MD5 sum is being calculated
|
||||
## for the current file transfer.
|
||||
calculating_md5: bool &default=F;
|
||||
calc_md5: bool &default=F;
|
||||
|
||||
## Optionally write the file to disk. Must be set prior to first
|
||||
## data chunk being seen in an event.
|
||||
extract_file: bool &default=F;
|
||||
extract_file: bool &default=F;
|
||||
## Store the file handle here for the file currently being extracted.
|
||||
extraction_file: file &log &optional;
|
||||
extraction_file: string &log &optional;
|
||||
};
|
||||
|
||||
redef record Info += {
|
||||
|
@ -51,9 +43,6 @@ export {
|
|||
};
|
||||
|
||||
redef record State += {
|
||||
## Store a count of the number of files that have been transferred in
|
||||
## a conversation to create unique file names on disk.
|
||||
num_extracted_files: count &default=0;
|
||||
## Track the number of MIME encoded files transferred during a session.
|
||||
mime_level: count &default=0;
|
||||
};
|
||||
|
@ -97,76 +86,126 @@ function set_session(c: connection, new_entity: bool)
|
|||
}
|
||||
}
|
||||
|
||||
function get_extraction_name(f: fa_file): string
|
||||
{
|
||||
local r = fmt("%s-%s.dat", extraction_prefix, f$id);
|
||||
return r;
|
||||
}
|
||||
|
||||
event mime_begin_entity(c: connection) &priority=10
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
|
||||
|
||||
set_session(c, T);
|
||||
}
|
||||
|
||||
# This has priority -10 because other handlers need to know the current
|
||||
# content_len before it's updated by this handler.
|
||||
event mime_segment_data(c: connection, length: count, data: string) &priority=-10
|
||||
event file_new(f: fa_file) &priority=5
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
|
||||
c$smtp$current_entity$content_len = c$smtp$current_entity$content_len + length;
|
||||
}
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "SMTP" ) return;
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
event mime_segment_data(c: connection, length: count, data: string) &priority=7
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
if ( c$smtp$current_entity$content_len == 0 )
|
||||
c$smtp$current_entity$mime_type = split1(identify_data(data, T), /;/)[1];
|
||||
}
|
||||
local fname: string;
|
||||
local extracting: bool = F;
|
||||
|
||||
event mime_segment_data(c: connection, length: count, data: string) &priority=-5
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
|
||||
if ( c$smtp$current_entity$content_len == 0 )
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
if ( generate_md5 in c$smtp$current_entity$mime_type && ! never_calc_md5 )
|
||||
c$smtp$current_entity$calc_md5 = T;
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( ! c?$smtp ) next;
|
||||
if ( ! c$smtp?$current_entity ) next;
|
||||
|
||||
if ( c$smtp$current_entity$extract_file )
|
||||
{
|
||||
if ( ! extracting )
|
||||
{
|
||||
fname = get_extraction_name(f);
|
||||
FileAnalysis::add_analyzer(f,
|
||||
[$tag=FileAnalysis::ANALYZER_EXTRACT,
|
||||
$extract_filename=fname]);
|
||||
extracting = T;
|
||||
}
|
||||
|
||||
c$smtp$current_entity$extraction_file = fname;
|
||||
}
|
||||
|
||||
if ( c$smtp$current_entity$calc_md5 )
|
||||
{
|
||||
c$smtp$current_entity$calculating_md5 = T;
|
||||
md5_hash_init(c$id);
|
||||
}
|
||||
}
|
||||
|
||||
if ( c$smtp$current_entity$calculating_md5 )
|
||||
md5_hash_update(c$id, data);
|
||||
}
|
||||
|
||||
## In the event of a content gap during the MIME transfer, detect the state for
|
||||
## the MD5 sum calculation and stop calculating the MD5 since it would be
|
||||
## incorrect anyway.
|
||||
event content_gap(c: connection, is_orig: bool, seq: count, length: count) &priority=5
|
||||
{
|
||||
if ( is_orig || ! c?$smtp || ! c$smtp?$current_entity ) return;
|
||||
|
||||
if ( c$smtp$current_entity$calculating_md5 )
|
||||
{
|
||||
c$smtp$current_entity$calculating_md5 = F;
|
||||
md5_hash_finish(c$id);
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_MD5]);
|
||||
}
|
||||
}
|
||||
|
||||
event mime_end_entity(c: connection) &priority=-3
|
||||
{
|
||||
# TODO: this check is only due to a bug in mime_end_entity that
|
||||
# causes the event to be generated twice for the same real event.
|
||||
if ( ! c?$smtp || ! c$smtp?$current_entity )
|
||||
function check_extract_by_type(f: fa_file)
|
||||
{
|
||||
if ( extract_file_types !in f$mime_type ) return;
|
||||
|
||||
if ( f?$info && FileAnalysis::ANALYZER_EXTRACT in f$info$analyzers )
|
||||
return;
|
||||
|
||||
if ( c$smtp$current_entity$calculating_md5 )
|
||||
{
|
||||
c$smtp$current_entity$md5 = md5_hash_finish(c$id);
|
||||
local fname: string = get_extraction_name(f);
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_EXTRACT,
|
||||
$extract_filename=fname]);
|
||||
|
||||
NOTICE([$note=MD5, $msg=fmt("Calculated a hash for a MIME entity from %s", c$id$orig_h),
|
||||
$sub=c$smtp$current_entity$md5, $conn=c]);
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
local c: connection = f$conns[cid];
|
||||
if ( ! c?$smtp ) next;
|
||||
c$smtp$current_entity$extraction_file = fname;
|
||||
}
|
||||
}
|
||||
|
||||
function check_md5_by_type(f: fa_file)
|
||||
{
|
||||
if ( never_calc_md5 ) return;
|
||||
if ( generate_md5 !in f$mime_type ) return;
|
||||
|
||||
FileAnalysis::add_analyzer(f, [$tag=FileAnalysis::ANALYZER_MD5]);
|
||||
}
|
||||
|
||||
event file_new(f: fa_file) &priority=5
|
||||
{
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "SMTP" ) return;
|
||||
if ( ! f?$mime_type ) return;
|
||||
|
||||
if ( f?$conns )
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( ! c?$smtp ) next;
|
||||
if ( ! c$smtp?$current_entity ) next;
|
||||
|
||||
c$smtp$current_entity$mime_type = f$mime_type;
|
||||
}
|
||||
|
||||
check_extract_by_type(f);
|
||||
check_md5_by_type(f);
|
||||
}
|
||||
|
||||
event file_state_remove(f: fa_file) &priority=4
|
||||
{
|
||||
if ( ! f?$source ) return;
|
||||
if ( f$source != "SMTP" ) return;
|
||||
if ( ! f?$conns ) return;
|
||||
|
||||
for ( cid in f$conns )
|
||||
{
|
||||
local c: connection = f$conns[cid];
|
||||
|
||||
if ( ! c?$smtp ) next;
|
||||
if ( ! c$smtp?$current_entity ) next;
|
||||
# Only log if there was some content.
|
||||
if ( f$seen_bytes == 0 ) next;
|
||||
|
||||
if ( f?$info && f$info?$md5 )
|
||||
c$smtp$current_entity$md5 = f$info$md5;
|
||||
|
||||
c$smtp$current_entity$content_len = f$seen_bytes;
|
||||
Log::write(SMTP::ENTITIES_LOG, c$smtp$current_entity);
|
||||
delete c$smtp$current_entity;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -177,63 +216,8 @@ event mime_one_header(c: connection, h: mime_header_rec)
|
|||
if ( h$name == "CONTENT-DISPOSITION" &&
|
||||
/[fF][iI][lL][eE][nN][aA][mM][eE]/ in h$value )
|
||||
c$smtp$current_entity$filename = extract_filename_from_content_disposition(h$value);
|
||||
}
|
||||
|
||||
event mime_end_entity(c: connection) &priority=-5
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
|
||||
# This check and the delete below are just to cope with a bug where
|
||||
# mime_end_entity can be generated multiple times for the same event.
|
||||
if ( ! c$smtp?$current_entity )
|
||||
return;
|
||||
|
||||
# Only log is there was some content.
|
||||
if ( c$smtp$current_entity$content_len > 0 )
|
||||
Log::write(SMTP::ENTITIES_LOG, c$smtp$current_entity);
|
||||
|
||||
delete c$smtp$current_entity;
|
||||
}
|
||||
|
||||
event mime_segment_data(c: connection, length: count, data: string) &priority=5
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
|
||||
if ( extract_file_types in c$smtp$current_entity$mime_type )
|
||||
c$smtp$current_entity$extract_file = T;
|
||||
}
|
||||
|
||||
event mime_segment_data(c: connection, length: count, data: string) &priority=3
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
|
||||
if ( c$smtp$current_entity$extract_file &&
|
||||
c$smtp$current_entity$content_len == 0 )
|
||||
{
|
||||
local suffix = fmt("%d.dat", ++c$smtp_state$num_extracted_files);
|
||||
local fname = generate_extraction_filename(extraction_prefix, c, suffix);
|
||||
c$smtp$current_entity$extraction_file = open(fname);
|
||||
enable_raw_output(c$smtp$current_entity$extraction_file);
|
||||
}
|
||||
}
|
||||
|
||||
event mime_segment_data(c: connection, length: count, data: string) &priority=-5
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
|
||||
if ( c$smtp$current_entity$extract_file && c$smtp$current_entity?$extraction_file )
|
||||
print c$smtp$current_entity$extraction_file, data;
|
||||
}
|
||||
|
||||
event mime_end_entity(c: connection) &priority=-3
|
||||
{
|
||||
if ( ! c?$smtp ) return;
|
||||
|
||||
# TODO: this check is only due to a bug in mime_end_entity that
|
||||
# causes the event to be generated twice for the same real event.
|
||||
if ( ! c$smtp?$current_entity )
|
||||
return;
|
||||
|
||||
if ( c$smtp$current_entity?$extraction_file )
|
||||
close(c$smtp$current_entity$extraction_file);
|
||||
|
||||
if ( h$name == "CONTENT-TYPE" &&
|
||||
/[nN][aA][mM][eE][:blank:]*=/ in h$value )
|
||||
c$smtp$current_entity$filename = extract_filename_from_content_disposition(h$value);
|
||||
}
|
||||
|
|
27
scripts/base/protocols/smtp/file-analysis.bro
Normal file
27
scripts/base/protocols/smtp/file-analysis.bro
Normal file
|
@ -0,0 +1,27 @@
|
|||
@load ./main
|
||||
@load ./entities
|
||||
@load base/utils/conn-ids
|
||||
@load base/frameworks/file-analysis/main
|
||||
|
||||
module SMTP;
|
||||
|
||||
export {
|
||||
## Default file handle provider for SMTP.
|
||||
global get_file_handle: function(c: connection, is_orig: bool): string;
|
||||
}
|
||||
|
||||
function get_file_handle(c: connection, is_orig: bool): string
|
||||
{
|
||||
if ( ! c?$smtp ) return "";
|
||||
return cat(Analyzer::ANALYZER_SMTP, " ", c$start_time, " ", c$smtp$trans_depth, " ",
|
||||
c$smtp_state$mime_level);
|
||||
}
|
||||
|
||||
module GLOBAL;
|
||||
|
||||
event get_file_handle(tag: Analyzer::Tag, c: connection, is_orig: bool)
|
||||
&priority=5
|
||||
{
|
||||
if ( tag != Analyzer::ANALYZER_SMTP ) return;
|
||||
set_file_handle(SMTP::get_file_handle(c, is_orig));
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
@load base/frameworks/notice
|
||||
@load base/frameworks/protocols
|
||||
@load base/utils/addrs
|
||||
@load base/utils/directions-and-hosts
|
||||
|
||||
|
@ -82,14 +81,16 @@ redef record connection += {
|
|||
smtp_state: State &optional;
|
||||
};
|
||||
|
||||
global analyzers = { ANALYZER_SMTP };
|
||||
redef Protocols::analyzer_map += { ["SMTP"] = analyzers };
|
||||
const ports = { 25/tcp, 587/tcp } &redef;
|
||||
redef Protocols::common_ports += { ["SMTP"] = ports };
|
||||
# Configure DPD
|
||||
redef capture_filters += { ["smtp"] = "tcp port 25 or tcp port 587" };
|
||||
|
||||
const ports = { 25/tcp, 587/tcp };
|
||||
redef likely_server_ports += { ports };
|
||||
|
||||
event bro_init() &priority=5
|
||||
{
|
||||
Log::create_stream(SMTP::LOG, [$columns=SMTP::Info, $ev=log_smtp]);
|
||||
Analyzer::register_for_ports(Analyzer::ANALYZER_SMTP, ports);
|
||||
}
|
||||
|
||||
function find_address_in_smtp_header(header: string): string
|
||||
|
|
|
@ -34,9 +34,13 @@ export {
|
|||
global log_socks: event(rec: Info);
|
||||
}
|
||||
|
||||
const ports = { 1080/tcp };
|
||||
redef likely_server_ports += { ports };
|
||||
|
||||
event bro_init() &priority=5
|
||||
{
|
||||
Log::create_stream(SOCKS::LOG, [$columns=Info, $ev=log_socks]);
|
||||
Analyzer::register_for_ports(Analyzer::ANALYZER_SOCKS, ports);
|
||||
}
|
||||
|
||||
redef record connection += {
|
||||
|
@ -45,7 +49,6 @@ redef record connection += {
|
|||
|
||||
# Configure DPD
|
||||
redef capture_filters += { ["socks"] = "tcp port 1080" };
|
||||
redef dpd_config += { [ANALYZER_SOCKS] = [$ports = set(1080/tcp)] };
|
||||
redef likely_server_ports += { 1080/tcp };
|
||||
|
||||
function set_session(c: connection, version: count)
|
||||
|
@ -67,7 +70,7 @@ event socks_request(c: connection, version: count, request_type: count,
|
|||
# proxied connection. We treat this as a singular "tunnel".
|
||||
local cid = copy(c$id);
|
||||
cid$orig_p = 0/tcp;
|
||||
Tunnel::register([$cid=cid, $tunnel_type=Tunnel::SOCKS, $payload_proxy=T]);
|
||||
Tunnel::register([$cid=cid, $tunnel_type=Tunnel::SOCKS]);
|
||||
}
|
||||
|
||||
event socks_reply(c: connection, version: count, reply: count, sa: SOCKS::Address, p: port) &priority=5
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
##! Base SSH analysis script. The heuristic to blindly determine success or
|
||||
##! Base SSH analysis script. The heuristic to blindly determine success or
|
||||
##! failure for SSH connections is implemented here. At this time, it only
|
||||
##! uses the size of the data being returned from the server to make the
|
||||
##! heuristic determination about success of the connection.
|
||||
##! heuristic determination about success of the connection.
|
||||
##! Requires that :bro:id:`use_conn_size_analyzer` is set to T! The heuristic
|
||||
##! is not attempted if the connection size analyzer isn't enabled.
|
||||
|
||||
@load base/protocols/conn
|
||||
@load base/frameworks/notice
|
||||
@load base/frameworks/protocols
|
||||
@load base/utils/site
|
||||
@load base/utils/thresholds
|
||||
@load base/utils/conn-ids
|
||||
|
@ -17,12 +17,6 @@ module SSH;
|
|||
export {
|
||||
## The SSH protocol logging stream identifier.
|
||||
redef enum Log::ID += { LOG };
|
||||
|
||||
redef enum Notice::Type += {
|
||||
## Indicates that a heuristically detected "successful" SSH
|
||||
## authentication occurred.
|
||||
Login
|
||||
};
|
||||
|
||||
type Info: record {
|
||||
## Time when the SSH connection began.
|
||||
|
@ -31,10 +25,10 @@ export {
|
|||
uid: string &log;
|
||||
## The connection's 4-tuple of endpoint addresses/ports.
|
||||
id: conn_id &log;
|
||||
## Indicates if the login was heuristically guessed to be "success"
|
||||
## or "failure".
|
||||
status: string &log &optional;
|
||||
## Direction of the connection. If the client was a local host
|
||||
## Indicates if the login was heuristically guessed to be "success",
|
||||
## "failure", or "undetermined".
|
||||
status: string &log &default="undetermined";
|
||||
## Direction of the connection. If the client was a local host
|
||||
## logging into an external host, this would be OUTBOUND. INBOUND
|
||||
## would be set for the opposite situation.
|
||||
# TODO: handle local-local and remote-remote better.
|
||||
|
@ -44,42 +38,44 @@ export {
|
|||
## Software string from the server.
|
||||
server: string &log &optional;
|
||||
## Amount of data returned from the server. This is currently
|
||||
## the only measure of the success heuristic and it is logged to
|
||||
## the only measure of the success heuristic and it is logged to
|
||||
## assist analysts looking at the logs to make their own determination
|
||||
## about the success on a case-by-case basis.
|
||||
resp_size: count &log &default=0;
|
||||
|
||||
|
||||
## Indicate if the SSH session is done being watched.
|
||||
done: bool &default=F;
|
||||
};
|
||||
|
||||
## The size in bytes of data sent by the server at which the SSH
|
||||
|
||||
## The size in bytes of data sent by the server at which the SSH
|
||||
## connection is presumed to be successful.
|
||||
const authentication_data_size = 5500 &redef;
|
||||
|
||||
const authentication_data_size = 4000 &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).
|
||||
## kinds of analyses.
|
||||
const skip_processing_after_detection = F &redef;
|
||||
|
||||
|
||||
## Event that is generated when the heuristic thinks that a login
|
||||
## was successful.
|
||||
global heuristic_successful_login: event(c: connection);
|
||||
|
||||
|
||||
## Event that is generated when the heuristic thinks that a login
|
||||
## failed.
|
||||
global heuristic_failed_login: event(c: connection);
|
||||
|
||||
|
||||
## Event that can be handled to access the :bro:type:`SSH::Info`
|
||||
## record as it is sent on to the logging framework.
|
||||
global log_ssh: event(rec: Info);
|
||||
}
|
||||
|
||||
global analyzers = { ANALYZER_SSH };
|
||||
redef Protocols::analyzer_map += { ["SSH"] = analyzers };
|
||||
global ports = { 22/tcp };
|
||||
redef Protocols::common_ports += { ["SSH"] = ports };
|
||||
# Configure DPD and the packet filter
|
||||
|
||||
const ports = { 22/tcp };
|
||||
|
||||
redef capture_filters += { ["ssh"] = "tcp port 22" };
|
||||
redef likely_server_ports += { ports };
|
||||
|
||||
redef record connection += {
|
||||
ssh: Info &optional;
|
||||
|
@ -88,6 +84,7 @@ redef record connection += {
|
|||
event bro_init() &priority=5
|
||||
{
|
||||
Log::create_stream(SSH::LOG, [$columns=Info, $ev=log_ssh]);
|
||||
Analyzer::register_for_ports(Analyzer::ANALYZER_SSH, ports);
|
||||
}
|
||||
|
||||
function set_session(c: connection)
|
||||
|
@ -104,55 +101,61 @@ function set_session(c: connection)
|
|||
|
||||
function check_ssh_connection(c: connection, done: bool)
|
||||
{
|
||||
# If done watching this connection, just return.
|
||||
# If already done watching this connection, just return.
|
||||
if ( c$ssh$done )
|
||||
return;
|
||||
|
||||
# Make sure conn_size_analyzer is active by checking
|
||||
# resp$num_bytes_ip. In general it should always be active though.
|
||||
if ( ! c$resp?$num_bytes_ip )
|
||||
return;
|
||||
|
||||
# Remove the IP and TCP header length from the total size.
|
||||
# TODO: Fix for IPv6. This whole approach also seems to break in some
|
||||
# cases where there are more header bytes than num_bytes_ip.
|
||||
local header_bytes = c$resp$num_pkts*32 + c$resp$num_pkts*20;
|
||||
local server_bytes = c$resp$num_bytes_ip;
|
||||
if ( server_bytes >= header_bytes )
|
||||
server_bytes = server_bytes - header_bytes;
|
||||
else
|
||||
server_bytes = c$resp$size;
|
||||
|
||||
# If this is still a live connection and the byte count has not crossed
|
||||
# the threshold, just return and let the rescheduled check happen later.
|
||||
if ( ! done && server_bytes < 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 ( server_bytes < 50 )
|
||||
return;
|
||||
|
||||
c$ssh$direction = Site::is_local_addr(c$id$orig_h) ? OUTBOUND : INBOUND;
|
||||
c$ssh$resp_size = server_bytes;
|
||||
|
||||
if ( server_bytes < authentication_data_size )
|
||||
if ( done )
|
||||
{
|
||||
c$ssh$status = "failure";
|
||||
event SSH::heuristic_failed_login(c);
|
||||
# If this connection is done, then we can look to see if
|
||||
# this matches the conditions for a failed login. Failed
|
||||
# logins are only detected at connection state removal.
|
||||
|
||||
if ( # Require originators to have sent at least 50 bytes.
|
||||
c$orig$size > 50 &&
|
||||
# Responders must be below 4000 bytes.
|
||||
c$resp$size < 4000 &&
|
||||
# Responder must have sent fewer than 40 packets.
|
||||
c$resp$num_pkts < 40 &&
|
||||
# If there was a content gap we can't reliably do this heuristic.
|
||||
c?$conn && c$conn$missed_bytes == 0 )# &&
|
||||
# Only "normal" connections can count.
|
||||
#c$conn?$conn_state && c$conn$conn_state in valid_states )
|
||||
{
|
||||
c$ssh$status = "failure";
|
||||
event SSH::heuristic_failed_login(c);
|
||||
}
|
||||
|
||||
if ( c$resp$size > authentication_data_size )
|
||||
{
|
||||
c$ssh$status = "success";
|
||||
event SSH::heuristic_successful_login(c);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
# presumed successful login
|
||||
c$ssh$status = "success";
|
||||
event SSH::heuristic_successful_login(c);
|
||||
{
|
||||
# If this connection is still being tracked, then it's possible
|
||||
# to watch for it to be a successful connection.
|
||||
if ( c$resp$size > authentication_data_size )
|
||||
{
|
||||
c$ssh$status = "success";
|
||||
event SSH::heuristic_successful_login(c);
|
||||
}
|
||||
else
|
||||
# This connection must be tracked longer. Let the scheduled
|
||||
# check happen again.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
# Set the direction for the log.
|
||||
c$ssh$direction = Site::is_local_addr(c$id$orig_h) ? OUTBOUND : INBOUND;
|
||||
|
||||
# Set the "done" flag to prevent the watching event from rescheduling
|
||||
# after detection is done.
|
||||
c$ssh$done=T;
|
||||
|
||||
|
||||
Log::write(SSH::LOG, c$ssh);
|
||||
|
||||
if ( skip_processing_after_detection )
|
||||
{
|
||||
# Stop watching this connection, we don't care about it anymore.
|
||||
|
@ -161,18 +164,6 @@ function check_ssh_connection(c: connection, done: bool)
|
|||
}
|
||||
}
|
||||
|
||||
event SSH::heuristic_successful_login(c: connection) &priority=-5
|
||||
{
|
||||
NOTICE([$note=Login,
|
||||
$msg="Heuristically detected successful SSH login.",
|
||||
$conn=c]);
|
||||
|
||||
Log::write(SSH::LOG, c$ssh);
|
||||
}
|
||||
event SSH::heuristic_failed_login(c: connection) &priority=-5
|
||||
{
|
||||
Log::write(SSH::LOG, c$ssh);
|
||||
}
|
||||
|
||||
event connection_state_remove(c: connection) &priority=-5
|
||||
{
|
||||
|
@ -187,6 +178,7 @@ event ssh_watcher(c: connection)
|
|||
if ( ! connection_exists(id) )
|
||||
return;
|
||||
|
||||
lookup_connection(c$id);
|
||||
check_ssh_connection(c, F);
|
||||
if ( ! c$ssh$done )
|
||||
schedule +15secs { ssh_watcher(c) };
|
||||
|
@ -197,12 +189,12 @@ 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;
|
||||
|
||||
|
||||
# The heuristic detection for SSH relies on the ConnSize analyzer.
|
||||
# Don't do the heuristics if it's disabled.
|
||||
if ( use_conn_size_analyzer )
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
@load ./consts
|
||||
@load ./main
|
||||
@load ./mozilla-ca-list
|
||||
@load ./mozilla-ca-list
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
##! Base SSL analysis script. This script logs information about the SSL/TLS
|
||||
##! handshaking and encryption establishment process.
|
||||
|
||||
@load base/frameworks/protocols
|
||||
@load ./consts
|
||||
|
||||
module SSL;
|
||||
|
@ -68,10 +67,16 @@ export {
|
|||
## (especially with large file transfers).
|
||||
const disable_analyzer_after_detection = T &redef;
|
||||
|
||||
## The openssl command line utility. If it's in the path the default
|
||||
## value will work, otherwise a full path string can be supplied for the
|
||||
## utility.
|
||||
const openssl_util = "openssl" &redef;
|
||||
## The maximum amount of time a script can delay records from being logged.
|
||||
const max_log_delay = 15secs &redef;
|
||||
|
||||
## Delays an SSL record for a specific token: the record will not be logged
|
||||
## as longs the token exists or until :bro:id:`SSL::max_log_delay` elapses.
|
||||
global delay_log: function(info: Info, token: string);
|
||||
|
||||
## Undelays an SSL record for a previously inserted token, allowing the
|
||||
## record to be logged.
|
||||
global undelay_log: function(info: Info, token: string);
|
||||
|
||||
## Event that can be handled to access the SSL
|
||||
## record as it is sent on to the logging framework.
|
||||
|
@ -82,17 +87,40 @@ redef record connection += {
|
|||
ssl: Info &optional;
|
||||
};
|
||||
|
||||
redef record Info += {
|
||||
# Adding a string "token" to this set will cause the SSL script
|
||||
# to delay logging the record until either the token has been removed or
|
||||
# the record has been delayed for :bro:id:`SSL::max_log_delay`.
|
||||
delay_tokens: set[string] &optional;
|
||||
};
|
||||
|
||||
redef capture_filters += {
|
||||
["ssl"] = "tcp port 443",
|
||||
["nntps"] = "tcp port 563",
|
||||
["imap4-ssl"] = "tcp port 585",
|
||||
["sshell"] = "tcp port 614",
|
||||
["ldaps"] = "tcp port 636",
|
||||
["ftps-data"] = "tcp port 989",
|
||||
["ftps"] = "tcp port 990",
|
||||
["telnets"] = "tcp port 992",
|
||||
["imaps"] = "tcp port 993",
|
||||
["ircs"] = "tcp port 994",
|
||||
["pop3s"] = "tcp port 995",
|
||||
["xmpps"] = "tcp port 5223",
|
||||
};
|
||||
|
||||
const ports = {
|
||||
443/tcp, 563/tcp, 585/tcp, 614/tcp, 636/tcp,
|
||||
989/tcp, 990/tcp, 992/tcp, 993/tcp, 995/tcp, 5223/tcp
|
||||
} &redef;
|
||||
|
||||
redef likely_server_ports += { ports };
|
||||
|
||||
event bro_init() &priority=5
|
||||
{
|
||||
Log::create_stream(SSL::LOG, [$columns=Info, $ev=log_ssl]);
|
||||
Analyzer::register_for_ports(Analyzer::ANALYZER_SSL, ports);
|
||||
}
|
||||
|
||||
global analyzers = { ANALYZER_SSL };
|
||||
redef Protocols::analyzer_map += { ["SSL"] = analyzers };
|
||||
global ports = { 443/tcp, 563/tcp, 585/tcp, 614/tcp, 636/tcp,
|
||||
989/tcp, 990/tcp, 992/tcp, 993/tcp, 995/tcp, 5223/tcp };
|
||||
redef Protocols::common_ports += { ["SSL"] = ports };
|
||||
|
||||
|
||||
function set_session(c: connection)
|
||||
{
|
||||
|
@ -101,12 +129,44 @@ function set_session(c: connection)
|
|||
$client_cert_chain=vector()];
|
||||
}
|
||||
|
||||
function delay_log(info: Info, token: string)
|
||||
{
|
||||
if ( ! info?$delay_tokens )
|
||||
info$delay_tokens = set();
|
||||
add info$delay_tokens[token];
|
||||
}
|
||||
|
||||
function undelay_log(info: Info, token: string)
|
||||
{
|
||||
if ( info?$delay_tokens && token in info$delay_tokens )
|
||||
delete info$delay_tokens[token];
|
||||
}
|
||||
|
||||
function log_record(info: Info)
|
||||
{
|
||||
if ( ! info?$delay_tokens || |info$delay_tokens| == 0 )
|
||||
{
|
||||
Log::write(SSL::LOG, info);
|
||||
}
|
||||
else
|
||||
{
|
||||
when ( |info$delay_tokens| == 0 )
|
||||
{
|
||||
log_record(info);
|
||||
}
|
||||
timeout max_log_delay
|
||||
{
|
||||
Reporter::info(fmt("SSL delay tokens not released in time (%s tokens remaining)",
|
||||
|info$delay_tokens|));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finish(c: connection)
|
||||
{
|
||||
Log::write(SSL::LOG, c$ssl);
|
||||
log_record(c$ssl);
|
||||
if ( disable_analyzer_after_detection && c?$ssl && c$ssl?$analyzer_id )
|
||||
disable_analyzer(c$id, c$ssl$analyzer_id);
|
||||
delete c$ssl;
|
||||
}
|
||||
|
||||
event ssl_client_hello(c: connection, version: count, possible_ts: time, session_id: string, ciphers: count_set) &priority=5
|
||||
|
@ -194,14 +254,14 @@ event ssl_established(c: connection) &priority=-5
|
|||
finish(c);
|
||||
}
|
||||
|
||||
event protocol_confirmation(c: connection, atype: count, aid: count) &priority=5
|
||||
event protocol_confirmation(c: connection, atype: Analyzer::Tag, aid: count) &priority=5
|
||||
{
|
||||
# Check by checking for existence of c$ssl record.
|
||||
if ( c?$ssl && analyzer_name(atype) == "SSL" )
|
||||
if ( c?$ssl && atype == Analyzer::ANALYZER_SSL )
|
||||
c$ssl$analyzer_id = aid;
|
||||
}
|
||||
|
||||
event protocol_violation(c: connection, atype: count, aid: count,
|
||||
event protocol_violation(c: connection, atype: Analyzer::Tag, aid: count,
|
||||
reason: string) &priority=5
|
||||
{
|
||||
if ( c?$ssl )
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
##! Core script support for logging syslog messages. This script represents
|
||||
##! one syslog message as one logged record.
|
||||
|
||||
@load base/frameworks/protocols
|
||||
@load ./consts
|
||||
|
||||
module Syslog;
|
||||
|
@ -27,10 +26,10 @@ export {
|
|||
};
|
||||
}
|
||||
|
||||
global analyzers = { ANALYZER_SYSLOG_BINPAC };
|
||||
redef Protocols::analyzer_map += { ["SYSLOG"] = analyzers };
|
||||
global ports = { 514/udp };
|
||||
redef Protocols::common_ports += { ["SYSLOG"] = ports };
|
||||
redef capture_filters += { ["syslog"] = "port 514" };
|
||||
|
||||
const ports = { 514/udp };
|
||||
redef likely_server_ports += { ports };
|
||||
|
||||
redef record connection += {
|
||||
syslog: Info &optional;
|
||||
|
@ -39,6 +38,7 @@ redef record connection += {
|
|||
event bro_init() &priority=5
|
||||
{
|
||||
Log::create_stream(Syslog::LOG, [$columns=Info]);
|
||||
Analyzer::register_for_ports(Analyzer::ANALYZER_SYSLOG, ports);
|
||||
}
|
||||
|
||||
event syslog_message(c: connection, facility: count, severity: count, msg: string) &priority=5
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue