diff --git a/rust/src/util.rs b/rust/src/util.rs index d7109464f773..d3f76e41eb7c 100644 --- a/rust/src/util.rs +++ b/rust/src/util.rs @@ -20,7 +20,68 @@ use std::ffi::CStr; use std::os::raw::c_char; +use nom7::bytes::complete::take_while1; +use nom7::character::complete::char; +use nom7::character::{is_alphabetic, is_alphanumeric}; +use nom7::combinator::verify; +use nom7::multi::many1_count; +use nom7::IResult; + #[no_mangle] pub unsafe extern "C" fn rs_check_utf8(val: *const c_char) -> bool { CStr::from_ptr(val).to_str().is_ok() } + +fn is_alphanumeric_or_hyphen(chr: u8) -> bool { + return is_alphanumeric(chr) || chr == b'-'; +} + +fn parse_domain_label(i: &[u8]) -> IResult<&[u8], ()> { + let (i, _) = verify(take_while1(is_alphanumeric_or_hyphen), |x: &[u8]| { + is_alphabetic(x[0]) && x[x.len() - 1] != b'-' + })(i)?; + return Ok((i, ())); +} + +fn parse_subdomain(input: &[u8]) -> IResult<&[u8], ()> { + let (input, _) = parse_domain_label(input)?; + let (input, _) = char('.')(input)?; + return Ok((input, ())); +} + +fn parse_domain(input: &[u8]) -> IResult<&[u8], ()> { + let (input, _) = many1_count(parse_subdomain)(input)?; + let (input, _) = parse_domain_label(input)?; + return Ok((input, ())); +} + +#[no_mangle] +pub unsafe extern "C" fn SCValidateDomain(input: *const u8, in_len: u32) -> u32 { + let islice = build_slice!(input, in_len as usize); + if let Ok((rem, _)) = parse_domain(islice) { + return (islice.len() - rem.len()) as u32; + } + return 0; +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_parse_domain() { + let buf0: &[u8] = "a-1.oisf.net more".as_bytes(); + let (rem, _) = parse_domain(buf0).unwrap(); + // And we should have 5 bytes left. + assert_eq!(rem.len(), 5); + let buf1: &[u8] = "justatext".as_bytes(); + assert!(parse_domain(buf1).is_err()); + let buf1: &[u8] = "1.com".as_bytes(); + assert!(parse_domain(buf1).is_err()); + let buf1: &[u8] = "a-.com".as_bytes(); + assert!(parse_domain(buf1).is_err()); + let buf1: &[u8] = "a(x)y.com".as_bytes(); + assert!(parse_domain(buf1).is_err()); + } +} diff --git a/src/app-layer-ftp.c b/src/app-layer-ftp.c index 7384e732e533..1fb03c78717e 100644 --- a/src/app-layer-ftp.c +++ b/src/app-layer-ftp.c @@ -969,6 +969,31 @@ static AppProto FTPUserProbingParser( return ALPROTO_FTP; } +static AppProto FTPServerProbingParser( + Flow *f, uint8_t direction, const uint8_t *input, uint32_t len, uint8_t *rdir) +{ + // another check for minimum length + if (len < 5) { + return ALPROTO_UNKNOWN; + } + // begins by 220 + if (input[0] != '2' || input[1] != '2' || input[2] != '0') { + return ALPROTO_FAILED; + } + // followed by space or hypen + if (input[3] != ' ' && input[3] != '-') { + return ALPROTO_FAILED; + } + if (f->alproto_ts == ALPROTO_FTP || (f->todstbytecnt > 4 && f->alproto_ts == ALPROTO_UNKNOWN)) { + // only validates FTP if client side was FTP + // or if client side is unknown despite having received bytes + if (memchr(input + 4, '\n', len - 4) != NULL) { + return ALPROTO_FTP; + } + } + return ALPROTO_UNKNOWN; +} + static int FTPRegisterPatternsForProtocolDetection(void) { if (AppLayerProtoDetectPMRegisterPatternCI( @@ -991,7 +1016,15 @@ static int FTPRegisterPatternsForProtocolDetection(void) IPPROTO_TCP, ALPROTO_FTP, "PORT ", 5, 0, STREAM_TOSERVER) < 0) { return -1; } - + // Only check FTP on known ports as the banner has nothing special beyond + // the response code shared with SMTP. + if (!AppLayerProtoDetectPPParseConfPorts( + "tcp", IPPROTO_TCP, "ftp", ALPROTO_FTP, 0, 5, NULL, FTPServerProbingParser)) { + // STREAM_TOSERVER here means use 21 as flow destination port + // and NULL, FTPServerProbingParser means use probing parser to client + AppLayerProtoDetectPPRegister(IPPROTO_TCP, "21", ALPROTO_FTP, 0, 5, STREAM_TOSERVER, NULL, + FTPServerProbingParser); + } return 0; } diff --git a/src/app-layer-smtp.c b/src/app-layer-smtp.c index 944187aa4df7..64e2cdded593 100644 --- a/src/app-layer-smtp.c +++ b/src/app-layer-smtp.c @@ -178,10 +178,14 @@ enum SMTPCode { SMTP_REPLY_334, SMTP_REPLY_354, + SMTP_REPLY_401, // Unauthorized + SMTP_REPLY_402, // Command not implemented SMTP_REPLY_421, + SMTP_REPLY_435, // Your account has not yet been verified SMTP_REPLY_450, SMTP_REPLY_451, SMTP_REPLY_452, + SMTP_REPLY_454, // Temporary authentication failure SMTP_REPLY_455, SMTP_REPLY_500, @@ -189,6 +193,15 @@ enum SMTPCode { SMTP_REPLY_502, SMTP_REPLY_503, SMTP_REPLY_504, + SMTP_REPLY_511, // Bad email address + SMTP_REPLY_521, // Server does not accept mail + SMTP_REPLY_522, // Recipient has exceeded mailbox limit + SMTP_REPLY_525, // User Account Disabled + SMTP_REPLY_530, // Authentication required + SMTP_REPLY_534, // Authentication mechanism is too weak + SMTP_REPLY_535, // Authentication credentials invalid + SMTP_REPLY_541, // No response from host + SMTP_REPLY_543, // Routing server failure. No available route SMTP_REPLY_550, SMTP_REPLY_551, SMTP_REPLY_552, @@ -197,7 +210,7 @@ enum SMTPCode { SMTP_REPLY_555, }; -SCEnumCharMap smtp_reply_map[ ] = { +SCEnumCharMap smtp_reply_map[] = { { "211", SMTP_REPLY_211 }, { "214", SMTP_REPLY_214 }, { "220", SMTP_REPLY_220 }, @@ -210,10 +223,15 @@ SCEnumCharMap smtp_reply_map[ ] = { { "334", SMTP_REPLY_334 }, { "354", SMTP_REPLY_354 }, + { "401", SMTP_REPLY_401 }, + { "402", SMTP_REPLY_402 }, { "421", SMTP_REPLY_421 }, + { "435", SMTP_REPLY_435 }, { "450", SMTP_REPLY_450 }, { "451", SMTP_REPLY_451 }, { "452", SMTP_REPLY_452 }, + { "454", SMTP_REPLY_454 }, + // { "4.7.0", SMTP_REPLY_454 }, // rfc4954 { "455", SMTP_REPLY_455 }, { "500", SMTP_REPLY_500 }, @@ -221,13 +239,22 @@ SCEnumCharMap smtp_reply_map[ ] = { { "502", SMTP_REPLY_502 }, { "503", SMTP_REPLY_503 }, { "504", SMTP_REPLY_504 }, + { "511", SMTP_REPLY_511 }, + { "521", SMTP_REPLY_521 }, + { "522", SMTP_REPLY_522 }, + { "525", SMTP_REPLY_525 }, + { "530", SMTP_REPLY_530 }, + { "534", SMTP_REPLY_534 }, + { "535", SMTP_REPLY_535 }, + { "541", SMTP_REPLY_541 }, + { "543", SMTP_REPLY_543 }, { "550", SMTP_REPLY_550 }, { "551", SMTP_REPLY_551 }, { "552", SMTP_REPLY_552 }, { "553", SMTP_REPLY_553 }, { "554", SMTP_REPLY_554 }, { "555", SMTP_REPLY_555 }, - { NULL, -1 }, + { NULL, -1 }, }; /* Create SMTP config structure */ @@ -1417,7 +1444,7 @@ static AppLayerResult SMTPParse(uint8_t direction, Flow *f, SMTPState *state, AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TC)))) { SCReturnStruct(APP_LAYER_OK); } else if (input_buf == NULL || input_len == 0) { - SCReturnStruct(APP_LAYER_ERROR); + SCReturnStruct(APP_LAYER_OK); } SMTPInput input = { .buf = input_buf, .len = input_len, .orig_len = input_len, .consumed = 0 }; @@ -1713,6 +1740,46 @@ static int SMTPStateGetEventInfoById(int event_id, const char **event_name, return 0; } +static AppProto SMTPServerProbingParser( + Flow *f, uint8_t direction, const uint8_t *input, uint32_t len, uint8_t *rdir) +{ + // another check for minimum length + if (len < 5) { + return ALPROTO_UNKNOWN; + } + // begins by 220 + if (input[0] != '2' || input[1] != '2' || input[2] != '0') { + return ALPROTO_FAILED; + } + // followed by space or hypen + if (input[3] != ' ' && input[3] != '-') { + return ALPROTO_FAILED; + } + // If client side is SMTP, do not validate domain + // so that server banner can be parsed first. + if (f->alproto_ts == ALPROTO_SMTP) { + if (memchr(input + 4, '\n', len - 4) != NULL) { + return ALPROTO_SMTP; + } + return ALPROTO_UNKNOWN; + } + AppProto r = ALPROTO_UNKNOWN; + if (f->todstbytecnt > 4 && f->alproto_ts == ALPROTO_UNKNOWN) { + // Only validates SMTP if client side is unknown + // despite having received bytes. + r = ALPROTO_SMTP; + } + uint32_t offset = SCValidateDomain(input + 4, len - 4); + if (offset == 0) { + return ALPROTO_FAILED; + } + if (r != ALPROTO_UNKNOWN && memchr(input + 4, '\n', len - 4) != NULL) { + return r; + } + // This should not go forever because of engine limiting probing parsers. + return ALPROTO_UNKNOWN; +} + static int SMTPRegisterPatternsForProtocolDetection(void) { if (AppLayerProtoDetectPMRegisterPatternCI(IPPROTO_TCP, ALPROTO_SMTP, @@ -1730,6 +1797,20 @@ static int SMTPRegisterPatternsForProtocolDetection(void) { return -1; } + if (!AppLayerProtoDetectPPParseConfPorts( + "tcp", IPPROTO_TCP, "smtp", ALPROTO_SMTP, 0, 5, NULL, SMTPServerProbingParser)) { + // STREAM_TOSERVER means here use 25 as flow destination port + AppLayerProtoDetectPPRegister(IPPROTO_TCP, "25", ALPROTO_SMTP, 0, 5, STREAM_TOSERVER, NULL, + SMTPServerProbingParser); + } + if (AppLayerProtoDetectPMRegisterPatternCSwPP(IPPROTO_TCP, ALPROTO_SMTP, "220 ", 4, 0, + STREAM_TOCLIENT, SMTPServerProbingParser, 5, 5) < 0) { + return -1; + } + if (AppLayerProtoDetectPMRegisterPatternCSwPP(IPPROTO_TCP, ALPROTO_SMTP, "220-", 4, 0, + STREAM_TOCLIENT, SMTPServerProbingParser, 5, 5) < 0) { + return -1; + } return 0; }