From: Umang Sharma (umasharm) Date: Thu, 14 Aug 2025 21:08:21 +0000 (+0000) Subject: Pull request #4808: imap: abort fallback functionality X-Git-Tag: 3.9.5.0~18 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=fbdf15187997c3007d239808c09a2f84096f2689;p=thirdparty%2Fsnort3.git Pull request #4808: imap: abort fallback functionality Merge in SNORT/snort3 from ~UMASHARM/snort3:imap_abort to master Squashed commit of the following: commit 4dac91772f004283b3ea40ab1428def2483adf7a Author: Umang Sharma Date: Tue Jun 10 15:10:21 2025 -0400 imap: abort fallback functionality --- diff --git a/src/service_inspectors/imap/imap.cc b/src/service_inspectors/imap/imap.cc index 89abfc384..4ca71f93f 100644 --- a/src/service_inspectors/imap/imap.cc +++ b/src/service_inspectors/imap/imap.cc @@ -113,14 +113,27 @@ IMAPToken imap_resps[] = { "RECENT", 6, RESP_RECENT }, { "EXPUNGE", 7, RESP_EXPUNGE }, { "FETCH", 5, RESP_FETCH }, + { "LOGIN", 5, RESP_LOGIN }, + { "LOGOUT", 6, RESP_LOGOUT }, + { "SELECT", 6, RESP_SELECT }, + { "EXAMINE", 7, RESP_EXAMINE }, + { "CREATE", 6, RESP_CREATE }, + { "DELETE", 6, RESP_DELETE }, + { "RENAME", 6, RESP_RENAME }, + { "SUBSCRIBE", 9, RESP_SUBSCRIBE }, + { "UNSUBSCRIBE", 11, RESP_UNSUBSCRIBE }, + { "APPEND", 6, RESP_APPEND }, + { "COPY", 4, RESP_COPY }, { "BAD", 3, RESP_BAD }, { "BYE", 3, RESP_BYE }, { "NO", 2, RESP_NO }, { "OK", 2, RESP_OK }, { "PREAUTH", 7, RESP_PREAUTH }, { "ENVELOPE", 8, RESP_ENVELOPE }, + { "NAMESPACE", 9, RESP_NAMESPACE }, { "UID", 3, RESP_UID }, - { nullptr, 0, 0 } + + { nullptr, 0, 0 } }; SearchTool* imap_resp_search_mpse = nullptr; diff --git a/src/service_inspectors/imap/imap.h b/src/service_inspectors/imap/imap.h index 637bca276..264590d63 100644 --- a/src/service_inspectors/imap/imap.h +++ b/src/service_inspectors/imap/imap.h @@ -111,12 +111,24 @@ typedef enum _IMAPRespEnum RESP_RECENT, RESP_EXPUNGE, RESP_FETCH, + RESP_LOGIN, + RESP_LOGOUT, + RESP_SELECT, + RESP_EXAMINE, + RESP_CREATE, + RESP_DELETE, + RESP_RENAME, + RESP_SUBSCRIBE, + RESP_UNSUBSCRIBE, + RESP_APPEND, + RESP_COPY, RESP_BAD, RESP_BYE, RESP_NO, RESP_OK, RESP_PREAUTH, RESP_ENVELOPE, + RESP_NAMESPACE, RESP_UID, RESP_LAST } IMAPRespEnum; diff --git a/src/service_inspectors/imap/imap_paf.cc b/src/service_inspectors/imap/imap_paf.cc index 363cc61d4..82da737e9 100644 --- a/src/service_inspectors/imap/imap_paf.cc +++ b/src/service_inspectors/imap/imap_paf.cc @@ -47,11 +47,16 @@ static inline void reset_data_states(ImapPafData* pfdata) // reset server info pfdata->imap_state = IMAP_PAF_CMD_IDENTIFIER; + pfdata->data_end_state = IMAP_PAF_DATA_END_UNKNOWN; + pfdata->end_of_data = false; - // reset fetch data information information + // reset fetch data information pfdata->imap_data_info.paren_cnt = 0; pfdata->imap_data_info.next_letter = nullptr; + pfdata->imap_data_info.found_len = false; pfdata->imap_data_info.length = 0; + pfdata->imap_data_info.esc_nxt_char = false; + pfdata->imap_data_info.cmd = nullptr; } static inline bool is_untagged(const uint8_t ch) @@ -214,6 +219,38 @@ static bool find_data_end_mime_data(const uint8_t ch, ImapPafData* pfdata) return false; } +/* + * This function only does something when the character is a blank or a CR/LF. + * In those specific cases, this function will set the appropriate next + * state information + * + * PARAMS: + * const uint8_t ch - the next character to analyze. + * ImapPafData *pfdata - the struct containing all imap paf information + * ImapPafData base_state - if a space is not found, revert to this state + * ImapPafData next_state - if a space is found, go to this state + * RETURNS: + * true - if the status has been eaten + * false - if a CR or LF has been found + */ +static inline void eat_character(const uint8_t ch, ImapPafData* pfdata, + ImapPafState base_state, ImapPafState next_state) +{ + switch (ch) + { + case ' ': + case '\t': + case '[': + pfdata->imap_state = next_state; + break; + + case '\r': + case '\n': + pfdata->imap_state = base_state; + break; + } +} + /* * Initial command processing function. Determine if this command * may be analyzed irregularly ( which currently means if emails @@ -223,14 +260,134 @@ static bool find_data_end_mime_data(const uint8_t ch, ImapPafData* pfdata) * const uint8_t ch - the next character to analyze. * ImapPafData *pfdata - the struct containing all imap paf information */ -static inline void init_command_search(const uint8_t ch, ImapPafData* pfdata) +static inline void init_command_search(const uint8_t ch, const uint8_t next_ch, ImapPafData* pfdata) { switch (ch) { + case '[': + eat_character(ch, pfdata, IMAP_PAF_REG_STATE, IMAP_PAF_CMD_SEARCH); + break; case 'F': case 'f': - // may be a FETCH response - pfdata->imap_data_info.next_letter = &(imap_resps[RESP_FETCH].name[1]); + if (pfdata->imap_state == IMAP_PAF_CMD_STATUS) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_FLAGS].name[1]); + } + else if (pfdata->imap_state == IMAP_PAF_CMD_SEARCH) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_FETCH].name[1]); + pfdata->imap_data_info.cmd = imap_resps[RESP_FETCH].name; + } + break; + + case 'N': + case 'n': + if (imap_resps[RESP_NAMESPACE].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_NAMESPACE].name[1]); + } + break; + + case 'L': + case 'l': + if (imap_resps[RESP_LIST].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_LIST].name[1]); + } + else if (imap_resps[RESP_LSUB].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_LSUB].name[1]); + } + break; + + case 'S': + case 's': + if (imap_resps[RESP_SEARCH].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_SEARCH].name[1]); + } + else if (imap_resps[RESP_STATUS].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_STATUS].name[1]); + } + else if (imap_resps[RESP_SUBSCRIBE].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_SUBSCRIBE].name[1]); + } + break; + + case 'C': + case 'c': + if (imap_resps[RESP_CREATE].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_CREATE].name[1]); + } + else if (imap_resps[RESP_CAPABILITY].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_CAPABILITY].name[1]); + } + break; + + case 'D': + case 'd': + if (imap_resps[RESP_DELETE].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_DELETE].name[1]); + } + break; + + case 'E': + case 'e': + if (imap_resps[RESP_EXISTS].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_EXISTS].name[1]); + } + else if (imap_resps[RESP_EXPUNGE].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_EXPUNGE].name[1]); + } + + break; + + case 'R': + case 'r': + if (imap_resps[RESP_RENAME].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_RENAME].name[1]); + } + break; + + case 'A': + case 'a': + if (imap_resps[RESP_APPEND].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_APPEND].name[1]); + } + break; + + case 'B': + case 'b': + if (imap_resps[RESP_BAD].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_BAD].name[1]); + } + else if (imap_resps[RESP_BYE].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_BYE].name[1]); + } + break; + + case 'O': + case 'o': + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_OK].name[1]); + break; + + case 'U': + case 'u': + if (imap_resps[RESP_UID].name[1] == next_ch) + { + pfdata->imap_data_info.next_letter = &(imap_resps[RESP_UID].name[1]); + } break; default: @@ -254,8 +411,21 @@ static inline void parse_command(const uint8_t ch, ImapPafData* pfdata) char val = *(pfdata->imap_data_info.next_letter); if (val == '\0' && isblank(ch)) - pfdata->imap_state = IMAP_PAF_DATA_HEAD_STATE; - + { + if (pfdata->imap_state == IMAP_PAF_CMD_STATUS) + { + pfdata->imap_state = IMAP_PAF_CMD_SEARCH; + pfdata->imap_data_info.next_letter = nullptr; + return; + } + + if (pfdata->imap_state == IMAP_PAF_CMD_SEARCH) + { + pfdata->imap_state = (pfdata->imap_data_info.cmd == imap_resps[RESP_FETCH].name) + ? IMAP_PAF_DATA_HEAD_STATE + : IMAP_PAF_VAL_STATE; + } + } else if (toupper(ch) == toupper(val)) pfdata->imap_data_info.next_letter++; @@ -272,57 +442,30 @@ static inline void parse_command(const uint8_t ch, ImapPafData* pfdata) * const uint8_t ch - the next character to analyze. * ImapPafData *pfdata - the struct containing all imap paf information */ -static inline void process_command(const uint8_t ch, ImapPafData* pfdata) +static inline void process_command(const uint8_t ch, const uint8_t next_ch, ImapPafData* pfdata) { if (pfdata->imap_data_info.next_letter) parse_command(ch, pfdata); else - init_command_search(ch, pfdata); + init_command_search(ch, next_ch, pfdata); } -/* - * This function only does something when the character is a blank or a CR/LF. - * In those specific cases, this function will set the appropriate next - * state information - * - * PARAMS: - * const uint8_t ch - the next character to analyze. - * ImapPafData *pfdata - the struct containing all imap paf information - * ImapPafData base_state - if a space is not found, revert to this state - * ImapPafData next_state - if a space is found, go to this state - * RETURNS: - * true - if the status has been eaten - * false - if a CR or LF has been found - */ -static inline void eat_character(const uint8_t ch, ImapPafData* pfdata, - ImapPafState base_state, ImapPafState next_state) +static inline void process_second_argument(const uint8_t ch, const uint8_t next_ch, ImapPafData* pfdata) { - switch (ch) + if (isdigit(ch)) + return; + else if (isblank(ch)) { - case ' ': - case '\t': - pfdata->imap_state = next_state; - break; - - case '\r': - case '\n': - pfdata->imap_state = base_state; - break; + pfdata->imap_state = IMAP_PAF_CMD_SEARCH; + return; } -} -/* - * defined above in the eat_character function - * - * Keeping the next two functions to ease any future development - * where these cases will no longer be simple or identical - */ -static inline void eat_second_argument(const uint8_t ch, ImapPafData* pfdata) -{ - eat_character(ch, pfdata, IMAP_PAF_REG_STATE, IMAP_PAF_CMD_SEARCH); + if (pfdata->imap_data_info.next_letter) + parse_command(ch, pfdata); + else + init_command_search(ch, next_ch, pfdata); } -/* explanation in 'eat_second_argument' above */ static inline void eat_response_identifier(const uint8_t ch, ImapPafData* pfdata) { eat_character(ch, pfdata, IMAP_PAF_REG_STATE, IMAP_PAF_CMD_STATUS); @@ -351,9 +494,14 @@ static StreamSplitter::Status imap_paf_server(ImapPafData* pfdata, pfdata->end_of_data = false; + bool saw_reg_state = false; for (i = 0; i < len; i++) { uint8_t ch = data[i]; + uint8_t next_ch = 0; + if(i+1 < len) + next_ch = data[i+1]; + switch (pfdata->imap_state) { case IMAP_PAF_CMD_IDENTIFIER: @@ -376,16 +524,23 @@ static StreamSplitter::Status imap_paf_server(ImapPafData* pfdata, case IMAP_PAF_CMD_STATUS: // can be a command name, msg sequence number, msg count, etc... - // since we are only interested in fetch, eat this argument - eat_second_argument(ch, pfdata); + process_second_argument(ch, next_ch, pfdata); + find_data_end_single_line(ch, pfdata); + break; case IMAP_PAF_CMD_SEARCH: - process_command(ch, pfdata); + process_command(ch, next_ch, pfdata); find_data_end_single_line(ch, pfdata); break; + + case IMAP_PAF_VAL_STATE: + pfdata->server_bytes_seen = 0; + reset_data_states(pfdata); + return StreamSplitter::SEARCH; case IMAP_PAF_REG_STATE: + saw_reg_state = true; find_data_end_single_line(ch, pfdata); // data reset when end of line hit break; @@ -424,6 +579,19 @@ static StreamSplitter::Status imap_paf_server(ImapPafData* pfdata, } } + if( saw_reg_state ) + { + pfdata->server_bytes_seen += len; + if (pfdata->server_bytes_seen > IMAP_MAX_OCTETS) + { + return StreamSplitter::ABORT; + } + } + else + { + pfdata->server_bytes_seen = 0; + } + if (flush_len) { // flush at the final termination sequence @@ -451,15 +619,96 @@ static StreamSplitter::Status imap_paf_server(ImapPafData* pfdata, * StreamSplitter::Status - StreamSplitter::FLUSH if flush point found, * StreamSplitter::SEARCH otherwise */ -static StreamSplitter::Status imap_paf_client(const uint8_t* data, uint32_t len, uint32_t* fp) + +static StreamSplitter::Status imap_paf_client(ImapPafData* pfdata, const uint8_t* data, uint32_t len, uint32_t* fp) { - const char* pch; + uint32_t i; + uint32_t flush_len = 0; + ImapClientState client_state = IMAP_CLIENT_FIRST_CHAR; + bool is_valid_command = false; + bool is_valid_base64 = len >= MIN_BASE64_LEN; + bool found_space = false; + + for (i = 0; i < len; i++) + { + uint8_t ch = data[i]; + + if (ch == '\n') + { + flush_len = i + 1; + break; + } + else if (ch == '\r') + { + continue; + } - pch = (const char *)memchr (data, '\n', len); + switch (client_state) + { + case IMAP_CLIENT_FIRST_CHAR: + if (isalnum(ch)) + { + is_valid_command = true; + client_state = IMAP_CLIENT_COMMAND_TAG; + } + else + { + is_valid_command = false; + client_state = is_valid_base64 ? IMAP_CLIENT_BASE64_CHECK : IMAP_CLIENT_FLUSH_LINE; + } + break; - if (pch != nullptr) + case IMAP_CLIENT_COMMAND_TAG: + if (ch == ' ') + { + found_space = true; + client_state = IMAP_CLIENT_FLUSH_LINE; + } + else if (!isalnum(ch) && ch != '-') + { + is_valid_command = false; + client_state = is_valid_base64 ? IMAP_CLIENT_BASE64_CHECK : IMAP_CLIENT_FLUSH_LINE; + } + break; + + case IMAP_CLIENT_BASE64_CHECK: + if (i < MIN_BASE64_LEN) + { + if (!(isalnum(ch) || ch == '+' || ch == '/' || ch == '=')) + { + is_valid_base64 = false; + client_state = IMAP_CLIENT_FLUSH_LINE; + } + } + break; + + case IMAP_CLIENT_FLUSH_LINE: + break; + } + } + + is_valid_command = is_valid_command && found_space; + bool is_valid = is_valid_command || is_valid_base64; + uint32_t check_len = flush_len ? flush_len : len; + + if (is_valid) + { + pfdata->client_bytes_seen = 0; + } + else { - *fp = (uint32_t)(pch - (const char*)data) + 1; + pfdata->client_bytes_seen += check_len; + + if (pfdata->client_bytes_seen > IMAP_MAX_OCTETS) + { + pfdata->client_bytes_seen = 0; + return StreamSplitter::ABORT; + } + } + + if (flush_len) + { + *fp = flush_len; return StreamSplitter::FLUSH; } return StreamSplitter::SEARCH; @@ -505,7 +754,7 @@ StreamSplitter::Status ImapSplitter::scan( if (flags & PKT_FROM_SERVER) return imap_paf_server(pfdata, data, len, fp); else - return imap_paf_client(data, len, fp); + return imap_paf_client(pfdata, data, len, fp); } bool imap_is_data_end(Flow* ssn) diff --git a/src/service_inspectors/imap/imap_paf.h b/src/service_inspectors/imap/imap_paf.h index b98db9036..5c63e7e06 100644 --- a/src/service_inspectors/imap/imap_paf.h +++ b/src/service_inspectors/imap/imap_paf.h @@ -27,6 +27,9 @@ #include "mime/file_mime_paf.h" #include "stream/stream_splitter.h" +#define IMAP_MAX_OCTETS 5120 +#define MIN_BASE64_LEN 4 + struct ImapDataInfo { int paren_cnt; // The open parentheses count in fetch @@ -34,12 +37,15 @@ struct ImapDataInfo bool found_len; uint32_t length; bool esc_nxt_char; // true if the next character has been escaped + const char* cmd; + }; -// States for IMAP PAF +// States for IMAP PAF (Server) typedef enum _ImapPafState { IMAP_PAF_REG_STATE, // default state. eat until LF + IMAP_PAF_VAL_STATE, // parse a valid command IMAP_PAF_DATA_HEAD_STATE, // parses the fetch header IMAP_PAF_DATA_LEN_STATE, // parse the literal length IMAP_PAF_DATA_STATE, // search for and flush on MIME boundaries @@ -50,6 +56,16 @@ typedef enum _ImapPafState IMAP_PAF_CMD_SEARCH // currently searching data for a command } ImapPafState; +// States for IMAP PAF (Client) +typedef enum _ImapClientState +{ + IMAP_CLIENT_FIRST_CHAR, // analyzing first character + IMAP_CLIENT_COMMAND_TAG, // parsing command tag/name + IMAP_CLIENT_BASE64_CHECK, // checking for base64-like data + IMAP_CLIENT_FLUSH_LINE // ready to flush at newline +} ImapClientState; + + typedef enum _ImapDataEnd { IMAP_PAF_DATA_END_UNKNOWN, @@ -64,6 +80,8 @@ struct ImapPafData ImapDataInfo imap_data_info; // Used for parsing data ImapDataEnd data_end_state; bool end_of_data; + uint32_t server_bytes_seen; + uint32_t client_bytes_seen; }; class ImapSplitter : public snort::StreamSplitter