From 49b8c7e3909ba0b57019285eaa07022c44f45270 Mon Sep 17 00:00:00 2001 From: Jon Siwek Date: Fri, 5 Oct 2012 10:43:23 -0500 Subject: [PATCH] Add analyzer for GSI mechanism of GSSAPI FTP AUTH method. GSI authentication involves an encoded TLS/SSL handshake over the FTP control session. Decoding the exchanged tokens and passing them to an SSL analyzer instance allows use of all the familiar script-layer events in inspecting the handshake (e.g. client/server certificats are available). For FTP sessions that attempt GSI authentication, the service field of the connection record will have both "ftp" and "ssl". One additional change is an FTP server's acceptance of an AUTH request no longer causes analysis of the connection to cease (because further analysis likely wasn't possible). This decision can be made more dynamically at the script-layer (plus there's now the fact that further analysis can be done at least on the GSSAPI AUTH method). --- doc/scripts/DocSourcesList.cmake | 2 + scripts/base/protocols/ftp/main.bro | 6 +- .../protocols/ftp/gridftp-data-detection.bro | 4 +- scripts/test-all-policy.bro | 1 + src/Analyzer.cc | 1 + src/AnalyzerTags.h | 1 + src/FTP.cc | 173 ++++++++++++++++-- src/FTP.h | 22 +++ .../Baseline/core.print-bpf-filters/output | 2 +- .../canonified_loaded_scripts.log | 1 + 10 files changed, 188 insertions(+), 25 deletions(-) diff --git a/doc/scripts/DocSourcesList.cmake b/doc/scripts/DocSourcesList.cmake index 1abe6b9305..077e103dca 100644 --- a/doc/scripts/DocSourcesList.cmake +++ b/doc/scripts/DocSourcesList.cmake @@ -65,6 +65,7 @@ rest_target(${psd} base/frameworks/tunnels/main.bro) rest_target(${psd} base/protocols/conn/contents.bro) rest_target(${psd} base/protocols/conn/inactivity.bro) rest_target(${psd} base/protocols/conn/main.bro) +rest_target(${psd} base/protocols/conn/polling.bro) rest_target(${psd} base/protocols/dns/consts.bro) rest_target(${psd} base/protocols/dns/main.bro) rest_target(${psd} base/protocols/ftp/file-extract.bro) @@ -122,6 +123,7 @@ rest_target(${psd} policy/protocols/conn/weirds.bro) rest_target(${psd} policy/protocols/dns/auth-addl.bro) rest_target(${psd} policy/protocols/dns/detect-external-names.bro) rest_target(${psd} policy/protocols/ftp/detect.bro) +rest_target(${psd} policy/protocols/ftp/gridftp-data-detection.bro) rest_target(${psd} policy/protocols/ftp/software.bro) rest_target(${psd} policy/protocols/http/detect-MHR.bro) rest_target(${psd} policy/protocols/http/detect-intel.bro) diff --git a/scripts/base/protocols/ftp/main.bro b/scripts/base/protocols/ftp/main.bro index d20bc92d8a..0a4bfc07cc 100644 --- a/scripts/base/protocols/ftp/main.bro +++ b/scripts/base/protocols/ftp/main.bro @@ -96,11 +96,11 @@ redef record connection += { }; # Configure DPD -const ports = { 21/tcp } &redef; -redef capture_filters += { ["ftp"] = "port 21" }; +const ports = { 21/tcp, 2811/tcp } &redef; +redef capture_filters += { ["ftp"] = "port 21 and port 2811" }; redef dpd_config += { [ANALYZER_FTP] = [$ports = ports] }; -redef likely_server_ports += { 21/tcp }; +redef likely_server_ports += { 21/tcp, 2811/tcp }; # Establish the variable for tracking expected connections. global ftp_data_expected: table[addr, port] of Info &create_expire=5mins; diff --git a/scripts/policy/protocols/ftp/gridftp-data-detection.bro b/scripts/policy/protocols/ftp/gridftp-data-detection.bro index 15acfba65b..ffa2fa5816 100644 --- a/scripts/policy/protocols/ftp/gridftp-data-detection.bro +++ b/scripts/policy/protocols/ftp/gridftp-data-detection.bro @@ -10,12 +10,12 @@ ##! benefit of saving CPU cycles that otherwise go to analyzing such ##! large (and hopefully benign) connections. -module GridFTP; - @load base/protocols/conn @load base/protocols/ssl @load base/frameworks/notice +module GridFTP; + export { ## Number of bytes transferred before guessing a connection is a ## GridFTP data channel. diff --git a/scripts/test-all-policy.bro b/scripts/test-all-policy.bro index a7c43b14b3..f535d88cd5 100644 --- a/scripts/test-all-policy.bro +++ b/scripts/test-all-policy.bro @@ -34,6 +34,7 @@ @load protocols/dns/auth-addl.bro @load protocols/dns/detect-external-names.bro @load protocols/ftp/detect.bro +@load protocols/ftp/gridftp-data-detection.bro @load protocols/ftp/software.bro @load protocols/http/detect-intel.bro @load protocols/http/detect-MHR.bro diff --git a/src/Analyzer.cc b/src/Analyzer.cc index 9e30da0066..8c5573f96b 100644 --- a/src/Analyzer.cc +++ b/src/Analyzer.cc @@ -171,6 +171,7 @@ const Analyzer::Config Analyzer::analyzer_configs[] = { { AnalyzerTag::Contents_SMB, "CONTENTS_SMB", 0, 0, 0, false }, { AnalyzerTag::Contents_RPC, "CONTENTS_RPC", 0, 0, 0, false }, { AnalyzerTag::Contents_NFS, "CONTENTS_NFS", 0, 0, 0, false }, + { AnalyzerTag::FTP_ADAT, "FTP_ADAT", 0, 0, 0, false }, }; AnalyzerTimer::~AnalyzerTimer() diff --git a/src/AnalyzerTags.h b/src/AnalyzerTags.h index 7fad4d35bb..4301de8f71 100644 --- a/src/AnalyzerTags.h +++ b/src/AnalyzerTags.h @@ -46,6 +46,7 @@ namespace AnalyzerTag { Contents, ContentLine, NVT, Zip, Contents_DNS, Contents_NCP, Contents_NetbiosSSN, Contents_Rlogin, Contents_Rsh, Contents_DCE_RPC, Contents_SMB, Contents_RPC, Contents_NFS, + FTP_ADAT, // End-marker. LastAnalyzer }; diff --git a/src/FTP.cc b/src/FTP.cc index 588348ea8d..fba6b3eea6 100644 --- a/src/FTP.cc +++ b/src/FTP.cc @@ -8,6 +8,8 @@ #include "FTP.h" #include "NVT.h" #include "Event.h" +#include "SSL.h" +#include "Base64.h" FTP_Analyzer::FTP_Analyzer(Connection* conn) : TCP_ApplicationAnalyzer(AnalyzerTag::FTP, conn) @@ -44,6 +46,14 @@ void FTP_Analyzer::Done() Weird("partial_ftp_request"); } +static uint32 get_reply_code(int len, const char* line) + { + if ( len >= 3 && isdigit(line[0]) && isdigit(line[1]) && isdigit(line[2]) ) + return (line[0] - '0') * 100 + (line[1] - '0') * 10 + (line[2] - '0'); + else + return 0; + } + void FTP_Analyzer::DeliverStream(int length, const u_char* data, bool orig) { TCP_ApplicationAnalyzer::DeliverStream(length, data, orig); @@ -93,16 +103,7 @@ void FTP_Analyzer::DeliverStream(int length, const u_char* data, bool orig) } else { - uint32 reply_code; - if ( length >= 3 && - isdigit(line[0]) && isdigit(line[1]) && isdigit(line[2]) ) - { - reply_code = (line[0] - '0') * 100 + - (line[1] - '0') * 10 + - (line[2] - '0'); - } - else - reply_code = 0; + uint32 reply_code = get_reply_code(length, line); int cont_resp; @@ -143,19 +144,22 @@ void FTP_Analyzer::DeliverStream(int length, const u_char* data, bool orig) else line = end_of_line; - if ( auth_requested.size() > 0 && - (reply_code == 234 || reply_code == 335) ) - // Server accepted AUTH requested, - // which means that very likely we - // won't be able to parse the rest - // of the session, and thus we stop - // here. - SetSkip(true); - cont_resp = 0; } } + if ( reply_code == 334 && auth_requested.size() > 0 && + auth_requested == "GSSAPI" ) + { + // Server wants to proceed with an ADAT exchange and we + // know how to analyze the GSI mechanism, so attach analyzer + // to look for that. + SSL_Analyzer* ssl = new SSL_Analyzer(Conn()); + ssl->AddSupportAnalyzer(new FTP_ADAT_Analyzer(Conn(), true)); + ssl->AddSupportAnalyzer(new FTP_ADAT_Analyzer(Conn(), false)); + AddChildAnalyzer(ssl); + } + vl->append(new Val(reply_code, TYPE_COUNT)); vl->append(new StringVal(end_of_line - line, line)); vl->append(new Val(cont_resp, TYPE_BOOL)); @@ -164,5 +168,136 @@ void FTP_Analyzer::DeliverStream(int length, const u_char* data, bool orig) } ConnectionEvent(f, vl); + + ForwardStream(length, data, orig); } +void FTP_ADAT_Analyzer::DeliverStream(int len, const u_char* data, bool orig) + { + // Don't know how to parse anything but the ADAT exchanges of GSI GSSAPI, + // which is basically just TLS/SSL. + if ( ! Parent()->GetTag() == AnalyzerTag::SSL ) + { + Parent()->Remove(); + return; + } + + bool done = false; + const char* line = (const char*) data; + const char* end_of_line = line + len; + + BroString* decoded_adat = 0; + + if ( orig ) + { + int cmd_len; + const char* cmd; + line = skip_whitespace(line, end_of_line); + get_word(len, line, cmd_len, cmd); + + if ( strncmp(cmd, "ADAT", cmd_len) == 0 ) + { + line = skip_whitespace(line + cmd_len, end_of_line); + StringVal* encoded = new StringVal(end_of_line - line, line); + decoded_adat = decode_base64(encoded->AsString()); + delete encoded; + + if ( first_token ) + { + // RFC 2743 section 3.1 specifies a framing format for tokens + // that includes an identifier for the mechanism type. The + // framing is supposed to be required for the initial context + // token, but GSI doesn't do that and starts right in on a + // TLS/SSL handshake, so look for that to identify it. + const u_char* msg = decoded_adat->Bytes(); + int msg_len = decoded_adat->Len(); + + // Just check that it looks like a viable TLS/SSL handshake + // record from the first byte (content type of 0x16) and + // that the fourth and fifth bytes indicating the length of + // the record match the length of the decoded data. + if ( msg_len < 5 || msg[0] != 0x16 || + msg_len - 5 != ntohs(*((uint16*)(msg + 3))) ) + { + // Doesn't look like TLS/SSL, so done analyzing. + done = true; + delete decoded_adat; + decoded_adat = 0; + } + } + + first_token = false; + } + else if ( strncmp(cmd, "AUTH", cmd_len) == 0 ) + { + // Security state will be reset by a reissued AUTH + done = true; + } + } + else + { + uint32 reply_code = get_reply_code(len, line); + + switch ( reply_code ) { + case 232: + case 234: + // Indicates security data exchange is complete, but nothing + // more to decode in replies. + done = true; + break; + + case 235: + // Security data exchange complete, but may have more to decode + // in the reply (same format at 334 and 335). + done = true; + case 334: + case 335: + // Security data exchange still in progress, and there could be data + // to decode in the reply. + line += 3; + if ( len > 3 && line[0] == '-' ) line++; + line = skip_whitespace(line, end_of_line); + + if ( end_of_line - line >= 5 && strncmp(line, "ADAT=", 5) == 0 ) + { + line += 5; + StringVal* encoded = new StringVal(end_of_line - line, line); + decoded_adat = decode_base64(encoded->AsString()); + delete encoded; + } + break; + + case 421: + case 431: + case 500: + case 501: + case 503: + case 535: + // Server isn't going to accept named security mechanism. + // Client has to restart back at the AUTH. + done = true; + break; + + case 631: + case 632: + case 633: + // If the server is sending protected replies, the security + // data exchange must have already succeeded. It does have + // encoded data in the reply, but 632 and 633 are also encrypted. + done = true; + break; + + default: + break; + } + } + + if ( decoded_adat ) + { + ForwardStream(decoded_adat->Len(), decoded_adat->Bytes(), orig); + delete decoded_adat; + } + + if ( done ) + Parent()->Remove(); + } diff --git a/src/FTP.h b/src/FTP.h index 4ef6c44d83..f8d7644808 100644 --- a/src/FTP.h +++ b/src/FTP.h @@ -30,4 +30,26 @@ protected: string auth_requested; // AUTH method requested }; +/** + * Analyzes security data of ADAT exchanges over FTP control session (RFC 2228). + * Currently only the GSI mechanism of GSSAPI AUTH method is understood. + * The ADAT exchange for GSI is base64 encoded TLS/SSL handshake tokens. This + * analyzer just decodes the tokens and passes them on to the parent, which must + * be an SSL analyzer instance. + */ +class FTP_ADAT_Analyzer : public SupportAnalyzer { +public: + FTP_ADAT_Analyzer(Connection* conn, bool arg_orig) + : SupportAnalyzer(AnalyzerTag::FTP_ADAT, conn, arg_orig), + first_token(true) { } + + void DeliverStream(int len, const u_char* data, bool orig); + +protected: + // Used by the client-side analyzer to tell if it needs to peek at the + // initial context token and do sanity checking (i.e. does it look like + // a TLS/SSL handshake token). + bool first_token; +}; + #endif diff --git a/testing/btest/Baseline/core.print-bpf-filters/output b/testing/btest/Baseline/core.print-bpf-filters/output index c55952ffed..55473b8991 100644 --- a/testing/btest/Baseline/core.print-bpf-filters/output +++ b/testing/btest/Baseline/core.print-bpf-filters/output @@ -16,7 +16,7 @@ #open 2012-07-27-19-14-29 #fields ts node filter init success #types time string string bool bool -1343416469.888870 - (((((((((((((((((((((((((port 53) or (tcp port 989)) or (tcp port 443)) or (port 6669)) or (udp and port 5353)) or (port 6668)) or (tcp port 1080)) or (udp and port 5355)) or (tcp port 22)) or (tcp port 995)) or (port 21)) or (tcp port 25 or tcp port 587)) or (port 6667)) or (tcp port 614)) or (tcp port 990)) or (udp port 137)) or (tcp port 993)) or (tcp port 5223)) or (port 514)) or (tcp port 585)) or (tcp port 992)) or (tcp port 563)) or (tcp port 994)) or (tcp port 636)) or (tcp and port (80 or 81 or 631 or 1080 or 3138 or 8000 or 8080 or 8888))) or (port 6666) T T +1343416469.888870 - (((((((((((((((((((((((((port 53) or (tcp port 989)) or (tcp port 443)) or (port 6669)) or (udp and port 5353)) or (port 6668)) or (tcp port 1080)) or (udp and port 5355)) or (tcp port 22)) or (tcp port 995)) or (port 21 and port 2811)) or (tcp port 25 or tcp port 587)) or (port 6667)) or (tcp port 614)) or (tcp port 990)) or (udp port 137)) or (tcp port 993)) or (tcp port 5223)) or (port 514)) or (tcp port 585)) or (tcp port 992)) or (tcp port 563)) or (tcp port 994)) or (tcp port 636)) or (tcp and port (80 or 81 or 631 or 1080 or 3138 or 8000 or 8080 or 8888))) or (port 6666) T T #close 2012-07-27-19-14-29 #separator \x09 #set_separator , diff --git a/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log b/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log index b2afadc0fe..755260351b 100644 --- a/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log +++ b/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log @@ -77,6 +77,7 @@ scripts/base/init-default.bro scripts/base/protocols/conn/./main.bro scripts/base/protocols/conn/./contents.bro scripts/base/protocols/conn/./inactivity.bro + scripts/base/protocols/conn/./polling.bro scripts/base/protocols/dns/__load__.bro scripts/base/protocols/dns/./consts.bro scripts/base/protocols/dns/./main.bro