mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 06:38:20 +00:00
557 lines
13 KiB
Text
557 lines
13 KiB
Text
# $Id: smtp.bro 5230 2008-01-14 01:38:18Z vern $
|
|
|
|
@load conn
|
|
|
|
module SMTP;
|
|
|
|
export {
|
|
redef enum Notice += { HotEmailRecipient, };
|
|
|
|
const process_smtp_relay = F &redef;
|
|
|
|
const smtp_log = open_log_file("smtp") &redef;
|
|
|
|
# Used to detect relaying.
|
|
const local_mail_addr = /.*@.*lbl.gov/ &redef;
|
|
|
|
const hot_recipients = /@/ &redef;
|
|
|
|
const smtp_legal_cmds: set[string] = {
|
|
">", "EHLO", "HELO", "MAIL",
|
|
"RCPT", "DATA", ".", "QUIT",
|
|
"RSET", "VRFY", "EXPN", "HELP", "NOOP",
|
|
"SEND", "SOML", "SAML", "TURN",
|
|
"STARTTLS",
|
|
"BDAT",
|
|
"ETRN",
|
|
"AUTH",
|
|
"***",
|
|
} &redef;
|
|
|
|
const smtp_hot_cmds: table[string] of pattern = {
|
|
["MAIL"] = /.*<.*@.*:.*>.*/, # relay path
|
|
["RCPT"] = /.*<.*@.*:.*>.*/, # relay path
|
|
["VRFY"] = /.*/,
|
|
["EXPN"] = /.*/,
|
|
["TURN"] = /.*/,
|
|
} &redef;
|
|
|
|
const smtp_sensitive_cmds: set[string] = {
|
|
"VRFY", "EXPN", "TURN",
|
|
} &redef;
|
|
|
|
const smtp_expected_reply: set[string, count] = {
|
|
[">", 220],
|
|
["EHLO", 250],
|
|
["HELO", 250],
|
|
["MAIL", 250],
|
|
["RCPT", 250],
|
|
["RCPT", 554], # transaction failed
|
|
["QUIT", 221],
|
|
["DATA", 354],
|
|
[".", 250], # end of data
|
|
["RSET", 250],
|
|
["VRFY", 250],
|
|
["EXPN", 250],
|
|
["HELP", 250],
|
|
["HELP", 502], # help command not supported
|
|
["NOOP", 250],
|
|
["AUTH", 334], # two round authentication
|
|
["AUTH", 235], # one round authentication
|
|
["AUTH_ANSWER", 334], # multiple step authentication
|
|
["AUTH_ANSWER", 235], # authentication successful
|
|
["STARTTLS", 220], # Willing to do TLS
|
|
["TURN", 502], # TURN is expected to be rejected
|
|
};
|
|
|
|
type smtp_cmd_info: record {
|
|
cmd: string;
|
|
cmd_arg: string;
|
|
reply: count;
|
|
reply_arg: string;
|
|
cont_reply: bool;
|
|
log_reply: bool;
|
|
};
|
|
|
|
type smtp_cmd_info_list: table[count] of smtp_cmd_info;
|
|
|
|
type smtp_session_info: record {
|
|
id: count;
|
|
connection_id: conn_id;
|
|
external_orig: bool;
|
|
in_data: bool;
|
|
num_cmds: count;
|
|
num_replies: count;
|
|
cmds: smtp_cmd_info_list;
|
|
in_header: bool;
|
|
keep_current_header: bool; # hack till MIME rewriter ready
|
|
recipients: string;
|
|
subject: string;
|
|
content_hash: string;
|
|
num_lines_in_body: count;
|
|
# lines in RFC 822 body before MIME decoding
|
|
num_bytes_in_body: count;
|
|
# bytes in entity bodies after MIME decoding
|
|
content_gap: bool; # whether content gap in conversation
|
|
|
|
relay_1_rcpt: string; # external recipients
|
|
relay_2_from: count; # session id of same recipient
|
|
relay_2_to: count;
|
|
relay_3_from: count; # session id of same msg id
|
|
relay_3_to: count;
|
|
relay_4_from: count; # session id of same content hash
|
|
relay_4_to: count;
|
|
};
|
|
|
|
global smtp_sessions: table[conn_id] of smtp_session_info;
|
|
global smtp_session_id = 0;
|
|
|
|
global new_smtp_session: function(c: connection);
|
|
}
|
|
|
|
redef capture_filters += { ["smtp"] = "tcp port smtp or tcp port 587" };
|
|
|
|
# DPM configuration.
|
|
global smtp_ports = { 25/tcp, 587/tcp } &redef;
|
|
redef dpd_config += { [ANALYZER_SMTP] = [$ports = smtp_ports] };
|
|
|
|
function is_smtp_connection(c: connection): bool
|
|
{
|
|
return c$id$resp_p == smtp;
|
|
}
|
|
|
|
event bro_init()
|
|
{
|
|
have_SMTP = T;
|
|
}
|
|
|
|
global add_to_smtp_relay_table: function(session: smtp_session_info);
|
|
|
|
function new_smtp_command(session: smtp_session_info, cmd: string, arg: string)
|
|
{
|
|
++session$num_cmds;
|
|
|
|
local cmd_info: smtp_cmd_info;
|
|
cmd_info$cmd = cmd;
|
|
cmd_info$cmd_arg = arg;
|
|
cmd_info$reply = 0;
|
|
cmd_info$reply_arg = "";
|
|
cmd_info$cont_reply = F;
|
|
cmd_info$log_reply = F;
|
|
|
|
session$cmds[session$num_cmds] = cmd_info;
|
|
}
|
|
|
|
function new_smtp_session(c: connection)
|
|
{
|
|
local session = c$id;
|
|
local new_id = ++smtp_session_id;
|
|
|
|
local info: smtp_session_info;
|
|
local cmds: smtp_cmd_info_list;
|
|
|
|
info$id = new_id;
|
|
info$connection_id = session;
|
|
info$in_data = F;
|
|
info$num_cmds = 0;
|
|
info$num_replies = 0;
|
|
info$cmds = cmds;
|
|
info$in_header = F;
|
|
info$keep_current_header = T;
|
|
info$external_orig = !is_local_addr(session$orig_h);
|
|
|
|
info$subject = "";
|
|
info$recipients = "";
|
|
info$content_hash = "";
|
|
info$num_lines_in_body = info$num_bytes_in_body = 0;
|
|
info$content_gap = F;
|
|
|
|
info$relay_1_rcpt = "";
|
|
info$relay_2_from = info$relay_2_to = info$relay_3_from =
|
|
info$relay_3_to = info$relay_4_from = info$relay_4_to = 0;
|
|
|
|
new_smtp_command(info, ">", "<connection>");
|
|
|
|
smtp_sessions[session] = info;
|
|
append_addl(c, fmt("#%s", prefixed_id(new_id)));
|
|
|
|
print smtp_log, fmt("%.6f #%s %s start %s", c$start_time,
|
|
prefixed_id(new_id), id_string(session), info$external_orig ?
|
|
"external" : "internal" );
|
|
}
|
|
|
|
function smtp_message(session: smtp_session_info, msg: string)
|
|
{
|
|
print smtp_log, fmt("%.6f #%s %s",
|
|
network_time(), prefixed_id(session$id), msg);
|
|
}
|
|
|
|
function smtp_log_msg(session: smtp_session_info, is_orig: bool, msg: string)
|
|
{
|
|
print smtp_log, fmt("%.6f #%s %s: %s",
|
|
network_time(),
|
|
prefixed_id(session$id),
|
|
directed_id_string(session$connection_id, is_orig),
|
|
msg);
|
|
}
|
|
|
|
function smtp_log_reject_recipient(session: smtp_session_info, rcpt: string)
|
|
{
|
|
if ( rcpt == "" )
|
|
rcpt = "<none>";
|
|
|
|
smtp_message(session, fmt("Recipient addresses rejected: %s", rcpt));
|
|
}
|
|
|
|
function smtp_log_command(session: smtp_session_info, is_orig: bool,
|
|
msg: string, cmd_info: smtp_cmd_info)
|
|
{
|
|
smtp_log_msg(session, is_orig, fmt("%s: %s(%s)",
|
|
msg, cmd_info$cmd, cmd_info$cmd_arg));
|
|
}
|
|
|
|
function smtp_log_reply(session: smtp_session_info, is_orig: bool,
|
|
msg: string, cmd_info: smtp_cmd_info)
|
|
{
|
|
smtp_log_msg(session, is_orig, fmt("%s: %s(%s) --> %d(%s)",
|
|
msg,
|
|
cmd_info$cmd, cmd_info$cmd_arg,
|
|
cmd_info$reply, cmd_info$reply_arg));
|
|
}
|
|
|
|
event smtp_request(c: connection, is_orig: bool, command: string, arg: string)
|
|
{
|
|
local id = c$id;
|
|
|
|
if ( id !in smtp_sessions )
|
|
new_smtp_session(c);
|
|
|
|
local session = smtp_sessions[id];
|
|
new_smtp_command(session, command, arg);
|
|
local cmd_info = session$cmds[session$num_cmds];
|
|
|
|
# Store the command in session record.
|
|
local log_this_cmd = F;
|
|
|
|
if ( command in smtp_hot_cmds && arg == smtp_hot_cmds[command] )
|
|
{
|
|
log_this_cmd = T;
|
|
cmd_info$log_reply = T;
|
|
}
|
|
|
|
if ( command in smtp_sensitive_cmds )
|
|
{
|
|
log_this_cmd = T;
|
|
cmd_info$log_reply = T;
|
|
}
|
|
|
|
if ( log_this_cmd )
|
|
smtp_log_command(session, is_orig, "unusual command", cmd_info);
|
|
|
|
if ( command == "DATA" )
|
|
{
|
|
session$in_data = T;
|
|
session$in_header = T;
|
|
}
|
|
|
|
else if ( command == "." )
|
|
session$in_data = F;
|
|
}
|
|
|
|
function check_cmd_info(session: smtp_session_info): bool
|
|
{
|
|
if ( session$num_replies == 0 )
|
|
return T;
|
|
|
|
if ( session$num_replies <= session$num_cmds &&
|
|
session$num_replies in session$cmds )
|
|
return T;
|
|
|
|
smtp_message(session, fmt("error: invalid num_replies: %d (num_cmds = %d)",
|
|
session$num_replies, session$num_cmds));
|
|
return F;
|
|
}
|
|
|
|
function smtp_command_mail(session: smtp_session_info, cmd_info: smtp_cmd_info)
|
|
{
|
|
local tokens = split(cmd_info$cmd_arg, /(<|:|>)*/);
|
|
|
|
local i = 0;
|
|
for ( i in tokens )
|
|
smtp_log_msg(session, T, fmt("%d: \"%s\"", i, tokens[i]));
|
|
}
|
|
|
|
function extract_recipient(session: smtp_session_info, rcpt_cmd_arg: string): string
|
|
{
|
|
local pair: string_array;
|
|
local s: string;
|
|
|
|
s = rcpt_cmd_arg;
|
|
|
|
pair = split1(s, /<( |\t)*/);
|
|
if ( length(pair) != 2 )
|
|
{
|
|
smtp_message(session, fmt("error: '<' not found in argument to RCPT: %s",
|
|
rcpt_cmd_arg));
|
|
return "";
|
|
}
|
|
|
|
s = pair[2];
|
|
# smtp_message(session, fmt("%s<%s", pair[1], pair[2]));
|
|
|
|
pair = split1(s, /( |\t)*>/);
|
|
if ( length(pair) != 2 )
|
|
{
|
|
smtp_message(session, fmt("error: '>' not found in argument to RCPT: %s",
|
|
rcpt_cmd_arg));
|
|
return "";
|
|
}
|
|
|
|
s = pair[1];
|
|
# smtp_message(session, fmt("%s>%s", pair[1], pair[2]));
|
|
|
|
pair = split1(s, /:/);
|
|
if ( length(pair) == 2 )
|
|
{
|
|
smtp_message(session, fmt("RCPT address is source route path: %s",
|
|
rcpt_cmd_arg));
|
|
s = pair[2];
|
|
}
|
|
|
|
# Actually the local part of an address might be case-sensitive,
|
|
# but in most cases it is not.
|
|
|
|
s = to_lower(s);
|
|
|
|
return s;
|
|
}
|
|
|
|
global check_relay_1: function(session: smtp_session_info, rcpt: string);
|
|
global check_relay_2: function(session: smtp_session_info, rcpt: string);
|
|
|
|
function smtp_command_rcpt(c: connection, session: smtp_session_info,
|
|
cmd_info: smtp_cmd_info)
|
|
{
|
|
local rcpt = extract_recipient(session, cmd_info$cmd_arg);
|
|
|
|
if ( cmd_info$reply == 554 )
|
|
smtp_log_reject_recipient(session, rcpt);
|
|
|
|
else if ( rcpt != "" )
|
|
{
|
|
smtp_message(session, fmt("recipient: <%s>", rcpt));
|
|
|
|
if ( session$recipients != "" )
|
|
session$recipients = cat(session$recipients, ",");
|
|
|
|
session$recipients = cat(session$recipients, rcpt);
|
|
|
|
if ( process_smtp_relay )
|
|
{
|
|
check_relay_1(session, rcpt);
|
|
check_relay_2(session, rcpt);
|
|
}
|
|
|
|
if ( rcpt == hot_recipients )
|
|
{
|
|
local src = session$connection_id$orig_h;
|
|
local dst = session$connection_id$resp_h;
|
|
|
|
NOTICE([$note=HotEmailRecipient, $src=src, $conn=c,
|
|
$user=rcpt,
|
|
$msg=fmt("hot email recipient %s -> %s@%s",
|
|
src, rcpt, dst)]);
|
|
}
|
|
}
|
|
}
|
|
|
|
event smtp_reply(c: connection, is_orig: bool, code: count, cmd: string,
|
|
msg: string, cont_resp: bool)
|
|
{
|
|
local id = c$id;
|
|
|
|
if ( id !in smtp_sessions )
|
|
new_smtp_session(c);
|
|
|
|
local session = smtp_sessions[id];
|
|
local new_reply = F;
|
|
|
|
# Check entry before indexing.
|
|
if ( ! check_cmd_info(session) )
|
|
return;
|
|
|
|
if ( session$num_replies == 0 ||
|
|
! session$cmds[session$num_replies]$cont_reply )
|
|
{
|
|
++session$num_replies;
|
|
if ( session$num_replies !in session$cmds )
|
|
{
|
|
smtp_message(session, fmt("error: unmatched reply: %d %s (%s)",
|
|
code, msg, cmd));
|
|
return;
|
|
}
|
|
|
|
new_reply = T;
|
|
}
|
|
|
|
if ( ! check_cmd_info(session) )
|
|
return;
|
|
|
|
local cmd_info = session$cmds[session$num_replies];
|
|
|
|
if ( cmd_info$cmd != cmd )
|
|
{
|
|
smtp_message(session,
|
|
fmt("error: command mismatch: %s(%d) %s(%d), %s (%d %s)",
|
|
cmd_info$cmd, session$num_replies,
|
|
session$cmds[session$num_cmds], session$num_cmds,
|
|
cmd, code, msg));
|
|
return;
|
|
}
|
|
|
|
cmd_info$reply = code;
|
|
if ( new_reply )
|
|
cmd_info$reply_arg = msg;
|
|
else
|
|
cmd_info$reply_arg = cat(cmd_info$reply_arg, "\r\n", msg);
|
|
|
|
cmd_info$cont_reply = cont_resp;
|
|
|
|
local log_this_reply = cmd_info$log_reply;
|
|
|
|
if ( [cmd, code] !in smtp_expected_reply )
|
|
log_this_reply = T;
|
|
|
|
if ( log_this_reply && ! cont_resp )
|
|
smtp_log_reply(session, is_orig, "unusual command/reply", cmd_info);
|
|
|
|
# else if ( cmd == "MAIL" && code == 250 )
|
|
# smtp_command_mail(session, cmd_info);
|
|
|
|
else if ( cmd == "RCPT" )
|
|
{
|
|
if ( code == 250 || code == 554 )
|
|
smtp_command_rcpt(c, session, cmd_info);
|
|
}
|
|
|
|
else if ( cmd == "STARTTLS" && code == 220 )
|
|
{ # it'll now go encrypted - no more we can do.
|
|
skip_further_processing(c$id);
|
|
smtp_message(session, cmd);
|
|
}
|
|
}
|
|
|
|
function reset_on_gap(session: smtp_session_info)
|
|
{
|
|
local i: count;
|
|
|
|
clear_table(session$cmds);
|
|
|
|
session$num_cmds = session$num_replies = 0;
|
|
session$in_data = F;
|
|
}
|
|
|
|
event smtp_unexpected(c: connection, is_orig: bool, msg: string, detail: string)
|
|
{
|
|
local id = c$id;
|
|
|
|
if ( id !in smtp_sessions )
|
|
new_smtp_session(c);
|
|
|
|
local session = smtp_sessions[id];
|
|
|
|
smtp_log_msg(session, is_orig, fmt("unexpected: %s: %s", msg, detail));
|
|
}
|
|
|
|
function clear_smtp_session(session: smtp_session_info)
|
|
{
|
|
clear_table(session$cmds);
|
|
}
|
|
|
|
event content_gap(c: connection, is_orig: bool, seq: count, length: count)
|
|
{
|
|
if ( is_smtp_connection(c) )
|
|
{
|
|
local id = c$id;
|
|
if ( id !in smtp_sessions )
|
|
new_smtp_session(c);
|
|
local session = smtp_sessions[id];
|
|
session$content_gap = T;
|
|
reset_on_gap(session);
|
|
}
|
|
}
|
|
|
|
event connection_finished(c: connection)
|
|
{
|
|
local id = c$id;
|
|
if ( id in smtp_sessions )
|
|
{
|
|
local session = smtp_sessions[id];
|
|
smtp_message(session, "finish");
|
|
clear_smtp_session(session);
|
|
delete smtp_sessions[id];
|
|
}
|
|
}
|
|
|
|
event connection_state_remove(c: connection)
|
|
{
|
|
local id = c$id;
|
|
if ( id in smtp_sessions )
|
|
{
|
|
local session = smtp_sessions[id];
|
|
smtp_message(session, "state remove");
|
|
clear_smtp_session(session);
|
|
delete smtp_sessions[id];
|
|
}
|
|
}
|
|
|
|
global rewrite_smtp_header_line:
|
|
function(c: connection, is_orig: bool,
|
|
session: smtp_session_info, line: string);
|
|
|
|
function smtp_header_line(c: connection, is_orig: bool,
|
|
session: smtp_session_info, line: string)
|
|
{
|
|
if ( rewriting_smtp_trace )
|
|
rewrite_smtp_header_line(c, is_orig, session, line);
|
|
}
|
|
|
|
function smtp_body_line(c: connection, is_orig: bool,
|
|
session: smtp_session_info, line: string)
|
|
{
|
|
++session$num_lines_in_body;
|
|
session$num_bytes_in_body =
|
|
session$num_bytes_in_body + byte_len(line) + 2; # including CRLF
|
|
}
|
|
|
|
event smtp_data(c: connection, is_orig: bool, data: string)
|
|
{
|
|
local id = c$id;
|
|
if ( id in smtp_sessions )
|
|
{
|
|
local session = smtp_sessions[id];
|
|
# smtp_log_msg(session, is_orig, fmt("data: %s", data));
|
|
if ( session$in_header )
|
|
{
|
|
if ( data == "" )
|
|
{
|
|
session$in_header = F;
|
|
skip_smtp_data(c);
|
|
}
|
|
else
|
|
{
|
|
smtp_header_line(c, is_orig, session, data);
|
|
# smtp_log_msg(session, T, fmt("header: %s", data));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
# smtp_body_line(c, is_orig, session, data);
|
|
}
|
|
}
|
|
}
|
|
|
|
event bro_done()
|
|
{
|
|
clear_table(smtp_sessions);
|
|
}
|