diff --git a/policy.old/ftp-cmd-arg.bro b/policy.old/ftp-cmd-arg.bro deleted file mode 100644 index 5bb7d269c8..0000000000 --- a/policy.old/ftp-cmd-arg.bro +++ /dev/null @@ -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 - ["", [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], - - ["", 0], # unexpected command-reply pair - ["", 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 = "", - $anonymized_arg = "", $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 = "", $arg = "", - $anonymized_cmd = "", $anonymized_arg = "", - $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); - } diff --git a/policy.old/ftp.bro b/policy.old/ftp.bro deleted file mode 100644 index 4686d2b6ee..0000000000 --- a/policy.old/ftp.bro +++ /dev/null @@ -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 = "/"; - -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] = { - "", "", - "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 = ""; - info$anonymized_user = ""; - 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 = "/"; - info$pending_requests = init_ftp_pending_cmds(); - - if ( add_init ) - add_to_ftp_pending_cmds(info$pending_requests, "", ""); - - 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 = ""; - } - } - - 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, ""); - # 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; - } diff --git a/policy/ftp-cmd-arg.bro b/policy/ftp-cmd-arg.bro new file mode 100644 index 0000000000..7f3c86be7d --- /dev/null +++ b/policy/ftp-cmd-arg.bro @@ -0,0 +1,161 @@ +module FTP; + +export { + type CmdArg: record { + cmd: string &default=""; + 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 + ["", [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], + + ["", 0], # unexpected command-reply pair + ["", 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; + } diff --git a/policy/ftp.bro b/policy/ftp.bro new file mode 100644 index 0000000000..22425143c9 --- /dev/null +++ b/policy/ftp.bro @@ -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=""; + password: string &optional; + cwd: string &default="/"; + 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] = { + "", "", + "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, "", ""); + + 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), //, "/."); + + 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, ""); + } + + #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); + } + } + diff --git a/policy/functions.bro b/policy/functions.bro index 1231864230..2b5a26de3b 100644 --- a/policy/functions.bro +++ b/policy/functions.bro @@ -1,6 +1,14 @@ @load site @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. ############# BEGIN ID FORMATTING ############# function id_string(id: conn_id): string