diff --git a/rust/src/util.rs b/rust/src/util.rs index d7109464f773..85b65aa71859 100644 --- a/rust/src/util.rs +++ b/rust/src/util.rs @@ -20,7 +20,103 @@ 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 rs_validate_domain(input: *const u8, in_len: u32) -> u32 { + let islice = build_slice!(input, in_len as usize); + match parse_domain(islice) { + Ok((rem, _)) => { + 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 r0 = parse_domain(buf0); + match r0 { + Ok((rem, _)) => { + // And we should have 5 bytes left. + assert_eq!(rem.len(), 5); + } + _ => { + panic!("Result should have been ok."); + } + } + let buf1: &[u8] = "justatext".as_bytes(); + let r1 = parse_domain(buf1); + match r1 { + Ok((_, _)) => { + panic!("Result should not have been ok."); + } + _ => {} + } + let buf1: &[u8] = "1.com".as_bytes(); + let r1 = parse_domain(buf1); + match r1 { + Ok((_, _)) => { + panic!("Result should not have been ok."); + } + _ => {} + } + let buf1: &[u8] = "a-.com".as_bytes(); + let r1 = parse_domain(buf1); + match r1 { + Ok((_, _)) => { + panic!("Result should not have been ok."); + } + _ => {} + } + let buf1: &[u8] = "a(x)y.com".as_bytes(); + let r1 = parse_domain(buf1); + match r1 { + Ok((_, _)) => { + panic!("Result should not have been ok."); + } + _ => {} + } + } +} diff --git a/src/app-layer-ftp.c b/src/app-layer-ftp.c index d2777198ab4e..b631d1256ad3 100644 --- a/src/app-layer-ftp.c +++ b/src/app-layer-ftp.c @@ -961,6 +961,35 @@ 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; + } + AppProto r = ALPROTO_UNKNOWN; + 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 + r = ALPROTO_FTP; + } + for (uint32_t i = 4; i < len; i++) { + if (input[i] == '\n') { + return r; + } + } + return ALPROTO_UNKNOWN; +} + static int FTPRegisterPatternsForProtocolDetection(void) { if (AppLayerProtoDetectPMRegisterPatternCI( @@ -983,7 +1012,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 5a4fc0c7a2c9..d9c7ea955631 100644 --- a/src/app-layer-smtp.c +++ b/src/app-layer-smtp.c @@ -1725,6 +1725,50 @@ 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) { + for (uint32_t i = 4; i < len; i++) { + if (input[i] == '\n') { + 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 = rs_validate_domain(input + 4, len - 4); + if (offset == 0) { + return ALPROTO_FAILED; + } + for (uint32_t i = offset + 4; i < len; i++) { + if (input[i] == '\n') { + 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, @@ -1742,6 +1786,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; }