From: Philippe Antoine Date: Tue, 28 Jun 2022 19:34:24 +0000 (+0200) Subject: smtp: adds server side detection X-Git-Tag: suricata-8.0.0-beta1~1005 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=cc3dde8ada54e55ce62ec325787ab1058c37fc53;p=thirdparty%2Fsuricata.git smtp: adds server side detection Ticket: #1125 --- diff --git a/rust/src/util.rs b/rust/src/util.rs index d7109464f7..d3f76e41eb 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-smtp.c b/src/app-layer-smtp.c index c03ff75cd0..b645e77676 100644 --- a/src/app-layer-smtp.c +++ b/src/app-layer-smtp.c @@ -1657,6 +1657,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, @@ -1674,6 +1714,12 @@ 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); + } return 0; }