mirror of
https://github.com/zeek/zeek.git
synced 2025-10-08 17:48:21 +00:00
Merge commit '03044c329e
' into topic/policy-scripts-new
* commit '03044c329e
':
Initial movement towards rewritten ftp.bro script.
This commit is contained in:
commit
cb4ca01c22
5 changed files with 805 additions and 1056 deletions
|
@ -1,188 +0,0 @@
|
||||||
# $Id: ftp-cmd-arg.bro 416 2004-09-17 03:52:28Z vern $
|
|
||||||
|
|
||||||
# For debugging purpose only
|
|
||||||
# global ftp_cmd_reply_log = open_log_file("ftp-cmd-arg") &redef;
|
|
||||||
|
|
||||||
const ftp_cmd_reply_code: set[string, count] = {
|
|
||||||
# According to RFC 959
|
|
||||||
["<init>", [120, 220, 421]],
|
|
||||||
["USER", [230, 530, 500, 501, 421, 331, 332]],
|
|
||||||
["PASS", [230, 202, 530, 500, 501, 503, 421, 332]],
|
|
||||||
["ACCT", [230, 202, 530, 500, 501, 503, 421]],
|
|
||||||
["CWD", [250, 500, 501, 502, 421, 530, 550]],
|
|
||||||
["CDUP", [200, 500, 501, 502, 421, 530, 550]],
|
|
||||||
["SMNT", [202, 250, 500, 501, 502, 421, 530, 550]],
|
|
||||||
["REIN", [120, 220, 421, 500, 502]],
|
|
||||||
["QUIT", [221, 500]],
|
|
||||||
["PORT", [200, 500, 501, 421, 530]],
|
|
||||||
["PASV", [227, 500, 501, 502, 421, 530]],
|
|
||||||
["MODE", [200, 500, 501, 504, 421, 530]],
|
|
||||||
["TYPE", [200, 500, 501, 504, 421, 530]],
|
|
||||||
["STRU", [200, 500, 501, 504, 421, 530]],
|
|
||||||
["ALLO", [200, 202, 500, 501, 504, 421, 530]],
|
|
||||||
["REST", [500, 501, 502, 421, 530, 350]],
|
|
||||||
["STOR", [125, 150, 110, 226, 250, 425, 426, 451, 551, 552, 532, 450, 452, 553, 500, 501, 421, 530]],
|
|
||||||
["STOU", [125, 150, 110, 226, 250, 425, 426, 451, 551, 552, 532, 450, 452, 553, 500, 501, 421, 530]],
|
|
||||||
["RETR", [125, 150, 110, 226, 250, 425, 426, 451, 450, 550, 500, 501, 421, 530]],
|
|
||||||
["LIST", [125, 150, 226, 250, 425, 426, 451, 450, 500, 501, 502, 421, 530]],
|
|
||||||
["NLST", [125, 150, 226, 250, 425, 426, 451, 450, 500, 501, 502, 421, 530]],
|
|
||||||
["APPE", [125, 150, 226, 250, 425, 426, 451, 551, 552, 532, 450, 550, 452, 553, 500, 501, 502, 421, 530]],
|
|
||||||
["RNFR", [450, 550, 500, 501, 502, 421, 530, 350]],
|
|
||||||
["RNTO", [250, 532, 553, 500, 501, 502, 503, 421, 530]],
|
|
||||||
["DELE", [250, 450, 550, 500, 501, 502, 421, 530]],
|
|
||||||
["RMD", [250, 500, 501, 502, 421, 530, 550]],
|
|
||||||
["MKD", [257, 500, 501, 502, 421, 530, 550]],
|
|
||||||
["PWD", [257, 500, 501, 502, 421, 550]],
|
|
||||||
["ABOR", [225, 226, 500, 501, 502, 421]],
|
|
||||||
["SYST", [215, 500, 501, 502, 421]],
|
|
||||||
["STAT", [211, 212, 213, 450, 500, 501, 502, 421, 530]],
|
|
||||||
["HELP", [211, 214, 500, 501, 502, 421]],
|
|
||||||
["SITE", [200, 202, 500, 501, 530]],
|
|
||||||
["NOOP", [200, 500, 421]],
|
|
||||||
|
|
||||||
# Extensions
|
|
||||||
|
|
||||||
# ["SIZE", [213, 550]],
|
|
||||||
# ["SITE", 214],
|
|
||||||
# ["MDTM", 213],
|
|
||||||
# ["EPSV", 500],
|
|
||||||
# ["FEAT", 500],
|
|
||||||
# ["OPTS", 500],
|
|
||||||
|
|
||||||
# ["CDUP", 250],
|
|
||||||
# ["CLNT", 200],
|
|
||||||
# ["CLNT", 500],
|
|
||||||
# ["EPRT", 500],
|
|
||||||
|
|
||||||
# ["FEAT", 211],
|
|
||||||
# ["HELP", 200],
|
|
||||||
# ["LIST", 550],
|
|
||||||
# ["LPRT", 500],
|
|
||||||
# ["MACB", 500],
|
|
||||||
# ["MDTM", 212],
|
|
||||||
# ["MDTM", 500],
|
|
||||||
# ["MDTM", 501],
|
|
||||||
# ["MDTM", 550],
|
|
||||||
# ["MLST", 500],
|
|
||||||
# ["MLST", 550],
|
|
||||||
# ["MODE", 502],
|
|
||||||
# ["NLST", 550],
|
|
||||||
# ["OPTS", 501],
|
|
||||||
# ["REST", 200],
|
|
||||||
# ["SITE", 502],
|
|
||||||
# ["SIZE", 500],
|
|
||||||
# ["STOR", 550],
|
|
||||||
# ["SYST", 530],
|
|
||||||
|
|
||||||
["<init>", 0], # unexpected command-reply pair
|
|
||||||
["<missing>", 0], # unexpected command-reply pair
|
|
||||||
["QUIT", 0], # unexpected command-reply pair
|
|
||||||
} &redef;
|
|
||||||
|
|
||||||
global ftp_unexpected_cmd_reply: set[string];
|
|
||||||
|
|
||||||
type ftp_cmd_arg: record {
|
|
||||||
cmd: string;
|
|
||||||
arg: string;
|
|
||||||
anonymized_cmd: string;
|
|
||||||
anonymized_arg: string; # anonymized arg
|
|
||||||
seq: count; # seq number
|
|
||||||
rewrite_slot: count;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ftp_pending_cmds: record {
|
|
||||||
seq: count;
|
|
||||||
cmds: table[count] of ftp_cmd_arg;
|
|
||||||
};
|
|
||||||
|
|
||||||
function init_ftp_pending_cmds(): ftp_pending_cmds
|
|
||||||
{
|
|
||||||
local cmds: table[count] of ftp_cmd_arg;
|
|
||||||
return [$seq = 1, $cmds = cmds];
|
|
||||||
}
|
|
||||||
|
|
||||||
function ftp_cmd_pending(s: ftp_pending_cmds): bool
|
|
||||||
{
|
|
||||||
return length(s$cmds) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function add_to_ftp_pending_cmds(s: ftp_pending_cmds, cmd: string, arg: string)
|
|
||||||
: ftp_cmd_arg
|
|
||||||
{
|
|
||||||
local ca = [$cmd = cmd, $arg = arg, $anonymized_cmd = "<TBD!>",
|
|
||||||
$anonymized_arg = "<TBD!>", $seq = s$seq,
|
|
||||||
$rewrite_slot = 0];
|
|
||||||
|
|
||||||
s$cmds[s$seq] = ca;
|
|
||||||
++s$seq;
|
|
||||||
|
|
||||||
return ca;
|
|
||||||
}
|
|
||||||
|
|
||||||
function find_ftp_pending_cmd(s: ftp_pending_cmds, reply_code: count, reply_msg: string): ftp_cmd_arg
|
|
||||||
{
|
|
||||||
if ( length(s$cmds) == 0 )
|
|
||||||
{
|
|
||||||
return [$cmd = "<unknown>", $arg = "",
|
|
||||||
$anonymized_cmd = "<TBD!>", $anonymized_arg = "<TBD!>",
|
|
||||||
$seq = 0, $rewrite_slot = 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
local best_match: ftp_cmd_arg;
|
|
||||||
local best_score: int = -1;
|
|
||||||
|
|
||||||
for ( seq in s$cmds )
|
|
||||||
{
|
|
||||||
local ca = s$cmds[seq];
|
|
||||||
local score: int = 0;
|
|
||||||
# if the command is compatible with the reply code
|
|
||||||
# code 500 (syntax error) is compatible with all commands
|
|
||||||
if ( reply_code == 500 || [ca$cmd, reply_code] in ftp_cmd_reply_code )
|
|
||||||
score = score + 100;
|
|
||||||
# if the command or the command arg appears in the reply message
|
|
||||||
if ( strstr(reply_msg, ca$cmd) > 0 )
|
|
||||||
score = score + 20;
|
|
||||||
if ( strstr(reply_msg, ca$cmd) > 0 )
|
|
||||||
score = score + 10;
|
|
||||||
if ( score > best_score ||
|
|
||||||
( score == best_score && ca$seq < best_match$seq ) ) # break tie with sequence number
|
|
||||||
{
|
|
||||||
best_score = score;
|
|
||||||
best_match = ca;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( [best_match$cmd, reply_code] !in ftp_cmd_reply_code )
|
|
||||||
{
|
|
||||||
local annotation = "";
|
|
||||||
if ( length(s$cmds) == 1 )
|
|
||||||
annotation = "for sure";
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for ( i in s$cmds )
|
|
||||||
annotation = cat(annotation, " ", s$cmds[i]$cmd);
|
|
||||||
annotation = cat("candidates:", annotation);
|
|
||||||
}
|
|
||||||
# add ftp_unexpected_cmd_reply[fmt("[\"%s\", %d], # %s",
|
|
||||||
# best_match$cmd, reply_code, annotation)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return best_match;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pop_from_ftp_pending_cmd(s: ftp_pending_cmds, ca: ftp_cmd_arg): bool
|
|
||||||
{
|
|
||||||
if ( ca$seq in s$cmds )
|
|
||||||
{
|
|
||||||
delete s$cmds[ca$seq];
|
|
||||||
return T;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return F;
|
|
||||||
}
|
|
||||||
|
|
||||||
event bro_done()
|
|
||||||
{
|
|
||||||
# for ( cmd_reply in ftp_unexpected_cmd_reply )
|
|
||||||
# print ftp_cmd_reply_log, fmt(" %s", cmd_reply);
|
|
||||||
}
|
|
|
@ -1,868 +0,0 @@
|
||||||
# $Id: ftp.bro 6726 2009-06-07 22:09:55Z vern $
|
|
||||||
|
|
||||||
@load notice
|
|
||||||
@load conn
|
|
||||||
@load scan
|
|
||||||
@load hot-ids
|
|
||||||
@load terminate-connection
|
|
||||||
|
|
||||||
@load ftp-cmd-arg
|
|
||||||
|
|
||||||
module FTP;
|
|
||||||
|
|
||||||
export {
|
|
||||||
# Indexed by source & destination addresses and the id.
|
|
||||||
const skip_hot: set[addr, addr, string] &redef;
|
|
||||||
|
|
||||||
# see: http://packetstormsecurity.org/UNIX/penetration/rootkits/index4.html
|
|
||||||
# for current list of rootkits to include here
|
|
||||||
|
|
||||||
const hot_files =
|
|
||||||
/.*(etc\/|master\.)?(passwd|shadow|s?pwd\.db)/
|
|
||||||
| /.*snoop\.(tar|tgz).*/
|
|
||||||
| /.*bnc\.(tar|tgz).*/
|
|
||||||
| /.*datapipe.*/
|
|
||||||
| /.*ADMw0rm.*/
|
|
||||||
| /.*newnick.*/
|
|
||||||
| /.*sniffit.*/
|
|
||||||
| /.*neet\.(tar|tgz).*/
|
|
||||||
| /.*\.\.\..*/
|
|
||||||
| /.*ftpscan.txt.*/
|
|
||||||
| /.*jcc.pdf.*/
|
|
||||||
| /.*\.[Ff]rom.*/
|
|
||||||
| /.*sshd\.(tar|tgz).*/
|
|
||||||
| /.*\/rk7.*/
|
|
||||||
| /.*rk7\..*/
|
|
||||||
| /.*[aA][dD][oO][rR][eE][bB][sS][dD].*/
|
|
||||||
| /.*[tT][aA][gG][gG][eE][dD].*/
|
|
||||||
| /.*shv4\.(tar|tgz).*/
|
|
||||||
| /.*lrk\.(tar|tgz).*/
|
|
||||||
| /.*lyceum\.(tar|tgz).*/
|
|
||||||
| /.*maxty\.(tar|tgz).*/
|
|
||||||
| /.*rootII\.(tar|tgz).*/
|
|
||||||
| /.*invader\.(tar|tgz).*/
|
|
||||||
&redef;
|
|
||||||
|
|
||||||
const hot_guest_files =
|
|
||||||
/.*\.rhosts/
|
|
||||||
| /.*\.forward/
|
|
||||||
&redef;
|
|
||||||
|
|
||||||
const hot_cmds: table[string] of pattern = {
|
|
||||||
["SITE"] = /[Ee][Xx][Ee][Cc].*/,
|
|
||||||
} &redef;
|
|
||||||
|
|
||||||
const excessive_filename_len = 250 &redef;
|
|
||||||
const excessive_filename_trunc_len = 32 &redef;
|
|
||||||
|
|
||||||
const guest_ids = { "anonymous", "ftp", "guest", } &redef;
|
|
||||||
|
|
||||||
# Invalid PORT/PASV directives that exactly match the following
|
|
||||||
# don't generate notice's.
|
|
||||||
const ignore_invalid_PORT =
|
|
||||||
/,0,0/ # these are common, dunno why
|
|
||||||
&redef;
|
|
||||||
|
|
||||||
# Some servers generate particular privileged PASV ports for benign
|
|
||||||
# reasons (presumably to tunnel through firewalls, sigh).
|
|
||||||
const ignore_privileged_PASVs = { ssh, } &redef;
|
|
||||||
|
|
||||||
# Pairs of IP addresses for which we shouldn't bother logging if one
|
|
||||||
# of them is used in lieu of the other in a PORT or PASV directive.
|
|
||||||
|
|
||||||
const skip_unexpected: set[addr] = {
|
|
||||||
15.253.0.10, 15.253.48.10, 15.254.56.2, # hp.com
|
|
||||||
gvaona1.cns.hp.com,
|
|
||||||
} &redef;
|
|
||||||
|
|
||||||
const skip_unexpected_net: set[addr] &redef;
|
|
||||||
|
|
||||||
const log_file = open_log_file("ftp") &redef;
|
|
||||||
|
|
||||||
redef enum Notice += {
|
|
||||||
FTP_UnexpectedConn, # FTP data transfer from unexpected src
|
|
||||||
FTP_ExcessiveFilename, # very long filename seen
|
|
||||||
FTP_PrivPort, # privileged port used in PORT/PASV;
|
|
||||||
# $sub says which
|
|
||||||
FTP_BadPort, # bad format in PORT/PASV;
|
|
||||||
# $sub says which
|
|
||||||
FTP_Sensitive, # sensitive connection -
|
|
||||||
# not more specific
|
|
||||||
FTP_SiteExecAttack, # specific "site exec" attack seen
|
|
||||||
};
|
|
||||||
|
|
||||||
type ftp_session_info: record {
|
|
||||||
id: count;
|
|
||||||
connection_id: conn_id;
|
|
||||||
user: string;
|
|
||||||
anonymized_user: string;
|
|
||||||
anonymous_login: bool;
|
|
||||||
|
|
||||||
request: string; # pending request or requests
|
|
||||||
num_requests: count; # count of pending requests
|
|
||||||
request_t: time; # time of request
|
|
||||||
log_if_not_denied: bool; # log unless code 530 on reply
|
|
||||||
log_if_not_unavail: bool; # log unless code 550 on reply
|
|
||||||
log_it: bool; # if true, log the request(s)
|
|
||||||
|
|
||||||
reply_code: count; # the most recent reply code
|
|
||||||
cwd: string; # current working directory
|
|
||||||
|
|
||||||
pending_requests: ftp_pending_cmds; # pending requests
|
|
||||||
delayed_request_rewrite: table[count] of ftp_cmd_arg;
|
|
||||||
|
|
||||||
expected: set[addr, port]; # data connections we expect
|
|
||||||
};
|
|
||||||
|
|
||||||
type ftp_expected_conn: record {
|
|
||||||
host: addr;
|
|
||||||
session: ftp_session_info;
|
|
||||||
};
|
|
||||||
|
|
||||||
global ftp_sessions: table[conn_id] of ftp_session_info &persistent;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
redef capture_filters += { ["ftp"] = "port ftp" };
|
|
||||||
|
|
||||||
# DPM configuration.
|
|
||||||
global ftp_ports = { 21/tcp } &redef;
|
|
||||||
redef dpd_config += { [ANALYZER_FTP] = [$ports = ftp_ports] };
|
|
||||||
|
|
||||||
function is_ftp_conn(c: connection): bool
|
|
||||||
{
|
|
||||||
return c$id$resp_p == ftp;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ftp_reply_code: record {
|
|
||||||
x: count; # high-order (3rd digit)
|
|
||||||
y: count; # middle (2nd) digit
|
|
||||||
z: count; # bottom digit
|
|
||||||
};
|
|
||||||
|
|
||||||
global ftp_session_id = 0;
|
|
||||||
|
|
||||||
# Indexed by the responder pair, yielding the address expected to connect to it.
|
|
||||||
global ftp_data_expected: table[addr, port] of ftp_expected_conn &persistent &create_expire = 1 min;
|
|
||||||
|
|
||||||
const ftp_init_dir: table[addr, string] of string = {
|
|
||||||
[131.243.1.10, "anonymous"] = "/",
|
|
||||||
} &default = "<unknown>/";
|
|
||||||
|
|
||||||
const ftp_file_cmds = {
|
|
||||||
"APPE", "CWD", "DELE", "MKD", "RETR", "RMD", "RNFR", "RNTO",
|
|
||||||
"STOR", "STOU",
|
|
||||||
};
|
|
||||||
|
|
||||||
const ftp_absolute_path_pat = /(\/|[A-Za-z]:[\\\/]).*/;
|
|
||||||
|
|
||||||
const ftp_dir_operation = {
|
|
||||||
["CWD", 250],
|
|
||||||
["CDUP", 200], # typo in RFC?
|
|
||||||
["CDUP", 250], # as found in traces
|
|
||||||
["PWD", 257],
|
|
||||||
["XPWD", 257],
|
|
||||||
};
|
|
||||||
|
|
||||||
const ftp_skip_replies = {
|
|
||||||
150, # "status okay - about to open connection"
|
|
||||||
331 # "user name okay, need password"
|
|
||||||
};
|
|
||||||
|
|
||||||
const ftp_replies: table[count] of string = {
|
|
||||||
[150] = "ok",
|
|
||||||
[200] = "ok",
|
|
||||||
[220] = "ready for new user",
|
|
||||||
[221] = "closed",
|
|
||||||
[226] = "complete",
|
|
||||||
[230] = "logged in",
|
|
||||||
[250] = "ok",
|
|
||||||
[257] = "done",
|
|
||||||
[331] = "id ok",
|
|
||||||
[500] = "syntax error",
|
|
||||||
[530] = "denied",
|
|
||||||
[550] = "unavail",
|
|
||||||
};
|
|
||||||
|
|
||||||
const ftp_other_replies = { ftp_replies };
|
|
||||||
|
|
||||||
const ftp_all_cmds: set[string] = {
|
|
||||||
"<init>", "<missing>",
|
|
||||||
"USER", "PASS", "ACCT",
|
|
||||||
"CWD", "CDUP", "SMNT",
|
|
||||||
"REIN", "QUIT",
|
|
||||||
"PORT", "PASV", "MODE", "TYPE", "STRU",
|
|
||||||
"ALLO", "REST", "STOR", "STOU", "RETR", "LIST", "NLST", "APPE",
|
|
||||||
"RNFR", "RNTO", "DELE", "RMD", "MKD", "PWD", "ABOR",
|
|
||||||
"SYST", "STAT", "HELP",
|
|
||||||
"SITE", "NOOP",
|
|
||||||
|
|
||||||
# FTP extensions
|
|
||||||
"SIZE", "MDTM", "MLST", "MLSD",
|
|
||||||
"EPRT", "EPSV",
|
|
||||||
};
|
|
||||||
|
|
||||||
const ftp_tested_cmds: set[string] = {};
|
|
||||||
const ftp_untested_cmds: set[string] = { ftp_all_cmds };
|
|
||||||
|
|
||||||
global ftp_first_seen_cmds: set[string];
|
|
||||||
global ftp_unlisted_cmds: set[string];
|
|
||||||
|
|
||||||
# const ftp_state_diagram: table[string] of count = {
|
|
||||||
# ["ABOR", "ALLO", "DELE", "CWD", "CDUP",
|
|
||||||
# "SMNT", "HELP", "MODE", "NOOP", "PASV",
|
|
||||||
# "QUIT", "SITE", "PORT", "SYST", "STAT",
|
|
||||||
# "RMD", "MKD", "PWD", "STRU", "TYPE"] = 1,
|
|
||||||
# ["APPE", "LIST", "NLST", "RETR", "STOR", "STOU"] = 2,
|
|
||||||
# ["REIN"] = 3,
|
|
||||||
# ["RNFR", "RNTO"] = 4,
|
|
||||||
# };
|
|
||||||
|
|
||||||
|
|
||||||
function parse_ftp_reply_code(code: count): ftp_reply_code
|
|
||||||
{
|
|
||||||
local a: ftp_reply_code;
|
|
||||||
|
|
||||||
a$z = code % 10;
|
|
||||||
|
|
||||||
code = code / 10;
|
|
||||||
a$y = code % 10;
|
|
||||||
|
|
||||||
code = code / 10;
|
|
||||||
a$x = code % 10;
|
|
||||||
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
event ftp_unexpected_conn_violation(id: conn_id, orig: addr, expected: addr)
|
|
||||||
{
|
|
||||||
NOTICE([$note=FTP_UnexpectedConn, $id=id,
|
|
||||||
$msg=fmt("%s > %s FTP connection from %s",
|
|
||||||
id$orig_h, id$resp_h, orig)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
event ftp_unexpected_conn(id: conn_id, orig: addr, expected: addr)
|
|
||||||
{
|
|
||||||
if ( orig in skip_unexpected || expected in skip_unexpected ||
|
|
||||||
mask_addr(orig, 24) in skip_unexpected_net ||
|
|
||||||
mask_addr(expected, 24) in skip_unexpected_net )
|
|
||||||
; # don't bother reporting
|
|
||||||
|
|
||||||
else if ( mask_addr(orig, 24) == mask_addr(expected, 24) )
|
|
||||||
; # close enough, probably multi-homed
|
|
||||||
|
|
||||||
else if ( mask_addr(orig, 16) == mask_addr(expected, 16) )
|
|
||||||
; # ditto
|
|
||||||
|
|
||||||
else
|
|
||||||
event ftp_unexpected_conn_violation(id, orig, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
event ftp_connection_expected(c: connection, orig_h: addr, resp_h: addr,
|
|
||||||
resp_p: port, session: ftp_session_info)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
event expected_connection_seen(c: connection, a: count)
|
|
||||||
{
|
|
||||||
local id = c$id;
|
|
||||||
if ( [id$resp_h, id$resp_p] in ftp_data_expected )
|
|
||||||
add c$service["ftp-data"];
|
|
||||||
}
|
|
||||||
|
|
||||||
# Deficiency: will miss data connections if the commands/replies
|
|
||||||
# are encrypted.
|
|
||||||
function is_ftp_data_conn(c: connection): bool
|
|
||||||
{
|
|
||||||
local id = c$id;
|
|
||||||
if ( [id$resp_h, id$resp_p] in ftp_data_expected )
|
|
||||||
{
|
|
||||||
local expected = ftp_data_expected[id$resp_h, id$resp_p];
|
|
||||||
if ( id$orig_h != expected$host )
|
|
||||||
event ftp_unexpected_conn(expected$session$connection_id,
|
|
||||||
id$orig_h, expected$host);
|
|
||||||
|
|
||||||
return T;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ( id$orig_p == 20/tcp &&
|
|
||||||
[$orig_h = id$resp_h, $orig_p = id$resp_p,
|
|
||||||
$resp_h = id$orig_h, $resp_p = 21/tcp] in ftp_sessions )
|
|
||||||
return T;
|
|
||||||
else
|
|
||||||
return F;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function new_ftp_session(c: connection, add_init: bool)
|
|
||||||
{
|
|
||||||
local session = c$id;
|
|
||||||
local new_id = ++ftp_session_id;
|
|
||||||
|
|
||||||
local info: ftp_session_info;
|
|
||||||
info$id = new_id;
|
|
||||||
info$connection_id = session;
|
|
||||||
info$user = "<unknown>";
|
|
||||||
info$anonymized_user = "<TBD!>";
|
|
||||||
info$anonymous_login = T;
|
|
||||||
info$request = "";
|
|
||||||
info$num_requests = 0;
|
|
||||||
info$request_t = c$start_time;
|
|
||||||
info$log_if_not_unavail = F;
|
|
||||||
info$log_if_not_denied = F;
|
|
||||||
info$log_it = F;
|
|
||||||
info$reply_code = 0;
|
|
||||||
info$cwd = "<before_login>/";
|
|
||||||
info$pending_requests = init_ftp_pending_cmds();
|
|
||||||
|
|
||||||
if ( add_init )
|
|
||||||
add_to_ftp_pending_cmds(info$pending_requests, "<init>", "");
|
|
||||||
|
|
||||||
ftp_sessions[session] = info;
|
|
||||||
append_addl(c, fmt("#%s", prefixed_id(new_id)));
|
|
||||||
|
|
||||||
print log_file, fmt("%.6f #%s %s start", c$start_time, prefixed_id(new_id),
|
|
||||||
id_string(session));
|
|
||||||
}
|
|
||||||
|
|
||||||
function ftp_message(id: conn_id, msg: string)
|
|
||||||
{
|
|
||||||
print log_file, fmt("%.6f #%s %s",
|
|
||||||
network_time(), prefixed_id(ftp_sessions[id]$id), msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
event ftp_sensitive_file(c: connection, session: ftp_session_info,
|
|
||||||
filename: string)
|
|
||||||
{
|
|
||||||
session$log_if_not_unavail = T;
|
|
||||||
}
|
|
||||||
|
|
||||||
event ftp_excessive_filename(session: ftp_session_info,
|
|
||||||
command: string, arg: string)
|
|
||||||
{
|
|
||||||
NOTICE([$note=FTP_ExcessiveFilename, $id=session$connection_id,
|
|
||||||
$user=session$user, $filename=arg,
|
|
||||||
$msg=fmt("%s #%s excessive filename: %s",
|
|
||||||
id_string(session$connection_id),
|
|
||||||
prefixed_id(session$id), arg)]);
|
|
||||||
session$log_it = T;
|
|
||||||
}
|
|
||||||
|
|
||||||
global ftp_request_rewrite: function(c: connection, session: ftp_session_info,
|
|
||||||
cmd_arg: ftp_cmd_arg);
|
|
||||||
global ftp_reply_rewrite: function(c: connection, session: ftp_session_info,
|
|
||||||
code: count, msg: string,
|
|
||||||
cont_resp: bool, cmd_arg: ftp_cmd_arg);
|
|
||||||
|
|
||||||
# Returns true if the given string is at least 25% composed of 8-bit
|
|
||||||
# characters.
|
|
||||||
function is_string_binary(s: string): bool
|
|
||||||
{
|
|
||||||
return byte_len(gsub(s, /[\x00-\x7f]/, "")) * 100 / byte_len(s) >= 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
event ftp_request(c: connection, command: string, arg: string)
|
|
||||||
{
|
|
||||||
# Command may contain garbage, e.g. if we're parsing something
|
|
||||||
# which isn't ftp. Ignore this.
|
|
||||||
if ( is_string_binary(command) )
|
|
||||||
return;
|
|
||||||
|
|
||||||
local id = c$id;
|
|
||||||
|
|
||||||
if ( id !in ftp_sessions )
|
|
||||||
new_ftp_session(c, F);
|
|
||||||
|
|
||||||
local session = ftp_sessions[id];
|
|
||||||
|
|
||||||
# Keep the original command and arg.
|
|
||||||
local cmd_arg =
|
|
||||||
add_to_ftp_pending_cmds(session$pending_requests, command, arg);
|
|
||||||
|
|
||||||
if ( command == "USER" )
|
|
||||||
{
|
|
||||||
if ( arg in hot_ids &&
|
|
||||||
[id$orig_h, id$resp_h, arg] !in skip_hot )
|
|
||||||
{
|
|
||||||
if ( arg in always_hot_ids )
|
|
||||||
session$log_it = T;
|
|
||||||
else
|
|
||||||
session$log_if_not_denied = T;
|
|
||||||
}
|
|
||||||
|
|
||||||
append_addl(c, arg);
|
|
||||||
session$user = arg;
|
|
||||||
|
|
||||||
if ( arg in forbidden_ids )
|
|
||||||
TerminateConnection::terminate_connection(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ( command == "PASS" )
|
|
||||||
{
|
|
||||||
if ( session$user in forbidden_ids_if_no_password &&
|
|
||||||
arg == "" )
|
|
||||||
TerminateConnection::terminate_connection(c);
|
|
||||||
|
|
||||||
if ( session$user in guest_ids )
|
|
||||||
append_addl_marker(c, arg, "/");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
event account_tried(c, session$user, arg);
|
|
||||||
arg = "<suppressed>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ( command == "PORT" || command == "EPRT" )
|
|
||||||
{
|
|
||||||
local data = (command == "PORT") ?
|
|
||||||
parse_ftp_port(arg) : parse_eftp_port(arg);
|
|
||||||
|
|
||||||
if ( data$valid )
|
|
||||||
{
|
|
||||||
if ( data$h != id$orig_h )
|
|
||||||
ftp_message(id, fmt("*> PORT host %s doesn't match originator host %s", data$h, id$orig_h));
|
|
||||||
|
|
||||||
if ( data$p < 1024/tcp && data$p in port_names )
|
|
||||||
NOTICE([$note=FTP_PrivPort, $id=id,
|
|
||||||
$user=session$user,
|
|
||||||
$msg=fmt("%s #%s privileged PORT %d: %s",
|
|
||||||
id_string(id),
|
|
||||||
prefixed_id(session$id),
|
|
||||||
data$p, arg),
|
|
||||||
$sub="PORT"]);
|
|
||||||
|
|
||||||
local expected = [$host=c$id$resp_h, $session=session];
|
|
||||||
ftp_data_expected[data$h, data$p] = expected;
|
|
||||||
add session$expected[data$h, data$p];
|
|
||||||
|
|
||||||
expect_connection(c$id$resp_h, data$h, data$p,
|
|
||||||
ANALYZER_FILE, 5 min);
|
|
||||||
|
|
||||||
event ftp_connection_expected(c, c$id$resp_h, data$h,
|
|
||||||
data$p, session);
|
|
||||||
}
|
|
||||||
else if ( arg != ignore_invalid_PORT )
|
|
||||||
NOTICE([$note=FTP_BadPort, $id=id,
|
|
||||||
$user=session$user,
|
|
||||||
$msg=fmt("%s #%s invalid ftp PORT directive: %s",
|
|
||||||
id_string(id),
|
|
||||||
prefixed_id(session$id), arg),
|
|
||||||
$sub="PORT"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ( command in ftp_file_cmds )
|
|
||||||
{
|
|
||||||
if ( arg == hot_files ||
|
|
||||||
(session$user in guest_ids &&
|
|
||||||
arg == hot_guest_files) )
|
|
||||||
event ftp_sensitive_file(c, session, arg);
|
|
||||||
|
|
||||||
if ( byte_len(arg) >= excessive_filename_len )
|
|
||||||
{
|
|
||||||
arg = fmt("%s..[%d]..",
|
|
||||||
sub_bytes(arg, 1, excessive_filename_trunc_len),
|
|
||||||
byte_len(arg));
|
|
||||||
event ftp_excessive_filename(session, command, arg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ( command == "ACCT" )
|
|
||||||
append_addl(c, fmt("(account %s)", arg));
|
|
||||||
|
|
||||||
if ( command in hot_cmds && arg == hot_cmds[command] )
|
|
||||||
{
|
|
||||||
session$log_it = T;
|
|
||||||
|
|
||||||
# Special hack for "site exec" attacks.
|
|
||||||
### Obviously, this should be generic and not specialized
|
|
||||||
### like the following.
|
|
||||||
if ( command == "SITE" && /[Ee][Xx][Ee][Cc]/ in arg &&
|
|
||||||
# We see legit use of "site exec cp / /", God knows why.
|
|
||||||
byte_len(arg) > 32 )
|
|
||||||
{ # Terminate with extreme prejudice.
|
|
||||||
TerminateConnection::terminate_connection(c);
|
|
||||||
NOTICE([$note=FTP_SiteExecAttack, $conn=c, $conn=c,
|
|
||||||
$msg=fmt("%s %s", command, arg)]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
local request = arg == "" ? command : cat(command, " ", arg);
|
|
||||||
if ( ++session$num_requests == 1 )
|
|
||||||
{
|
|
||||||
# First pending request
|
|
||||||
session$request = request;
|
|
||||||
session$request_t = network_time();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
# Don't append PASS commands, unless they're for an
|
|
||||||
# anonymous user.
|
|
||||||
|
|
||||||
### Is it okay to include the args of an ACCT command?
|
|
||||||
|
|
||||||
if ( command == "PASS" )
|
|
||||||
{
|
|
||||||
if ( session$user in guest_ids )
|
|
||||||
{
|
|
||||||
session$request =
|
|
||||||
cat(session$request, "/", arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
# Don't count this as a multiple request.
|
|
||||||
--session$num_requests;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if ( byte_len(session$request) < 256 )
|
|
||||||
session$request = cat(session$request, ", ", request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( rewriting_ftp_trace )
|
|
||||||
ftp_request_rewrite(c, session, cmd_arg);
|
|
||||||
|
|
||||||
if ( command in ftp_all_cmds )
|
|
||||||
{
|
|
||||||
if ( command in ftp_untested_cmds )
|
|
||||||
{
|
|
||||||
delete ftp_untested_cmds[command];
|
|
||||||
add ftp_first_seen_cmds[command];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
add ftp_unlisted_cmds[command];
|
|
||||||
}
|
|
||||||
|
|
||||||
event ftp_binary_response(session: ftp_session_info, code: count, msg: string)
|
|
||||||
{
|
|
||||||
print log_file, fmt("%.6f #%s binary response",
|
|
||||||
network_time(), prefixed_id(session$id));
|
|
||||||
}
|
|
||||||
|
|
||||||
function extract_dir_from_reply(session: ftp_session_info, msg: string,
|
|
||||||
hint: string): string
|
|
||||||
{
|
|
||||||
const dir_pattern = /\"([^\"]|\"\")*(\/|\\)([^\"]|\"\")*\"/;
|
|
||||||
local parts = split_all(msg, dir_pattern);
|
|
||||||
|
|
||||||
if ( length(parts) != 3 )
|
|
||||||
{ # not found or ambiguous
|
|
||||||
# print log_file, fmt("%.6f #%s cannot extract directory: \"%s\"",
|
|
||||||
# network_time(), prefixed_id(session$id), msg);
|
|
||||||
return hint;
|
|
||||||
}
|
|
||||||
|
|
||||||
local d = parts[2];
|
|
||||||
return sub_bytes(d, 2, int_to_count(byte_len(d) - 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
# Process ..'s and eliminate duplicate '/'s
|
|
||||||
# Deficiency: gives wrong results when a symbolic link is followed by ".."
|
|
||||||
function compress_path(dir: string): string
|
|
||||||
{
|
|
||||||
const cdup_sep = /((\/)+([^\/]|\\\/)+)?((\/)+\.\.(\/)+)/;
|
|
||||||
|
|
||||||
local parts = split_n(dir, cdup_sep, T, 1);
|
|
||||||
if ( length(parts) > 1 )
|
|
||||||
{
|
|
||||||
parts[2] = "/";
|
|
||||||
dir = cat_string_array(parts);
|
|
||||||
return compress_path(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
const multislash_sep = /(\/){2,}/;
|
|
||||||
parts = split_all(dir, multislash_sep);
|
|
||||||
for ( i in parts )
|
|
||||||
if ( i % 2 == 0 )
|
|
||||||
parts[i] = "/";
|
|
||||||
dir = cat_string_array(parts);
|
|
||||||
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Computes the absolute path with cwd (current working directory).
|
|
||||||
function absolute_path(session: ftp_session_info, file_name: string): string
|
|
||||||
{
|
|
||||||
local abs_file_name: string;
|
|
||||||
if ( file_name == ftp_absolute_path_pat ) # start with '/' or 'A:\'
|
|
||||||
abs_file_name = file_name;
|
|
||||||
else
|
|
||||||
abs_file_name = string_cat(session$cwd, "/", file_name);
|
|
||||||
return compress_path(abs_file_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function do_ftp_reply(c: connection, session: ftp_session_info,
|
|
||||||
code: count, msg: string, cmd: string, arg: string)
|
|
||||||
{
|
|
||||||
local id = c$id;
|
|
||||||
|
|
||||||
if ( session$log_if_not_denied && code != 530 &&
|
|
||||||
# skip password prompt, which we can get when the requests
|
|
||||||
# are stacked up
|
|
||||||
code != 331 )
|
|
||||||
session$log_it = T;
|
|
||||||
|
|
||||||
if ( session$log_if_not_unavail && code != 550 )
|
|
||||||
session$log_it = T;
|
|
||||||
|
|
||||||
if ( code == 227 || code == 229 )
|
|
||||||
{
|
|
||||||
local data = (code == 227) ?
|
|
||||||
parse_ftp_pasv(msg) : parse_ftp_epsv(msg);
|
|
||||||
|
|
||||||
if ( code == 229 && data$h == 0.0.0.0 )
|
|
||||||
data$h = id$resp_h;
|
|
||||||
|
|
||||||
if ( data$valid )
|
|
||||||
{
|
|
||||||
if ( data$h != id$resp_h )
|
|
||||||
ftp_message(id, fmt("*< PASV host %s doesn't match responder host %s", data$h, id$resp_h));
|
|
||||||
|
|
||||||
if ( data$p < 1024/tcp && data$p in port_names &&
|
|
||||||
data$p !in ignore_privileged_PASVs )
|
|
||||||
NOTICE([$note=FTP_PrivPort, $id=id,
|
|
||||||
$user=session$user, $n=code,
|
|
||||||
$msg=fmt("%s #%s privileged PASV %d: %s",
|
|
||||||
id_string(id), prefixed_id(session$id),
|
|
||||||
data$p, msg),
|
|
||||||
$sub="PASV"]);
|
|
||||||
|
|
||||||
local expected = [$host=id$orig_h, $session=session];
|
|
||||||
ftp_data_expected[data$h, data$p] = expected;
|
|
||||||
add session$expected[data$h, data$p];
|
|
||||||
event ftp_connection_expected(c, c$id$orig_h, data$h,
|
|
||||||
data$p, session);
|
|
||||||
|
|
||||||
expect_connection(id$orig_h, data$h, data$p,
|
|
||||||
ANALYZER_FILE, 5 min);
|
|
||||||
|
|
||||||
msg = endpoint_id(data$h, data$p);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ( msg != ignore_invalid_PORT )
|
|
||||||
{
|
|
||||||
NOTICE([$note=FTP_BadPort, $id=id,
|
|
||||||
$user=session$user, $n=code,
|
|
||||||
$msg=fmt("%s #%s invalid ftp PASV directive: %s",
|
|
||||||
id_string(id),
|
|
||||||
prefixed_id(session$id), msg),
|
|
||||||
$sub="PASV"]);
|
|
||||||
msg = "invalid PASV";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( [cmd, code] in ftp_dir_operation )
|
|
||||||
{
|
|
||||||
local cwd: string;
|
|
||||||
|
|
||||||
if ( cmd == "CWD" )
|
|
||||||
{
|
|
||||||
if ( arg == ftp_absolute_path_pat ) # absolute dir
|
|
||||||
cwd = arg;
|
|
||||||
else
|
|
||||||
cwd = cat(session$cwd, "/", arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ( cmd == "CDUP" )
|
|
||||||
cwd = cat(session$cwd, "/..");
|
|
||||||
|
|
||||||
else if ( cmd == "PWD" || cmd == "XPWD" )
|
|
||||||
# Here we need to guess how to extract the
|
|
||||||
# directory from the reply.
|
|
||||||
cwd = extract_dir_from_reply(session, msg,
|
|
||||||
session$cwd);
|
|
||||||
|
|
||||||
# cwd = cat(cwd, "/");
|
|
||||||
|
|
||||||
# Process "..", eliminate duplicate '/'s, and eliminate
|
|
||||||
# last '/' if cwd != "/"
|
|
||||||
# session$cwd = compress_path(cwd);
|
|
||||||
|
|
||||||
session$cwd = cwd;
|
|
||||||
|
|
||||||
# print log_file, fmt("*** DEBUG *** %.06f #%s (%s %s) CWD = \"%s\"",
|
|
||||||
# network_time(), prefixed_id(session$id),
|
|
||||||
# cmd, arg, session$cwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( session$num_requests > 0 )
|
|
||||||
{
|
|
||||||
if ( code in ftp_skip_replies )
|
|
||||||
; # Don't flush request yet.
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
local reply = code in ftp_replies ? ftp_replies[code] :
|
|
||||||
fmt("%d %s", code, msg);
|
|
||||||
|
|
||||||
local session_msg = fmt("#%s %s%s (%s)",
|
|
||||||
prefixed_id(session$id),
|
|
||||||
session$num_requests > 1 ? "*" : "",
|
|
||||||
session$request, reply);
|
|
||||||
|
|
||||||
if ( session$log_it )
|
|
||||||
NOTICE([$note=FTP_Sensitive, $id=id,
|
|
||||||
$user=session$user, $n=code,
|
|
||||||
$msg=fmt("ftp: %s %s",
|
|
||||||
id_string(id), session_msg)]);
|
|
||||||
|
|
||||||
print log_file, fmt("%.6f %s", session$request_t,
|
|
||||||
session_msg);
|
|
||||||
|
|
||||||
session$request = "";
|
|
||||||
session$num_requests = 0;
|
|
||||||
session$log_if_not_unavail = F;
|
|
||||||
session$log_if_not_denied = F;
|
|
||||||
session$log_it = F;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
# An unpaired response. This can happen in particular
|
|
||||||
# when the session is encrypted, so we check for that here.
|
|
||||||
if ( /[\x80-\xff]{3}/ in msg )
|
|
||||||
# Three 8-bit characters in a row - good enough.
|
|
||||||
# Note, this should of course be customizable.
|
|
||||||
event ftp_binary_response(session, code, msg);
|
|
||||||
|
|
||||||
else
|
|
||||||
print log_file, fmt("%.6f #%s response (%d %s)",
|
|
||||||
network_time(), prefixed_id(session$id), code, msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function do_ftp_login(c: connection, session: ftp_session_info)
|
|
||||||
{
|
|
||||||
session$cwd = ftp_init_dir[session$connection_id$resp_h, session$user];
|
|
||||||
event login_successful(c, session$user);
|
|
||||||
}
|
|
||||||
|
|
||||||
event ftp_reply(c: connection, code: count, msg: string, cont_resp: bool)
|
|
||||||
{
|
|
||||||
local id = c$id;
|
|
||||||
local response_xyz = parse_ftp_reply_code(code);
|
|
||||||
|
|
||||||
if ( id !in ftp_sessions )
|
|
||||||
new_ftp_session(c, T);
|
|
||||||
|
|
||||||
local session = ftp_sessions[id];
|
|
||||||
|
|
||||||
if ( code != 0 || ! cont_resp )
|
|
||||||
session$reply_code = code;
|
|
||||||
|
|
||||||
local cmd_arg = find_ftp_pending_cmd(session$pending_requests, session$reply_code, msg);
|
|
||||||
|
|
||||||
if ( ! cont_resp )
|
|
||||||
{
|
|
||||||
if ( response_xyz$x == 2 && # successful
|
|
||||||
(cmd_arg$cmd == /USER|PASS|ACCT/) )
|
|
||||||
do_ftp_login(c, session);
|
|
||||||
|
|
||||||
do_ftp_reply(c, session, code, msg, cmd_arg$cmd, cmd_arg$arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( rewriting_ftp_trace )
|
|
||||||
{
|
|
||||||
ftp_reply_rewrite(c, session, code, msg, cont_resp, cmd_arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! cont_resp )
|
|
||||||
{
|
|
||||||
if ( ftp_cmd_pending(session$pending_requests) )
|
|
||||||
{
|
|
||||||
if ( response_xyz$x == 1 )
|
|
||||||
# nothing
|
|
||||||
;
|
|
||||||
|
|
||||||
else if ( response_xyz$x >= 2 && response_xyz$x <= 5 )
|
|
||||||
{
|
|
||||||
pop_from_ftp_pending_cmd(session$pending_requests, cmd_arg);
|
|
||||||
# print log_file, fmt("*** DEBUG *** %.06f #%d: [%s %s] [%d %s]",
|
|
||||||
# network_time(), session$id, cmd_arg$cmd, cmd_arg$arg, code, msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ( code != 421 ) # closing connection
|
|
||||||
ftp_message(id, fmt("spontaneous response (%d %s)",
|
|
||||||
code, msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const call_ftp_connection_remove = F &redef;
|
|
||||||
global ftp_connection_remove: function(c: connection);
|
|
||||||
|
|
||||||
# Use state remove event instead of finish to cover connections terminated by
|
|
||||||
# RST.
|
|
||||||
event connection_state_remove(c: connection)
|
|
||||||
{
|
|
||||||
local id = c$id;
|
|
||||||
|
|
||||||
if ( is_ftp_conn(c) && call_ftp_connection_remove )
|
|
||||||
ftp_connection_remove(c);
|
|
||||||
|
|
||||||
if ( id in ftp_sessions )
|
|
||||||
{
|
|
||||||
local session = ftp_sessions[id];
|
|
||||||
|
|
||||||
if ( session$num_requests > 0 )
|
|
||||||
{
|
|
||||||
local msg = fmt("#%s %s%s (no reply)",
|
|
||||||
prefixed_id(session$id),
|
|
||||||
session$num_requests > 1 ? "*" : "",
|
|
||||||
session$request);
|
|
||||||
|
|
||||||
if ( session$log_it )
|
|
||||||
NOTICE([$note=FTP_Sensitive, $id=id,
|
|
||||||
$user=session$user,
|
|
||||||
$msg=fmt("ftp: %s %s",
|
|
||||||
id_string(id), msg)]);
|
|
||||||
|
|
||||||
print log_file, fmt("%.6f %s", session$request_t, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ftp_cmd_pending(session$pending_requests) )
|
|
||||||
{
|
|
||||||
local ca = find_ftp_pending_cmd(session$pending_requests, 0, "<finish>");
|
|
||||||
# print log_file, fmt("*** DEBUG *** requests pending from %s %s", ca$cmd, ca$arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
for ( [h, p] in session$expected )
|
|
||||||
delete ftp_data_expected[h, p];
|
|
||||||
|
|
||||||
ftp_message(id, "finish");
|
|
||||||
|
|
||||||
delete ftp_sessions[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event file_transferred(c: connection, prefix: string, descr: string,
|
|
||||||
mime_type: string)
|
|
||||||
{
|
|
||||||
if ( [c$id$resp_h, c$id$resp_p] in ftp_data_expected )
|
|
||||||
{
|
|
||||||
local expected = ftp_data_expected[c$id$resp_h, c$id$resp_p];
|
|
||||||
print log_file, fmt("%.6f #%s ftp-data %s '%s'",
|
|
||||||
c$start_time,
|
|
||||||
prefixed_id(expected$session$id),
|
|
||||||
mime_type, descr);
|
|
||||||
append_addl(c, descr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event file_virus(c: connection, virname: string)
|
|
||||||
{
|
|
||||||
if ( [c$id$resp_h, c$id$resp_p] in ftp_data_expected )
|
|
||||||
{
|
|
||||||
local expected = ftp_data_expected[c$id$resp_h, c$id$resp_p];
|
|
||||||
# FIXME: Throw NOTICE.
|
|
||||||
print log_file, fmt("%.6f #%s VIRUS %s found", c$start_time,
|
|
||||||
prefixed_id(expected$session$id),
|
|
||||||
virname);
|
|
||||||
append_addl(c, fmt("Virus %s", virname));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event bro_init()
|
|
||||||
{
|
|
||||||
have_FTP = T;
|
|
||||||
}
|
|
161
policy/ftp-cmd-arg.bro
Normal file
161
policy/ftp-cmd-arg.bro
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
module FTP;
|
||||||
|
|
||||||
|
export {
|
||||||
|
type CmdArg: record {
|
||||||
|
cmd: string &default="<unknown>";
|
||||||
|
arg: string &default="";
|
||||||
|
seq: count &default=0;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PendingCmds: table[count] of CmdArg;
|
||||||
|
|
||||||
|
const ftp_cmd_reply_code: set[string, count] = {
|
||||||
|
# According to RFC 959
|
||||||
|
["<init>", [120, 220, 421]],
|
||||||
|
["USER", [230, 530, 500, 501, 421, 331, 332]],
|
||||||
|
["PASS", [230, 202, 530, 500, 501, 503, 421, 332]],
|
||||||
|
["ACCT", [230, 202, 530, 500, 501, 503, 421]],
|
||||||
|
["CWD", [250, 500, 501, 502, 421, 530, 550]],
|
||||||
|
["CDUP", [200, 500, 501, 502, 421, 530, 550]],
|
||||||
|
["SMNT", [202, 250, 500, 501, 502, 421, 530, 550]],
|
||||||
|
["REIN", [120, 220, 421, 500, 502]],
|
||||||
|
["QUIT", [221, 500]],
|
||||||
|
["PORT", [200, 500, 501, 421, 530]],
|
||||||
|
["PASV", [227, 500, 501, 502, 421, 530]],
|
||||||
|
["MODE", [200, 500, 501, 504, 421, 530]],
|
||||||
|
["TYPE", [200, 500, 501, 504, 421, 530]],
|
||||||
|
["STRU", [200, 500, 501, 504, 421, 530]],
|
||||||
|
["ALLO", [200, 202, 500, 501, 504, 421, 530]],
|
||||||
|
["REST", [500, 501, 502, 421, 530, 350]],
|
||||||
|
["STOR", [125, 150, 110, 226, 250, 425, 426, 451, 551, 552, 532, 450, 452, 553, 500, 501, 421, 530]],
|
||||||
|
["STOU", [125, 150, 110, 226, 250, 425, 426, 451, 551, 552, 532, 450, 452, 553, 500, 501, 421, 530]],
|
||||||
|
["RETR", [125, 150, 110, 226, 250, 425, 426, 451, 450, 550, 500, 501, 421, 530]],
|
||||||
|
["LIST", [125, 150, 226, 250, 425, 426, 451, 450, 500, 501, 502, 421, 530]],
|
||||||
|
["NLST", [125, 150, 226, 250, 425, 426, 451, 450, 500, 501, 502, 421, 530]],
|
||||||
|
["APPE", [125, 150, 226, 250, 425, 426, 451, 551, 552, 532, 450, 550, 452, 553, 500, 501, 502, 421, 530]],
|
||||||
|
["RNFR", [450, 550, 500, 501, 502, 421, 530, 350]],
|
||||||
|
["RNTO", [250, 532, 553, 500, 501, 502, 503, 421, 530]],
|
||||||
|
["DELE", [250, 450, 550, 500, 501, 502, 421, 530]],
|
||||||
|
["RMD", [250, 500, 501, 502, 421, 530, 550]],
|
||||||
|
["MKD", [257, 500, 501, 502, 421, 530, 550]],
|
||||||
|
["PWD", [257, 500, 501, 502, 421, 550]],
|
||||||
|
["ABOR", [225, 226, 500, 501, 502, 421]],
|
||||||
|
["SYST", [215, 500, 501, 502, 421]],
|
||||||
|
["STAT", [211, 212, 213, 450, 500, 501, 502, 421, 530]],
|
||||||
|
["HELP", [211, 214, 500, 501, 502, 421]],
|
||||||
|
["SITE", [200, 202, 500, 501, 530]],
|
||||||
|
["NOOP", [200, 500, 421]],
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
#["SIZE", [213, 550]],
|
||||||
|
#["SITE", 214],
|
||||||
|
#["MDTM", 213],
|
||||||
|
#["EPSV", 500],
|
||||||
|
#["FEAT", 500],
|
||||||
|
#["OPTS", 500],
|
||||||
|
|
||||||
|
#["CDUP", 250],
|
||||||
|
#["CLNT", 200],
|
||||||
|
#["CLNT", 500],
|
||||||
|
#["EPRT", 500],
|
||||||
|
|
||||||
|
#["FEAT", 211],
|
||||||
|
#["HELP", 200],
|
||||||
|
#["LIST", 550],
|
||||||
|
#["LPRT", 500],
|
||||||
|
#["MACB", 500],
|
||||||
|
#["MDTM", 212],
|
||||||
|
#["MDTM", 500],
|
||||||
|
#["MDTM", 501],
|
||||||
|
#["MDTM", 550],
|
||||||
|
#["MLST", 500],
|
||||||
|
#["MLST", 550],
|
||||||
|
#["MODE", 502],
|
||||||
|
#["NLST", 550],
|
||||||
|
#["OPTS", 501],
|
||||||
|
#["REST", 200],
|
||||||
|
#["SITE", 502],
|
||||||
|
#["SIZE", 500],
|
||||||
|
#["STOR", 550],
|
||||||
|
#["SYST", 530],
|
||||||
|
|
||||||
|
["<init>", 0], # unexpected command-reply pair
|
||||||
|
["<missing>", 0], # unexpected command-reply pair
|
||||||
|
["QUIT", 0], # unexpected command-reply pair
|
||||||
|
} &redef;
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_pending_cmd(pc: PendingCmds, cmd: string, arg: string): CmdArg
|
||||||
|
{
|
||||||
|
local ca = [$cmd = cmd, $arg = arg, $seq=|pc|+1];
|
||||||
|
pc[|pc|+1] = ca;
|
||||||
|
|
||||||
|
return ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_pending_cmd(pc: PendingCmds, reply_code: count, reply_msg: string): CmdArg
|
||||||
|
{
|
||||||
|
local best_match: CmdArg;
|
||||||
|
local best_seq = 0;
|
||||||
|
local best_score: int = -1;
|
||||||
|
|
||||||
|
#if ( |pc| == 0 )
|
||||||
|
# return best_match;
|
||||||
|
|
||||||
|
for ( cmd_seq in pc )
|
||||||
|
{
|
||||||
|
local cmd = pc[cmd_seq];
|
||||||
|
local score: int = 0;
|
||||||
|
# if the command is compatible with the reply code
|
||||||
|
# code 500 (syntax error) is compatible with all commands
|
||||||
|
if ( reply_code == 500 || [cmd$cmd, reply_code] in ftp_cmd_reply_code )
|
||||||
|
score = score + 100;
|
||||||
|
# if the command or the command arg appears in the reply message
|
||||||
|
if ( strstr(reply_msg, cmd$cmd) > 0 )
|
||||||
|
score = score + 20;
|
||||||
|
if ( strstr(reply_msg, cmd$cmd) > 0 )
|
||||||
|
score = score + 10;
|
||||||
|
if ( score > best_score ||
|
||||||
|
( score == best_score && best_seq > cmd_seq ) ) # break tie with sequence number
|
||||||
|
{
|
||||||
|
best_score = score;
|
||||||
|
best_seq = cmd_seq;
|
||||||
|
best_match = cmd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( [best_match$cmd, reply_code] !in ftp_cmd_reply_code )
|
||||||
|
{
|
||||||
|
local annotation = "";
|
||||||
|
if ( |pc| == 1 )
|
||||||
|
annotation = "for sure";
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for ( i in pc )
|
||||||
|
annotation = cat(annotation, " ", pc[i]);
|
||||||
|
annotation = cat("candidates:", annotation);
|
||||||
|
}
|
||||||
|
# add ftp_unexpected_cmd_reply[fmt("[\"%s\", %d], # %s",
|
||||||
|
# best_match$cmd, reply_code, annotation)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return best_match;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_pending_cmd(pc: PendingCmds, ca: CmdArg): bool
|
||||||
|
{
|
||||||
|
if ( ca$seq in pc )
|
||||||
|
{
|
||||||
|
delete pc[ca$seq];
|
||||||
|
return T;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return F;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pop_pending_cmd(pc: PendingCmds, reply_code: count, reply_msg: string): CmdArg
|
||||||
|
{
|
||||||
|
local ca = get_pending_cmd(pc, reply_code, reply_msg);
|
||||||
|
remove_pending_cmd(pc, ca);
|
||||||
|
return ca;
|
||||||
|
}
|
636
policy/ftp.bro
Normal file
636
policy/ftp.bro
Normal file
|
@ -0,0 +1,636 @@
|
||||||
|
@load functions
|
||||||
|
@load notice.bro
|
||||||
|
@load ftp-cmd-arg
|
||||||
|
|
||||||
|
#@load conn
|
||||||
|
#@load scan
|
||||||
|
#@load hot-ids
|
||||||
|
#@load terminate-connection
|
||||||
|
|
||||||
|
module FTP;
|
||||||
|
|
||||||
|
redef enum Notice::Type += {
|
||||||
|
FTP_UnexpectedConn, # FTP data transfer from unexpected src
|
||||||
|
FTP_ExcessiveFilename, # very long filename seen
|
||||||
|
FTP_PrivPort, # privileged port used in PORT/PASV;
|
||||||
|
# $sub says which
|
||||||
|
FTP_BadPort, # bad format in PORT/PASV; $sub says which
|
||||||
|
FTP_Sensitive, # sensitive connection - not more specific
|
||||||
|
FTP_SiteExecAttack, # specific "site exec" attack seen
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
redef enum Log::ID += { FTP_AUTH, FTP_FILES };
|
||||||
|
type LogFiles: record {
|
||||||
|
ts: time;
|
||||||
|
id: conn_id;
|
||||||
|
user: string &default="";
|
||||||
|
password: string &optional;
|
||||||
|
command: string &default="";
|
||||||
|
url: string &default="";
|
||||||
|
mime_type: string &default="";
|
||||||
|
mime_desc: string &default="";
|
||||||
|
reply_code: count &default=0;
|
||||||
|
reply_msg: string &default="";
|
||||||
|
};
|
||||||
|
|
||||||
|
type SessionInfo: record {
|
||||||
|
ts: time; # time of request
|
||||||
|
id: conn_id;
|
||||||
|
user: string &default="<unknown>";
|
||||||
|
password: string &optional;
|
||||||
|
cwd: string &default="<before_login>/";
|
||||||
|
command: CmdArg &optional;
|
||||||
|
reply_code: count &default=0; # the most recent reply code
|
||||||
|
reply_msg: string &default=""; # the most recent reply message
|
||||||
|
|
||||||
|
pending_commands: PendingCmds; # pending requests
|
||||||
|
|
||||||
|
log_it: bool &default=F; # if true, log the request(s)s
|
||||||
|
};
|
||||||
|
|
||||||
|
type FTPExpectedConn: record {
|
||||||
|
host: addr;
|
||||||
|
session: SessionInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReplyCode: record {
|
||||||
|
x: count; # high-order (3rd digit)
|
||||||
|
y: count; # middle (2nd) digit
|
||||||
|
z: count; # bottom digit
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
# Indexed by source & destination addresses and the id.
|
||||||
|
#const skip_hot: set[addr, addr, string] &redef;
|
||||||
|
|
||||||
|
# see: http://packetstormsecurity.org/UNIX/penetration/rootkits/index4.html
|
||||||
|
# for current list of rootkits to include here
|
||||||
|
|
||||||
|
#const hot_files =
|
||||||
|
# /.*(etc\/|master\.)?(passwd|shadow|s?pwd\.db)/
|
||||||
|
# | /.*snoop\.(tar|tgz).*/
|
||||||
|
# | /.*bnc\.(tar|tgz).*/
|
||||||
|
# | /.*datapipe.*/
|
||||||
|
# | /.*ADMw0rm.*/
|
||||||
|
# | /.*newnick.*/
|
||||||
|
# | /.*sniffit.*/
|
||||||
|
# | /.*neet\.(tar|tgz).*/
|
||||||
|
# | /.*\.\.\..*/
|
||||||
|
# | /.*ftpscan.txt.*/
|
||||||
|
# | /.*jcc.pdf.*/
|
||||||
|
# | /.*\.[Ff]rom.*/
|
||||||
|
# | /.*sshd\.(tar|tgz).*/
|
||||||
|
# | /.*\/rk7.*/
|
||||||
|
# | /.*rk7\..*/
|
||||||
|
# | /.*[aA][dD][oO][rR][eE][bB][sS][dD].*/
|
||||||
|
# | /.*[tT][aA][gG][gG][eE][dD].*/
|
||||||
|
# | /.*shv4\.(tar|tgz).*/
|
||||||
|
# | /.*lrk\.(tar|tgz).*/
|
||||||
|
# | /.*lyceum\.(tar|tgz).*/
|
||||||
|
# | /.*maxty\.(tar|tgz).*/
|
||||||
|
# | /.*rootII\.(tar|tgz).*/
|
||||||
|
# | /.*invader\.(tar|tgz).*/
|
||||||
|
#&redef;
|
||||||
|
|
||||||
|
#const hot_guest_files =
|
||||||
|
# /.*\.rhosts/
|
||||||
|
# | /.*\.forward/
|
||||||
|
# &redef;
|
||||||
|
|
||||||
|
#const hot_cmds: table[string] of pattern = {
|
||||||
|
# ["SITE"] = /[Ee][Xx][Ee][Cc].*/,
|
||||||
|
#} &redef;
|
||||||
|
|
||||||
|
const excessive_filename_len = 250 &redef;
|
||||||
|
const excessive_filename_trunc_len = 32 &redef;
|
||||||
|
|
||||||
|
const guest_ids = { "anonymous", "ftp", "guest", } &redef;
|
||||||
|
|
||||||
|
# Invalid PORT/PASV directives that exactly match the following
|
||||||
|
# don't generate notice's.
|
||||||
|
const ignore_invalid_PORT =
|
||||||
|
/,0,0/ # these are common, dunno why
|
||||||
|
&redef;
|
||||||
|
|
||||||
|
# Some servers generate particular privileged PASV ports for benign
|
||||||
|
# reasons (presumably to tunnel through firewalls, sigh).
|
||||||
|
const ignore_privileged_PASVs = { ssh, } &redef;
|
||||||
|
|
||||||
|
# Pairs of IP addresses for which we shouldn't bother logging if one
|
||||||
|
# of them is used in lieu of the other in a PORT or PASV directive.
|
||||||
|
|
||||||
|
const skip_unexpected: set[addr] = {
|
||||||
|
15.253.0.10, 15.253.48.10, 15.254.56.2, # hp.com
|
||||||
|
gvaona1.cns.hp.com,
|
||||||
|
} &redef;
|
||||||
|
|
||||||
|
const skip_unexpected_net: set[addr] &redef;
|
||||||
|
|
||||||
|
# This tracks all of the currently established FTP control sessions.
|
||||||
|
global ftp_sessions: table[conn_id] of SessionInfo;
|
||||||
|
|
||||||
|
# Indexed by the responder pair, yielding the address expected to connect to it.
|
||||||
|
global ftp_data_expected: table[addr, port] of FTPExpectedConn &create_expire=1min;
|
||||||
|
|
||||||
|
global ftp_ports = { 21/tcp } &redef;
|
||||||
|
redef dpd_config += { [ANALYZER_FTP] = [$ports = ftp_ports] };
|
||||||
|
redef capture_filters += { ["ftp"] = "port 20 or port 21" };
|
||||||
|
}
|
||||||
|
|
||||||
|
event bro_init()
|
||||||
|
{
|
||||||
|
Log::create_stream("FTP_FILES", "FTP::LogFiles");
|
||||||
|
Log::add_default_filter("FTP_FILES");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ftp_file_cmds = {
|
||||||
|
"APPE", "CWD", "DELE", "MKD", "RETR", "RMD", "RNFR", "RNTO",
|
||||||
|
"STOR", "STOU",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ftp_absolute_path_pat = /(\/|[A-Za-z]:[\\\/]).*/;
|
||||||
|
|
||||||
|
const ftp_dir_operation = {
|
||||||
|
["CWD", 250],
|
||||||
|
["CDUP", 200], # typo in RFC?
|
||||||
|
["CDUP", 250], # as found in traces
|
||||||
|
["PWD", 257],
|
||||||
|
["XPWD", 257],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ftp_skip_replies = {
|
||||||
|
150, # "status okay - about to open connection"
|
||||||
|
331 # "user name okay, need password"
|
||||||
|
};
|
||||||
|
|
||||||
|
const ftp_replies: table[count] of string = {
|
||||||
|
[150] = "ok",
|
||||||
|
[200] = "ok",
|
||||||
|
[220] = "ready for new user",
|
||||||
|
[221] = "closed",
|
||||||
|
[226] = "complete",
|
||||||
|
[230] = "logged in",
|
||||||
|
[250] = "ok",
|
||||||
|
[257] = "done",
|
||||||
|
[331] = "id ok",
|
||||||
|
[500] = "syntax error",
|
||||||
|
[530] = "denied",
|
||||||
|
[550] = "unavail",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ftp_other_replies = { ftp_replies };
|
||||||
|
|
||||||
|
const ftp_all_cmds: set[string] = {
|
||||||
|
"<init>", "<missing>",
|
||||||
|
"USER", "PASS", "ACCT",
|
||||||
|
"CWD", "CDUP", "SMNT",
|
||||||
|
"REIN", "QUIT",
|
||||||
|
"PORT", "PASV", "MODE", "TYPE", "STRU",
|
||||||
|
"ALLO", "REST", "STOR", "STOU", "RETR", "LIST", "NLST", "APPE",
|
||||||
|
"RNFR", "RNTO", "DELE", "RMD", "MKD", "PWD", "ABOR",
|
||||||
|
"SYST", "STAT", "HELP",
|
||||||
|
"SITE", "NOOP",
|
||||||
|
|
||||||
|
# FTP extensions
|
||||||
|
"SIZE", "MDTM", "MLST", "MLSD",
|
||||||
|
"EPRT", "EPSV",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
# const ftp_state_diagram: table[string] of count = {
|
||||||
|
# ["ABOR", "ALLO", "DELE", "CWD", "CDUP",
|
||||||
|
# "SMNT", "HELP", "MODE", "NOOP", "PASV",
|
||||||
|
# "QUIT", "SITE", "PORT", "SYST", "STAT",
|
||||||
|
# "RMD", "MKD", "PWD", "STRU", "TYPE"] = 1,
|
||||||
|
# ["APPE", "LIST", "NLST", "RETR", "STOR", "STOU"] = 2,
|
||||||
|
# ["REIN"] = 3,
|
||||||
|
# ["RNFR", "RNTO"] = 4,
|
||||||
|
# };
|
||||||
|
|
||||||
|
function cmd_pending(s: SessionInfo): bool
|
||||||
|
{
|
||||||
|
return |s$pending_commands| > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_ftp_reply_code(code: count): ReplyCode
|
||||||
|
{
|
||||||
|
local a: ReplyCode;
|
||||||
|
|
||||||
|
a$z = code % 10;
|
||||||
|
|
||||||
|
code = code / 10;
|
||||||
|
a$y = code % 10;
|
||||||
|
|
||||||
|
code = code / 10;
|
||||||
|
a$x = code % 10;
|
||||||
|
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process ..'s and eliminate duplicate '/'s
|
||||||
|
# Deficiency: gives wrong results when a symbolic link is followed by ".."
|
||||||
|
function compress_path(dir: string): string
|
||||||
|
{
|
||||||
|
const cdup_sep = /((\/)+([^\/]|\\\/)+)?((\/)+\.\.(\/)+)/;
|
||||||
|
|
||||||
|
local parts = split_n(dir, cdup_sep, T, 1);
|
||||||
|
if ( length(parts) > 1 )
|
||||||
|
{
|
||||||
|
parts[2] = "/";
|
||||||
|
dir = cat_string_array(parts);
|
||||||
|
return compress_path(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
const multislash_sep = /(\/){2,}/;
|
||||||
|
parts = split_all(dir, multislash_sep);
|
||||||
|
for ( i in parts )
|
||||||
|
if ( i % 2 == 0 )
|
||||||
|
parts[i] = "/";
|
||||||
|
dir = cat_string_array(parts);
|
||||||
|
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Computes the absolute path with cwd (current working directory).
|
||||||
|
function absolute_path(session: SessionInfo, file_name: string): string
|
||||||
|
{
|
||||||
|
local abs_file_name: string;
|
||||||
|
if ( file_name == ftp_absolute_path_pat ) # start with '/' or 'A:\'
|
||||||
|
abs_file_name = file_name;
|
||||||
|
else
|
||||||
|
abs_file_name = string_cat(session$cwd, "/", file_name);
|
||||||
|
return compress_path(abs_file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
event ftp_unexpected_conn(id: conn_id, orig: addr, expected: addr)
|
||||||
|
{
|
||||||
|
if ( orig in skip_unexpected || expected in skip_unexpected ||
|
||||||
|
mask_addr(orig, 24) in skip_unexpected_net ||
|
||||||
|
mask_addr(expected, 24) in skip_unexpected_net )
|
||||||
|
; # don't bother reporting
|
||||||
|
|
||||||
|
else if ( mask_addr(orig, 24) == mask_addr(expected, 24) )
|
||||||
|
; # close enough, probably multi-homed
|
||||||
|
|
||||||
|
else if ( mask_addr(orig, 16) == mask_addr(expected, 16) )
|
||||||
|
; # ditto
|
||||||
|
|
||||||
|
#else
|
||||||
|
# Notice::NOTICE([$note=FTP_UnexpectedConn, $id=id,
|
||||||
|
# $msg=fmt("%s > %s FTP connection from %s",
|
||||||
|
# id$orig_h, id$resp_h, orig)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
event expected_connection_seen(c: connection, a: count)
|
||||||
|
{
|
||||||
|
local id = c$id;
|
||||||
|
if ( [id$resp_h, id$resp_p] in ftp_data_expected )
|
||||||
|
{
|
||||||
|
add c$service["ftp-data"];
|
||||||
|
delete ftp_data_expected[id$resp_h, id$resp_p];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function new_ftp_session(c: connection, add_init: bool)
|
||||||
|
{
|
||||||
|
local id = c$id;
|
||||||
|
|
||||||
|
local info: SessionInfo;
|
||||||
|
info$id = id;
|
||||||
|
local cmds: table[count] of CmdArg = table();
|
||||||
|
info$pending_commands = cmds;
|
||||||
|
|
||||||
|
if ( add_init )
|
||||||
|
add_pending_cmd(info$pending_commands, "<init>", "");
|
||||||
|
|
||||||
|
ftp_sessions[id] = info;
|
||||||
|
|
||||||
|
#append_addl(c, fmt("#%s", prefixed_id(new_id)));
|
||||||
|
|
||||||
|
#print log_file, fmt("%.6f #%s %s start", c$start_time, prefixed_id(new_id),
|
||||||
|
# id_string(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ftp_message(s: SessionInfo)
|
||||||
|
{
|
||||||
|
if ( !s$log_it ) return;
|
||||||
|
|
||||||
|
local pass = "";
|
||||||
|
if ( s$user in guest_ids && s?$password )
|
||||||
|
pass = s$password;
|
||||||
|
local pathfile = sub(absolute_path(s, s$command$arg), /<unknown>/, "/.");
|
||||||
|
|
||||||
|
if ( s$command$cmd in ftp_file_cmds )
|
||||||
|
Log::write("FTP_FILES", [$ts=network_time(), $id=s$id,
|
||||||
|
$user=s$user, $password=pass,
|
||||||
|
$command=s$command$cmd,
|
||||||
|
$url=fmt("ftp://%s%s", s$id$resp_h, pathfile),
|
||||||
|
$mime_type="", $mime_desc="",
|
||||||
|
$reply_code=s$reply_code, $reply_msg=s$reply_msg]);
|
||||||
|
s$log_it = F;
|
||||||
|
}
|
||||||
|
|
||||||
|
event ftp_request(c: connection, command: string, arg: string)
|
||||||
|
{
|
||||||
|
local id = c$id;
|
||||||
|
|
||||||
|
# Command may contain garbage, e.g. if we're parsing something
|
||||||
|
# which isn't ftp. Ignore this.
|
||||||
|
if ( is_string_binary(command) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ( id !in ftp_sessions )
|
||||||
|
new_ftp_session(c, F);
|
||||||
|
local session = ftp_sessions[id];
|
||||||
|
|
||||||
|
# Queue up the command and argument
|
||||||
|
add_pending_cmd(session$pending_commands, command, arg);
|
||||||
|
|
||||||
|
if ( command == "USER" )
|
||||||
|
session$user = arg;
|
||||||
|
|
||||||
|
else if ( command == "PASS" )
|
||||||
|
session$password = arg;
|
||||||
|
|
||||||
|
else if ( command in ftp_file_cmds )
|
||||||
|
{
|
||||||
|
if ( |arg| >= excessive_filename_len )
|
||||||
|
{
|
||||||
|
arg = fmt("%s..[%d]..",
|
||||||
|
sub_bytes(arg, 1, excessive_filename_trunc_len), |arg|);
|
||||||
|
#Notice::NOTICE([$note=FTP_ExcessiveFilename, $id=session$id,
|
||||||
|
# #$user=session$user, $filename=arg,
|
||||||
|
# $msg=fmt("%s excessive filename: %s",
|
||||||
|
# id_string(session$id), arg)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if ( command == "ACCT" )
|
||||||
|
append_addl(c, fmt("(account %s)", arg));
|
||||||
|
|
||||||
|
else if ( command == "PORT" || command == "EPRT" )
|
||||||
|
{
|
||||||
|
local data = (command == "PORT") ?
|
||||||
|
parse_ftp_port(arg) : parse_eftp_port(arg);
|
||||||
|
|
||||||
|
if ( data$valid )
|
||||||
|
{
|
||||||
|
#if ( data$h != id$orig_h )
|
||||||
|
# ftp_message(id, fmt("*> PORT host %s doesn't match originator host %s", data$h, id$orig_h));
|
||||||
|
|
||||||
|
#if ( data$p < 1024/tcp && data$p in port_names )
|
||||||
|
# Notice::NOTICE([$note=FTP_PrivPort, $id=id,
|
||||||
|
# $user=session$user,
|
||||||
|
# $msg=fmt("%s privileged PORT %d: %s",
|
||||||
|
# id_string(id),data$p, arg),
|
||||||
|
# $sub="PORT"]);
|
||||||
|
|
||||||
|
local expected = [$host=c$id$resp_h, $session=session];
|
||||||
|
ftp_data_expected[data$h, data$p] = expected;
|
||||||
|
|
||||||
|
expect_connection(c$id$resp_h, data$h, data$p,
|
||||||
|
ANALYZER_FILE, 5 min);
|
||||||
|
}
|
||||||
|
#else if ( arg != ignore_invalid_PORT )
|
||||||
|
# Notice::NOTICE([$note=FTP_BadPort, $id=id,
|
||||||
|
# #$user=session$user,
|
||||||
|
# $msg=fmt("%s invalid ftp PORT directive: %s",
|
||||||
|
# id_string(id), arg),
|
||||||
|
# $sub="PORT"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if ( command in hot_cmds && arg == hot_cmds[command] )
|
||||||
|
# {
|
||||||
|
# session$log_it = T;
|
||||||
|
|
||||||
|
# TODO: generate a notice instead of terminating here.
|
||||||
|
# Special hack for "site exec" attacks.
|
||||||
|
### Obviously, this should be generic and not specialized
|
||||||
|
### like the following.
|
||||||
|
#if ( command == "SITE" && /[Ee][Xx][Ee][Cc]/ in arg &&
|
||||||
|
# # We see legit use of "site exec cp / /", God knows why.
|
||||||
|
# |arg| > 32 )
|
||||||
|
# { # Terminate with extreme prejudice.
|
||||||
|
# TerminateConnection::terminate_connection(c);
|
||||||
|
# Notice::NOTICE([$note=FTP_SiteExecAttack, $conn=c, $conn=c,
|
||||||
|
# $msg=fmt("%s %s", command, arg)]);
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
|
||||||
|
event ftp_binary_response(session: SessionInfo, code: count, msg: string)
|
||||||
|
{
|
||||||
|
#print log_file, fmt("%.6f #%s binary response",
|
||||||
|
# network_time(), prefixed_id(session$id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function extract_dir_from_reply(session: SessionInfo, msg: string,
|
||||||
|
hint: string): string
|
||||||
|
{
|
||||||
|
const dir_pattern = /\"([^\"]|\"\")*(\/|\\)([^\"]|\"\")*\"/;
|
||||||
|
local parts = split_all(msg, dir_pattern);
|
||||||
|
|
||||||
|
if ( |parts| != 3 )
|
||||||
|
{ # not found or ambiguous
|
||||||
|
# print log_file, fmt("%.6f #%s cannot extract directory: \"%s\"",
|
||||||
|
# network_time(), prefixed_id(session$id), msg);
|
||||||
|
return hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
local d = parts[2];
|
||||||
|
return sub_bytes(d, 2, int_to_count(|d| - 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function do_ftp_login(c: connection, session: SessionInfo)
|
||||||
|
{
|
||||||
|
#event login_successful(c, session$user);
|
||||||
|
}
|
||||||
|
|
||||||
|
event ftp_reply(c: connection, code: count, msg: string, cont_resp: bool)
|
||||||
|
{
|
||||||
|
# Not sure how to handle multiline responses yet.
|
||||||
|
if ( cont_resp ) return;
|
||||||
|
|
||||||
|
local id = c$id;
|
||||||
|
if ( id !in ftp_sessions )
|
||||||
|
new_ftp_session(c, T);
|
||||||
|
local session = ftp_sessions[id];
|
||||||
|
|
||||||
|
session$reply_code = code;
|
||||||
|
session$reply_msg = msg;
|
||||||
|
|
||||||
|
local cmd_arg = get_pending_cmd(session$pending_commands, code, msg);
|
||||||
|
local response_xyz = parse_ftp_reply_code(code);
|
||||||
|
|
||||||
|
#if ( response_xyz$x == 2 && # successful
|
||||||
|
# (cmd_arg$cmd == /USER|PASS|ACCT/) )
|
||||||
|
# do_ftp_login(c, session);
|
||||||
|
|
||||||
|
# skip password prompt, which we can get when the requests are stacked up
|
||||||
|
if ( code != 530 && code != 331 )
|
||||||
|
session$log_it = T;
|
||||||
|
|
||||||
|
if ( code == 227 || code == 229 )
|
||||||
|
{
|
||||||
|
local data = (code == 227) ? parse_ftp_pasv(msg) : parse_ftp_epsv(msg);
|
||||||
|
|
||||||
|
if ( code == 229 && data$h == 0.0.0.0 )
|
||||||
|
data$h = id$resp_h;
|
||||||
|
|
||||||
|
if ( data$valid )
|
||||||
|
{
|
||||||
|
#if ( data$h != id$resp_h )
|
||||||
|
# ftp_message(id, fmt("*< PASV host %s doesn't match responder host %s", data$h, id$resp_h));
|
||||||
|
|
||||||
|
#if ( data$p < 1024/tcp &&
|
||||||
|
# data$p !in ignore_privileged_PASVs )
|
||||||
|
# Notice::NOTICE([$note=FTP_PrivPort, $id=id,
|
||||||
|
# $msg=fmt("%s privileged PASV %d: %s",
|
||||||
|
# id_string(id), data$p, msg),
|
||||||
|
# $n=code, $sub="PASV"]);
|
||||||
|
|
||||||
|
local expected = [$host=id$orig_h, $session=session];
|
||||||
|
ftp_data_expected[data$h, data$p] = expected;
|
||||||
|
expect_connection(id$orig_h, data$h, data$p, ANALYZER_FILE, 5 min);
|
||||||
|
|
||||||
|
msg = fmt("%s %d", data$h, data$p);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if ( msg != ignore_invalid_PORT )
|
||||||
|
{
|
||||||
|
#Notice::NOTICE([$note=FTP_BadPort, $id=id,
|
||||||
|
# $msg=fmt("%s invalid ftp PASV directive: %s",
|
||||||
|
# id_string(id), msg),
|
||||||
|
# $sub="PASV", $n=code]);
|
||||||
|
msg = "invalid PASV";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( [cmd_arg$cmd, code] in ftp_dir_operation )
|
||||||
|
{
|
||||||
|
local cwd: string;
|
||||||
|
|
||||||
|
if ( cmd_arg$cmd == "CWD" )
|
||||||
|
{
|
||||||
|
if ( cmd_arg$arg == ftp_absolute_path_pat ) # absolute dir
|
||||||
|
cwd = cmd_arg$arg;
|
||||||
|
else
|
||||||
|
cwd = cat(session$cwd, "/", cmd_arg$arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if ( cmd_arg$cmd == "CDUP" )
|
||||||
|
cwd = cat(session$cwd, "/..");
|
||||||
|
|
||||||
|
else if ( cmd_arg$cmd == "PWD" || cmd_arg$cmd == "XPWD" )
|
||||||
|
# Here we need to guess how to extract the
|
||||||
|
# directory from the reply.
|
||||||
|
cwd = extract_dir_from_reply(session, msg, session$cwd);
|
||||||
|
|
||||||
|
session$cwd = cwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( cmd_pending(session) )
|
||||||
|
{
|
||||||
|
if ( code in ftp_skip_replies )
|
||||||
|
; # Don't flush request yet.
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
local reply = code in ftp_replies ? ftp_replies[code] :
|
||||||
|
fmt("%d %s", code, msg);
|
||||||
|
|
||||||
|
#local session_msg = fmt("%s%s (%s)",
|
||||||
|
# |session$pending_commands| > 1 ? "*" : "",
|
||||||
|
# session$command, reply);
|
||||||
|
#
|
||||||
|
#if ( session$log_it )
|
||||||
|
# Notice::NOTICE([$note=FTP_Sensitive, $id=id, $n=code,
|
||||||
|
# $msg=fmt("ftp: %s %s",
|
||||||
|
# id_string(id), session_msg)]);
|
||||||
|
|
||||||
|
#ftp_message(id, "whatever");
|
||||||
|
|
||||||
|
#session$command = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
# An unpaired response. This can happen in particular
|
||||||
|
# when the session is encrypted, so we check for that here.
|
||||||
|
if ( /[\x80-\xff]{3}/ in msg )
|
||||||
|
# Three 8-bit characters in a row - good enough.
|
||||||
|
# Note, this should of course be customizable.
|
||||||
|
event ftp_binary_response(session, code, msg);
|
||||||
|
|
||||||
|
else
|
||||||
|
print fmt("Saw an unpaired response %d %s", code, msg);
|
||||||
|
#print log_file, fmt("%.6f #%s response (%d %s)",
|
||||||
|
# network_time(), prefixed_id(session$id), code, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ( cmd_pending(session) )
|
||||||
|
# {
|
||||||
|
# if ( response_xyz$x == 1 )
|
||||||
|
# # nothing
|
||||||
|
# ;
|
||||||
|
#
|
||||||
|
# else if ( response_xyz$x >= 2 && response_xyz$x <= 5 )
|
||||||
|
# {
|
||||||
|
# remove_pending_cmd(session$pending_commands, cmd_arg);
|
||||||
|
# # print log_file, fmt("*** DEBUG *** %.06f #%d: [%s %s] [%d %s]",
|
||||||
|
# # network_time(), session$id, cmd_arg$cmd, cmd_arg$arg, code, msg);
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
session$command = pop_pending_cmd(session$pending_commands, code, msg);
|
||||||
|
# Go ahead and log for the oldest command.
|
||||||
|
ftp_message(session);
|
||||||
|
|
||||||
|
#else if ( code != 421 ) # closing connection
|
||||||
|
# ftp_message(id, fmt("spontaneous response (%d %s)", code, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use state remove event instead of finish to cover connections terminated by
|
||||||
|
# RST.
|
||||||
|
event connection_state_remove(c: connection)
|
||||||
|
{
|
||||||
|
local id = c$id;
|
||||||
|
if ( id !in ftp_sessions )
|
||||||
|
return;
|
||||||
|
|
||||||
|
local session = ftp_sessions[id];
|
||||||
|
|
||||||
|
if ( cmd_pending(session) )
|
||||||
|
{
|
||||||
|
#local msg = fmt("%s%s (no reply)",
|
||||||
|
# |session$pending_commands| > 1 ? "*" : "",
|
||||||
|
# session$command);
|
||||||
|
#
|
||||||
|
#if ( session$log_it )
|
||||||
|
# Notice::NOTICE([$note=FTP_Sensitive, $id=id,
|
||||||
|
# $msg=fmt("ftp: %s %s", id_string(id), msg)]);
|
||||||
|
|
||||||
|
local ca = get_pending_cmd(session$pending_commands, 0, "<finish>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#ftp_message(id, "finish");
|
||||||
|
|
||||||
|
delete ftp_sessions[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
event file_transferred(c: connection, prefix: string, descr: string,
|
||||||
|
mime_type: string)
|
||||||
|
{
|
||||||
|
if ( [c$id$resp_h, c$id$resp_p] in ftp_data_expected )
|
||||||
|
{
|
||||||
|
local expected = ftp_data_expected[c$id$resp_h, c$id$resp_p];
|
||||||
|
print fmt("%.6f ftp-data %s '%s'",
|
||||||
|
c$start_time,
|
||||||
|
mime_type, descr);
|
||||||
|
#append_addl(c, descr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
@load site
|
@load site
|
||||||
@load logging
|
@load logging
|
||||||
|
|
||||||
|
# Returns true if the given string is at least 25% composed of 8-bit
|
||||||
|
# characters.
|
||||||
|
function is_string_binary(s: string): bool
|
||||||
|
{
|
||||||
|
return byte_len(gsub(s, /[\x00-\x7f]/, "")) * 100 / |s| >= 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Simple functions for generating ASCII connection identifiers.
|
# Simple functions for generating ASCII connection identifiers.
|
||||||
############# BEGIN ID FORMATTING #############
|
############# BEGIN ID FORMATTING #############
|
||||||
function id_string(id: conn_id): string
|
function id_string(id: conn_id): string
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue