From: Tom Peters (thopeter) Date: Wed, 24 Mar 2021 16:05:05 +0000 (+0000) Subject: Merge pull request #2803 in SNORT/snort3 from ~THOPETER/snort3:nhttp156 to master X-Git-Tag: 3.1.3.0~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b3de7fe19b5efcb6368958af7ad83aec812da8e4;p=thirdparty%2Fsnort3.git Merge pull request #2803 in SNORT/snort3 from ~THOPETER/snort3:nhttp156 to master Squashed commit of the following: commit 124ef14653ebd8c95178155ef5fa94d76cb60aa0 Author: Tom Peters Date: Wed Mar 17 13:46:37 2021 -0400 http_inspect: alert on HTTP/2 upgrade attempts --- diff --git a/doc/user/http_inspect.txt b/doc/user/http_inspect.txt index 00ce605dc..90940c528 100755 --- a/doc/user/http_inspect.txt +++ b/doc/user/http_inspect.txt @@ -487,10 +487,6 @@ http_header when no specific header is specified. This is the body of a request or response message. It will be dechunked and unzipped if applicable but will not be normalized in any other way. -The difference between http_raw_body and packet data is a rule that uses -packet data will search and may match an HTTP header, but http_raw_body -is limited to the message body. Thus the latter is more efficient and -more accurate for most uses. ===== http_method @@ -524,15 +520,12 @@ request message those are http_method, http_raw_uri, and http_version. For a response message those are http_version, http_stat_code, and http_stat_msg. -===== file_data and packet data +===== file_data file_data contains the normalized message body. This is the normalization described above under gzip, normalize_utf, decompress_pdf, decompress_swf, and normalize_javascript. -The unnormalized message content is available in the packet data. If gzip -is configured the packet data will be unzipped. - ==== Timing issues and combining rule options HTTP inspector is stateful. That means it is aware of a bigger picture than diff --git a/src/service_inspectors/http_inspect/http_enum.h b/src/service_inspectors/http_inspect/http_enum.h index cc39b7fea..a1a957a38 100755 --- a/src/service_inspectors/http_inspect/http_enum.h +++ b/src/service_inspectors/http_inspect/http_enum.h @@ -108,6 +108,22 @@ enum InspectSection { IS_NONE, IS_HEADER, IS_FLEX_HEADER, IS_FIRST_BODY, IS_BODY // Part of the URI to be provided enum UriComponent { UC_SCHEME = 1, UC_HOST, UC_PORT, UC_PATH, UC_QUERY, UC_FRAGMENT }; +// Types of character for URI scanning +enum CharAction { CHAR_NORMAL=2, CHAR_PERCENT, CHAR_PATH, CHAR_EIGHTBIT, CHAR_SUBSTIT }; + +// Content codings +enum Contentcoding { CONTENTCODE__OTHER=1, CONTENTCODE_GZIP, CONTENTCODE_DEFLATE, + CONTENTCODE_COMPRESS, CONTENTCODE_EXI, CONTENTCODE_PACK200_GZIP, CONTENTCODE_X_GZIP, + CONTENTCODE_X_COMPRESS, CONTENTCODE_IDENTITY, CONTENTCODE_CHUNKED, CONTENTCODE_BR, + CONTENTCODE_BZIP2, CONTENTCODE_LZMA, CONTENTCODE_PEERDIST, CONTENTCODE_SDCH, + CONTENTCODE_XPRESS, CONTENTCODE_XZ }; + +// Transfer-Encoding header values +enum TransferEncoding { TE__OTHER=1, TE_CHUNKED, TE_IDENTITY }; + +// Upgrade header values +enum Upgrade { UP__OTHER=1, UP_H2C, UP_H2, UP_HTTP20 }; + // Every header we have ever heard of enum HeaderId { HEAD__NOT_COMPUTE=-14, HEAD__PROBLEMATIC=-12, HEAD__NOT_PRESENT=-11, HEAD__OTHER=1, HEAD_CACHE_CONTROL, HEAD_CONNECTION, HEAD_DATE, HEAD_PRAGMA, HEAD_TRAILER, HEAD_COOKIE, @@ -121,7 +137,7 @@ enum HeaderId { HEAD__NOT_COMPUTE=-14, HEAD__PROBLEMATIC=-12, HEAD__NOT_PRESENT= HEAD_CONTENT_LENGTH, HEAD_CONTENT_LOCATION, HEAD_CONTENT_MD5, HEAD_CONTENT_RANGE, HEAD_CONTENT_TYPE, HEAD_EXPIRES, HEAD_LAST_MODIFIED, HEAD_X_FORWARDED_FOR, HEAD_TRUE_CLIENT_IP, HEAD_X_WORKING_WITH, HEAD_CONTENT_TRANSFER_ENCODING, HEAD_MIME_VERSION, HEAD_PROXY_AGENT, - HEAD_CONTENT_DISPOSITION, HEAD__MAX_VALUE }; + HEAD_CONTENT_DISPOSITION, HEAD_HTTP2_SETTINGS, HEAD__MAX_VALUE }; // All the infractions we might find while parsing and analyzing a message enum Infraction @@ -246,19 +262,11 @@ enum Infraction INF_TRUNCATED_MSG_BODY_CHUNK, INF_LONG_SCHEME, INF_MULTIPLE_HOST_HDRS, + INF_HTTP2_SETTINGS, + INF_UPGRADE_HEADER_HTTP2, INF__MAX_VALUE }; -// Types of character for URI scanning -enum CharAction { CHAR_NORMAL=2, CHAR_PERCENT, CHAR_PATH, CHAR_EIGHTBIT, CHAR_SUBSTIT }; - -// Content codings -enum Contentcoding { CONTENTCODE__OTHER=1, CONTENTCODE_GZIP, CONTENTCODE_DEFLATE, - CONTENTCODE_COMPRESS, CONTENTCODE_EXI, CONTENTCODE_PACK200_GZIP, CONTENTCODE_X_GZIP, - CONTENTCODE_X_COMPRESS, CONTENTCODE_IDENTITY, CONTENTCODE_CHUNKED, CONTENTCODE_BR, - CONTENTCODE_BZIP2, CONTENTCODE_LZMA, CONTENTCODE_PEERDIST, CONTENTCODE_SDCH, - CONTENTCODE_XPRESS, CONTENTCODE_XZ }; - enum EventSid { EVENT__NONE = -1, @@ -377,6 +385,8 @@ enum EventSid EVENT_TRUNCATED_MSG_BODY_CL = 260, EVENT_TRUNCATED_MSG_BODY_CHUNK = 261, EVENT_LONG_SCHEME = 262, + EVENT_HTTP2_UPGRADE_REQUEST = 263, + EVENT_HTTP2_UPGRADE_RESPONSE = 264, EVENT__MAX_VALUE }; diff --git a/src/service_inspectors/http_inspect/http_msg_head_shared.h b/src/service_inspectors/http_inspect/http_msg_head_shared.h index 5741dff73..05573be72 100755 --- a/src/service_inspectors/http_inspect/http_msg_head_shared.h +++ b/src/service_inspectors/http_inspect/http_msg_head_shared.h @@ -51,6 +51,8 @@ public: static const StrCode content_code_list[]; static const StrCode charset_code_list[]; static const StrCode charset_code_opt_list[]; + static const StrCode transfer_encoding_list[]; + static const StrCode upgrade_list[]; // The file_cache_index is used along with the source ip and destination ip to cache file // verdicts. diff --git a/src/service_inspectors/http_inspect/http_msg_header.cc b/src/service_inspectors/http_inspect/http_msg_header.cc index e7f418588..20e181e3e 100755 --- a/src/service_inspectors/http_inspect/http_msg_header.cc +++ b/src/service_inspectors/http_inspect/http_msg_header.cc @@ -155,6 +155,37 @@ void HttpMsgHeader::gen_events() add_infraction(INF_CTE_HEADER); create_event(EVENT_CTE_HEADER); } + + // We don't support HTTP/1 to HTTP/2 upgrade and we alert on any attempt to do it + if (get_header_count(HEAD_HTTP2_SETTINGS) > 0) + { + add_infraction(INF_HTTP2_SETTINGS); + if (source_id == SRC_CLIENT) + create_event(EVENT_HTTP2_UPGRADE_REQUEST); + else + create_event(EVENT_HTTP2_UPGRADE_RESPONSE); + } + if ((get_header_count(HEAD_UPGRADE) > 0) && + ((source_id == SRC_CLIENT) || (status_code_num == 101))) + { + const Field& up_header = get_header_value_norm(HEAD_UPGRADE); + int32_t consumed = 0; + do + { + const int32_t upgrade = get_code_from_token_list(up_header.start(), up_header.length(), + consumed, upgrade_list); + if ((upgrade == UP_H2C) || (upgrade == UP_H2) || (upgrade == UP_HTTP20)) + { + add_infraction(INF_UPGRADE_HEADER_HTTP2); + if (source_id == SRC_CLIENT) + create_event(EVENT_HTTP2_UPGRADE_REQUEST); + else + create_event(EVENT_HTTP2_UPGRADE_RESPONSE); + break; + } + } + while (consumed != -1); + } } void HttpMsgHeader::update_flow() @@ -253,12 +284,15 @@ void HttpMsgHeader::update_flow() if (session_data->for_http2) { // The only transfer-encoding header we should see for HTTP/2 traffic is "identity" - const int IDENTITY_SIZE = 8; - if ((te_header.length() > 0) && ( (te_header.length() != IDENTITY_SIZE) || - memcmp(te_header.start(), "identity", IDENTITY_SIZE) != 0)) + if (te_header.length() > 0) { - add_infraction(INF_H2_NON_IDENTITY_TE); - create_event(EVENT_H2_NON_IDENTITY_TE); + int32_t consumed = 0; + if ((get_code_from_token_list(te_header.start(), te_header.length(), consumed, + transfer_encoding_list) != TE_IDENTITY) || (consumed != -1)) + { + add_infraction(INF_H2_NON_IDENTITY_TE); + create_event(EVENT_H2_NON_IDENTITY_TE); + } } if (get_header_value_norm(HEAD_CONTENT_LENGTH).length() > 0) { @@ -293,35 +327,36 @@ void HttpMsgHeader::update_flow() // If there is a Transfer-Encoding header, it should be "chunked" without any other // encodings being listed. The RFC allows other encodings to come before chunked but // no one does this in real life. - const int CHUNKED_SIZE = 7; - bool is_chunked = false; - - if ((te_header.length() == CHUNKED_SIZE) && - !memcmp(te_header.start(), "chunked", CHUNKED_SIZE)) + unsigned token_count = 0; + int32_t consumed = 0; + int32_t transfer_encoding; + do { - is_chunked = true; + transfer_encoding = get_code_from_token_list(te_header.start(), te_header.length(), + consumed, transfer_encoding_list); + token_count++; } - else if ((te_header.length() > CHUNKED_SIZE) && - !memcmp(te_header.start() + (te_header.length() - (CHUNKED_SIZE+1)), - ",chunked", CHUNKED_SIZE+1)) + while (consumed != -1); + + if (transfer_encoding != TE_CHUNKED) { - add_infraction(INF_PADDED_TE_HEADER); - create_event(EVENT_PADDED_TE_HEADER); - is_chunked = true; + add_infraction(INF_BAD_TE_HEADER); + create_event(EVENT_BAD_TE_HEADER); } - - if (is_chunked) + else { + // Last Transfer-Encoding is chunked ... + if (token_count > 1) + { + // ... but there were others before it + add_infraction(INF_PADDED_TE_HEADER); + create_event(EVENT_PADDED_TE_HEADER); + } session_data->type_expected[source_id] = SEC_BODY_CHUNK; HttpModule::increment_peg_counts(PEG_CHUNKED); prepare_body(); return; } - else - { - add_infraction(INF_BAD_TE_HEADER); - create_event(EVENT_BAD_TE_HEADER); - } } // else because Transfer-Encoding header negates Content-Length header even if something was diff --git a/src/service_inspectors/http_inspect/http_str_to_code.cc b/src/service_inspectors/http_inspect/http_str_to_code.cc index 230ec5972..c1360ca5a 100755 --- a/src/service_inspectors/http_inspect/http_str_to_code.cc +++ b/src/service_inspectors/http_inspect/http_str_to_code.cc @@ -50,7 +50,8 @@ int32_t substr_to_code(const uint8_t* text, const int32_t text_len, const StrCod { for (int32_t k=0; table[k].name != nullptr; k++) { - int32_t len = (text_len <= (int)strlen(table[k].name) ) ? text_len : (int)strlen(table[k].name); + int32_t len = (text_len <= (int)strlen(table[k].name) ) ? text_len : + (int)strlen(table[k].name); if (memcmp(text, table[k].name, len) == 0) { @@ -60,3 +61,13 @@ int32_t substr_to_code(const uint8_t* text, const int32_t text_len, const StrCod return HttpCommon::STAT_OTHER; } +int32_t get_code_from_token_list(const uint8_t* token_list, const int32_t text_len, + int32_t& consumed, const StrCode table[]) +{ + int32_t k; + for (k = consumed; (k < text_len) and (token_list[k] != ','); k++); + const int32_t code = str_to_code(token_list + consumed, k - consumed, table); + consumed = (k < text_len) ? k + 1 : -1; + return code; +} + diff --git a/src/service_inspectors/http_inspect/http_str_to_code.h b/src/service_inspectors/http_inspect/http_str_to_code.h index 8e783ad86..3a3f0c5db 100755 --- a/src/service_inspectors/http_inspect/http_str_to_code.h +++ b/src/service_inspectors/http_inspect/http_str_to_code.h @@ -32,5 +32,10 @@ int32_t str_to_code(const char* text, const StrCode table[]); int32_t str_to_code(const uint8_t* text, const int32_t text_len, const StrCode table[]); int32_t substr_to_code(const uint8_t* text, const int32_t text_len, const StrCode table[]); +// Convert the first value in a comma-separated list into a code. consumed is the number of bytes +// used from the list or -1 if there are no more list entries. +int32_t get_code_from_token_list(const uint8_t* token_list, const int32_t text_len, + int32_t& bytes_consumed, const StrCode table[]); + #endif diff --git a/src/service_inspectors/http_inspect/http_tables.cc b/src/service_inspectors/http_inspect/http_tables.cc index 6125af834..45b897ced 100755 --- a/src/service_inspectors/http_inspect/http_tables.cc +++ b/src/service_inspectors/http_inspect/http_tables.cc @@ -139,6 +139,7 @@ const StrCode HttpMsgHeadShared::header_list[] = { HEAD_MIME_VERSION, "mime-version" }, { HEAD_PROXY_AGENT, "proxy-agent" }, { HEAD_CONTENT_DISPOSITION, "content-disposition" }, + { HEAD_HTTP2_SETTINGS, "http2-settings" }, { 0, nullptr } }; @@ -181,6 +182,21 @@ const StrCode HttpMsgHeadShared::charset_code_opt_list[] = { 0, nullptr } }; +const StrCode HttpMsgHeadShared::upgrade_list[] = +{ + { UP_H2C, "h2c" }, + { UP_H2, "h2" }, + { UP_HTTP20, "http/2.0" }, + { 0, nullptr } +}; + +const StrCode HttpMsgHeadShared::transfer_encoding_list[] = +{ + { TE_CHUNKED, "chunked" }, + { TE_IDENTITY, "identity" }, + { 0, nullptr } +}; + const HeaderNormalizer HttpMsgHeadShared::NORMALIZER_BASIC { EVENT__NONE, INF__NONE, false, nullptr, nullptr, nullptr }; @@ -224,10 +240,10 @@ const HeaderNormalizer* const HttpMsgHeadShared::header_norms[HEAD__MAX_VALUE + &NORMALIZER_DATE, // HEAD_DATE &NORMALIZER_TOKEN_LIST, // HEAD_PRAGMA &NORMALIZER_TOKEN_LIST, // HEAD_TRAILER - &NORMALIZER_BASIC, //HEAD_COOKIE - &NORMALIZER_BASIC, //HEAD_SET_COOKIE + &NORMALIZER_BASIC, // HEAD_COOKIE + &NORMALIZER_BASIC, // HEAD_SET_COOKIE &NORMALIZER_TOKEN_LIST, // HEAD_TRANSFER_ENCODING - &NORMALIZER_BASIC, // HEAD_UPGRADE + &NORMALIZER_TOKEN_LIST, // HEAD_UPGRADE &NORMALIZER_BASIC, // HEAD_VIA &NORMALIZER_BASIC, // HEAD_WARNING &NORMALIZER_TOKEN_LIST, // HEAD_ACCEPT @@ -275,6 +291,7 @@ const HeaderNormalizer* const HttpMsgHeadShared::header_norms[HEAD__MAX_VALUE + &NORMALIZER_BASIC, // HEAD_MIME_VERSION &NORMALIZER_BASIC, // HEAD_PROXY_AGENT &NORMALIZER_BASIC, // HEAD_CONTENT_DISPOSITION + &NORMALIZER_TOKEN_LIST, // HEAD_HTTP2_SETTINGS &NORMALIZER_BASIC, // HEAD__MAX_VALUE &NORMALIZER_BASIC, // HEAD_CUSTOM_XFF_HEADER &NORMALIZER_BASIC, // HEAD_CUSTOM_XFF_HEADER @@ -408,6 +425,8 @@ const RuleMap HttpModule::http_events[] = { EVENT_TRUNCATED_MSG_BODY_CL, "HTTP Content-Length message body was truncated" }, { EVENT_TRUNCATED_MSG_BODY_CHUNK, "HTTP chunked message body was truncated" }, { EVENT_LONG_SCHEME, "HTTP URI scheme longer than 10 characters" }, + { EVENT_HTTP2_UPGRADE_REQUEST, "HTTP/1 client requested HTTP/2 upgrade" }, + { EVENT_HTTP2_UPGRADE_RESPONSE, "HTTP/1 server granted HTTP/2 upgrade" }, { 0, nullptr } };