diff --git a/rust/src/detect/transforms/urldecode.rs b/rust/src/detect/transforms/urldecode.rs index e693080d72..da89e62223 100644 --- a/rust/src/detect/transforms/urldecode.rs +++ b/rust/src/detect/transforms/urldecode.rs @@ -41,7 +41,8 @@ fn hex_value(i: u8) -> Option { _ => None, } } -fn url_decode_transform_do(input: &[u8], output: &mut [u8]) -> u32 { + +pub(crate) fn url_decode_transform_do(input: &[u8], output: &mut [u8], plus_space: bool) -> u32 { let mut state = (0u8, 0u8); let mut nb = 0; for &i in input.iter() { @@ -68,7 +69,7 @@ fn url_decode_transform_do(input: &[u8], output: &mut [u8]) -> u32 { } else if i == b'%' { state = (1u8, 0u8); } else { - if i == b'+' { + if i == b'+' && plus_space { output[nb] = b' '; } else { output[nb] = i; @@ -104,7 +105,7 @@ unsafe extern "C" fn url_decode_transform( } let output = std::slice::from_raw_parts_mut(output, input_len as usize); - let out_len = url_decode_transform_do(input, output); + let out_len = url_decode_transform_do(input, output, true); SCInspectionBufferTruncate(buffer, out_len); } @@ -138,11 +139,11 @@ mod tests { let mut buf = Vec::new(); buf.extend_from_slice(b"Suricata%20is+%27%61wesome%21%27%25%30%30%ZZ%4"); let mut out = vec![0; buf.len()]; - let nb = url_decode_transform_do(&buf, &mut out); + let nb = url_decode_transform_do(&buf, &mut out, true); assert_eq!(&out[..nb as usize], b"Suricata is 'awesome!'%00%ZZ%4"); // test in place let still_buf = unsafe { std::slice::from_raw_parts(buf.as_ptr(), buf.len()) }; - let nb = url_decode_transform_do(still_buf, &mut buf); + let nb = url_decode_transform_do(still_buf, &mut buf, true); assert_eq!(&still_buf[..nb as usize], b"Suricata is 'awesome!'%00%ZZ%4"); } } diff --git a/rust/src/mime/mime.rs b/rust/src/mime/mime.rs index fd4c8f61e7..bd65bf3dbd 100644 --- a/rust/src/mime/mime.rs +++ b/rust/src/mime/mime.rs @@ -16,6 +16,7 @@ */ use crate::common::nom8::take_until_and_consume; +use crate::detect::transforms::urldecode::url_decode_transform_do; use nom8::branch::alt; use nom8::bytes::complete::{tag, take, take_till, take_until, take_while}; use nom8::character::complete::char; @@ -25,32 +26,41 @@ use nom8::{Err, IResult, Parser}; use std; use std::collections::HashMap; -#[derive(Clone)] -pub struct HeaderTokens<'a> { - pub tokens: HashMap<&'a [u8], &'a [u8]>, +fn mime_skip_value_delimited(input: &[u8]) -> IResult<&[u8], &[u8]> { + let (input, _) = char('"').parse(input)?; + let mut escaping = false; + for i in 0..input.len() { + if escaping { + escaping = false; + } else if input[i] == b'\\' { + escaping = true; + continue; + } else if input[i] == b'"' { + return Ok((&input[i + 1..], &input[..i])); + } + } + return Err(Err::Error(make_error(input, ErrorKind::Eof))); } -fn mime_parse_value_delimited(input: &[u8]) -> IResult<&[u8], &[u8]> { +fn mime_parse_value_delimited(input: &[u8]) -> IResult<&[u8], Vec> { let (input, _) = char('"').parse(input)?; let mut escaping = false; + let mut r = Vec::new(); for i in 0..input.len() { - if input[i] == b'\\' { - escaping = true; - } else { - if input[i] == b'"' && !escaping { - return Ok((&input[i + 1..], &input[..i])); - } - // unescape can be processed later + if escaping { escaping = false; + } else if input[i] == b'\\' { + escaping = true; + continue; + } else if input[i] == b'"' { + return Ok((&input[i + 1..], r)); } + r.push(input[i]); } - // should fail - let (input, value) = take_until("\"").parse(input)?; - let (input, _) = char('"').parse(input)?; - return Ok((input, value)); + return Err(Err::Error(make_error(input, ErrorKind::Eof))); } -fn mime_parse_value_until_semicolon(input: &[u8]) -> IResult<&[u8], &[u8]> { +fn mime_skip_value_until_semicolon(input: &[u8]) -> IResult<&[u8], &[u8]> { let (input, value) = alt((take_till(|ch: u8| ch == b';'), rest)).parse(input)?; for i in 0..value.len() { if !is_mime_space(value[value.len() - i - 1]) { @@ -60,93 +70,171 @@ fn mime_parse_value_until_semicolon(input: &[u8]) -> IResult<&[u8], &[u8]> { return Ok((input, value)); } +fn mime_parse_value_until_semicolon(input: &[u8]) -> IResult<&[u8], Vec> { + let (input, value) = mime_skip_value_until_semicolon(input)?; + return Ok((input, value.to_vec())); +} + #[inline] fn is_mime_space(ch: u8) -> bool { ch == 0x20 || ch == 0x09 || ch == 0x0a || ch == 0x0d } -pub fn mime_parse_header_token(input: &[u8]) -> IResult<&[u8], (&'_ [u8], &'_ [u8])> { +// arbitrary value to avoid quadratic complexity +const MIME_MAX_CHARSETLANG_LEN: usize = 64; + +fn is_attribute_char(c: u8) -> bool { + match c { + 0..0x20 => false, + 0x80.. => false, + b'(' | b')' | b'<' | b'>' | b'@' | b',' | b';' | b':' | b'\\' | b'"' | b'/' | b'[' + | b']' | b'?' | b'=' | b'*' | b'\'' | b'%' => false, + _ => true, + } +} + +fn mime_parse_token_name(input: &[u8]) -> IResult<&[u8], (bool, &[u8])> { + // handle RFC2331 part 4 "Parameter Value Character Set and Language Information" + let mut star = false; + for i in 0..input.len() { + match input[i] { + b'=' => { + if star { + return Ok((&input[i + 1..], (star, &input[..i - 1]))); + } else { + return Ok((&input[i + 1..], (star, &input[..i]))); + } + } + b'*' => { + star = true; + } + _ => { + star = false; + } + } + } + Err(Err::Error(make_error(input, ErrorKind::Tag))) +} + +pub fn mime_parse_header_token_name(input: &[u8]) -> IResult<&[u8], (bool, &[u8])> { // from RFC2047 : like ch.is_ascii_whitespace but without 0x0c FORM-FEED let (input, _) = take_while(is_mime_space).parse(input)?; - let (input, name) = take_until("=").parse(input)?; - let (input, _) = char('=').parse(input)?; + let (input, (star, name)) = mime_parse_token_name(input)?; + return Ok((input, (star, name))); +} + +pub fn mime_skip_header_token_value(input: &[u8]) -> IResult<&[u8], ()> { + let (input, _) = + alt((mime_skip_value_delimited, mime_skip_value_until_semicolon)).parse(input)?; + let (input, _) = take_while(is_mime_space).parse(input)?; + let (input, _) = opt(complete(char(';'))).parse(input)?; + return Ok((input, ())); +} + +pub fn mime_parse_header_token_value(input: &[u8], star: bool) -> IResult<&[u8], Vec> { let (input, value) = alt((mime_parse_value_delimited, mime_parse_value_until_semicolon)).parse(input)?; let (input, _) = take_while(is_mime_space).parse(input)?; let (input, _) = opt(complete(char(';'))).parse(input)?; - return Ok((input, (name, value))); + if star { + let mut single_quotes = false; + let mut start = 0; + for (j, v) in value.iter().enumerate() { + if *v == b'\'' { + if single_quotes { + start = j + 1; + break; + } + single_quotes = true; + } else if j > MIME_MAX_CHARSETLANG_LEN || !is_attribute_char(*v) { + break; + } + } + let mut decoded = vec![0; value.len() - start]; + let out_len = url_decode_transform_do(&value[start..], &mut decoded, false); + decoded.truncate(out_len as usize); + return Ok((input, decoded)); + } + return Ok((input, value)); } -fn mime_parse_header_tokens(input: &[u8]) -> IResult<&[u8], HeaderTokens<'_>> { - let (mut input, _) = take_until_and_consume(b";").parse(input)?; - let mut tokens = HashMap::new(); +pub fn mime_find_header_token(header: &[u8], token: &[u8]) -> Option> { + let (mut input, _) = take_until_and_consume::>(b";") + .parse(header) + .ok()?; + + // in case of multiple sections for the parameter cf RFC2231 + let mut current_section_slice = Vec::with_capacity(token.len() + 2); + current_section_slice.extend_from_slice(token); + current_section_slice.extend_from_slice(b"*0"); + let mut sections_values = Vec::new(); + let mut current_section_seen = 0; + let mut sections = HashMap::new(); + while !input.is_empty() { - match mime_parse_header_token(input) { - Ok((rem, t)) => { - tokens.insert(t.0, t.1); + match mime_parse_header_token_name(input) { + Ok((rem, name)) => { + let prev = input; + if name.1 == token { + if let Ok((_, value)) = mime_parse_header_token_value(rem, name.0) { + return Some(value); + } else { + return None; + } + } else if name.1 == current_section_slice { + if let Ok((rem2, value)) = mime_parse_header_token_value(rem, name.0) { + sections_values.extend_from_slice(&value); + input = rem2; + } else { + return None; + } + current_section_seen += 1; + let nbdigits = current_section_slice.len() - token.len() - 1; + current_section_slice.truncate(current_section_slice.len() - nbdigits); + current_section_slice + .extend_from_slice(current_section_seen.to_string().as_bytes()); + } else if name.1.len() > token.len() && &name.1[..token.len()] == token { + // prefix + if let Ok((rem2, value)) = mime_parse_header_token_value(rem, name.0) { + sections.insert(name.1, value); + input = rem2; + } else { + return None; + } + } else if let Ok((rem2, _)) = mime_skip_header_token_value(rem) { + input = rem2; + } else { + break; + } + // should never happen - debug_validate_bug_on!(input.len() == rem.len()); - if input.len() == rem.len() { + debug_validate_bug_on!(input.len() == prev.len()); + if input.len() == prev.len() { //infinite loop - return Err(Err::Error(make_error(input, ErrorKind::Eof))); + return None; } - input = rem; } Err(_) => { - // keep first tokens is error in remaining buffer break; } } } - return Ok((input, HeaderTokens { tokens })); -} - -pub fn mime_find_header_token<'a>( - header: &'a [u8], token: &[u8], sections_values: &'a mut Vec, -) -> Option<&'a [u8]> { - match mime_parse_header_tokens(header) { - Ok((_rem, t)) => { - // in case of multiple sections for the parameter cf RFC2231 - let mut current_section_slice = Vec::new(); - - // look for the specific token - match t.tokens.get(token) { - // easy nominal case - Some(value) => return Some(value), - None => { - // check for initial section of a parameter - current_section_slice.extend_from_slice(token); - current_section_slice.extend_from_slice(b"*0"); - { - let value = t.tokens.get(¤t_section_slice[..])?; - sections_values.extend_from_slice(value); - let l = current_section_slice.len(); - current_section_slice[l - 1] = b'1'; - } + if current_section_seen > 0 { + loop { + match sections.get(¤t_section_slice[..]) { + Some(value) => { + sections_values.extend_from_slice(value); + current_section_seen += 1; + let nbdigits = current_section_slice.len() - token.len() - 1; + current_section_slice.truncate(current_section_slice.len() - nbdigits); + current_section_slice + .extend_from_slice(current_section_seen.to_string().as_bytes()); } + None => return Some(sections_values), } - - let mut current_section_seen = 1; - // we have at least the initial section - // try looping until we do not find anymore a next section - loop { - match t.tokens.get(¤t_section_slice[..]) { - Some(value) => { - sections_values.extend_from_slice(value); - current_section_seen += 1; - let nbdigits = current_section_slice.len() - token.len() - 1; - current_section_slice.truncate(current_section_slice.len() - nbdigits); - current_section_slice - .extend_from_slice(current_section_seen.to_string().as_bytes()); - } - None => return Some(sections_values), - } - } - } - Err(_) => { - return None; } } + return None; } pub(crate) const RS_MIME_MAX_TOKEN_LEN: usize = 255; @@ -257,19 +345,10 @@ fn mime_parse_headers<'a>( { if let Ok((value, name)) = mime_parse_header_line(line) { if slice_equals_lowercase(name, "content-disposition".as_bytes()) { - let mut sections_values = Vec::new(); - if let Some(filename) = - mime_find_header_token(value, "filename".as_bytes(), &mut sections_values) - { + if let Some(filename) = mime_find_header_token(value, "filename".as_bytes()) { if !filename.is_empty() { - ctx.filename = Vec::with_capacity(filename.len()); + ctx.filename = filename; fileopen = true; - for c in filename { - // unescape - if *c != b'\\' { - ctx.filename.push(*c); - } - } } } } @@ -418,8 +497,7 @@ fn mime_process(ctx: &mut MimeStateHTTP, i: &[u8]) -> (MimeParserResult, u32, u3 } pub fn mime_state_init(i: &[u8]) -> Option { - let mut sections_values = Vec::new(); - if let Some(value) = mime_find_header_token(i, "boundary".as_bytes(), &mut sections_values) { + if let Some(value) = mime_find_header_token(i, "boundary".as_bytes()) { if value.len() <= RS_MIME_MAX_TOKEN_LEN { let mut r = MimeStateHTTP { boundary: Vec::with_capacity(2 + value.len()), @@ -428,12 +506,7 @@ pub fn mime_state_init(i: &[u8]) -> Option { // start wih 2 additional hyphens r.boundary.push(b'-'); r.boundary.push(b'-'); - for c in value { - // unescape - if *c != b'\\' { - r.boundary.push(*c); - } - } + r.boundary.extend_from_slice(&value); return Some(r); } } @@ -491,78 +564,138 @@ mod test { #[test] fn test_mime_find_header_token() { - let mut outvec = Vec::new(); let undelimok = mime_find_header_token( "attachment; filename=test;".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(undelimok, Some("test".as_bytes())); + ) + .unwrap(); + assert_eq!(undelimok, b"test"); let delimok = mime_find_header_token( "attachment; filename=\"test2\";".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(delimok, Some("test2".as_bytes())); + ) + .unwrap(); + assert_eq!(delimok, b"test2"); let escaped = mime_find_header_token( "attachment; filename=\"test\\\"2\";".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(escaped, Some("test\\\"2".as_bytes())); + ) + .unwrap(); + assert_eq!(escaped, b"test\"2"); let evasion_othertoken = mime_find_header_token( "attachment; dummy=\"filename=wrong\"; filename=real;".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(evasion_othertoken, Some("real".as_bytes())); + ) + .unwrap(); + assert_eq!(evasion_othertoken, b"real"); let evasion_suffixtoken = mime_find_header_token( "attachment; notafilename=wrong; filename=good;".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(evasion_suffixtoken, Some("good".as_bytes())); + ) + .unwrap(); + assert_eq!(evasion_suffixtoken, b"good"); let badending = mime_find_header_token( "attachment; filename=oksofar; badending".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(badending, Some("oksofar".as_bytes())); + ) + .unwrap(); + assert_eq!(badending, b"oksofar"); let missend = mime_find_header_token( "attachment; filename=test".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(missend, Some("test".as_bytes())); + ) + .unwrap(); + assert_eq!(missend, b"test"); let spaces = mime_find_header_token( "attachment; filename=test me wrong".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(spaces, Some("test me wrong".as_bytes())); + ) + .unwrap(); + assert_eq!(spaces, b"test me wrong"); - assert_eq!(outvec.len(), 0); let multi = mime_find_header_token( "attachment; filename*0=abc; filename*1=\"def\";".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(multi, Some("abcdef".as_bytes())); - outvec.clear(); + ) + .unwrap(); + assert_eq!(multi, b"abcdef"); let multi = mime_find_header_token( "attachment; filename*1=456; filename*0=\"123\"".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(multi, Some("123456".as_bytes())); - outvec.clear(); + ) + .unwrap(); + assert_eq!(multi, b"123456"); + + let multi = mime_find_header_token( + "attachment; filename*=us-ascii'en-us'shell.php".as_bytes(), + "filename".as_bytes(), + ) + .unwrap(); + assert_eq!(multi, b"shell.php"); + + let multi = mime_find_header_token( + "attachment; filename*='en-us'shell.php".as_bytes(), + "filename".as_bytes(), + ) + .unwrap(); + assert_eq!(multi, b"shell.php"); + + let multi = mime_find_header_token( + "attachment; filename*=us-ascii''shell.php".as_bytes(), + "filename".as_bytes(), + ) + .unwrap(); + assert_eq!(multi, b"shell.php"); + + let multi = mime_find_header_token( + "attachment; filename=first; filename*=us-ascii''second;".as_bytes(), + "filename".as_bytes(), + ) + .unwrap(); + assert_eq!(multi, b"first"); + + let multi = mime_find_header_token( + "attachment; filename*=us-ascii''first; filename=second;".as_bytes(), + "filename".as_bytes(), + ) + .unwrap(); + assert_eq!(multi, b"first"); + + let multi = mime_find_header_token( + "attachment; filename*0*=us-ascii'en-us'shell; filename*1=\".php\"".as_bytes(), + "filename".as_bytes(), + ) + .unwrap(); + assert_eq!(multi, b"shell.php"); + + let quote_after_lang = mime_find_header_token( + "attachment; name=\"f\"; x*=a''\"; filename=\"shell.php\"".as_bytes(), + "filename".as_bytes(), + ) + .unwrap(); + assert_eq!(quote_after_lang, b"shell.php"); + + let encoded = mime_find_header_token( + "attachment; filename*=us-ascii'en-us'she%6c%6C.php".as_bytes(), + "filename".as_bytes(), + ) + .unwrap(); + assert_eq!(encoded, b"shell.php"); + + let multi_encoded = mime_find_header_token( + "attachment; filename*0*=us-ascii'en-us'sh%65; filename*1*=%6c%6C;filename*2=\".php\"" + .as_bytes(), + "filename".as_bytes(), + ) + .unwrap(); + assert_eq!(multi_encoded, b"shell.php"); } } diff --git a/rust/src/mime/smtp.rs b/rust/src/mime/smtp.rs index dc1dac7d97..cd70795b3b 100644 --- a/rust/src/mime/smtp.rs +++ b/rust/src/mime/smtp.rs @@ -184,26 +184,19 @@ pub const MIME_ANOM_LONG_BOUNDARY: u32 = 0x80; pub const MIME_ANOM_LONG_FILENAME: u32 = 0x100; fn mime_smtp_process_headers(ctx: &mut MimeStateSMTP) -> (u32, bool) { - let mut sections_values = Vec::new(); let mut warnings = 0; let mut encap = false; for h in &ctx.headers[ctx.main_headers_nb..] { if mime::slice_equals_lowercase(&h.name, b"content-disposition") { if ctx.filename.is_empty() { - if let Some(value) = - mime::mime_find_header_token(&h.value, b"filename", &mut sections_values) - { - let value = if value.len() > mime::RS_MIME_MAX_TOKEN_LEN { + if let Some(mut value) = mime::mime_find_header_token(&h.value, b"filename") { + if value.len() > mime::RS_MIME_MAX_TOKEN_LEN { warnings |= MIME_ANOM_LONG_FILENAME; - &value[..mime::RS_MIME_MAX_TOKEN_LEN] - } else { - value - }; - ctx.filename.extend_from_slice(value); - let mut newname = Vec::new(); - newname.extend_from_slice(value); - ctx.attachments.push(newname); - sections_values.clear(); + value.truncate(mime::RS_MIME_MAX_TOKEN_LEN); + value.shrink_to_fit(); + } + ctx.filename.extend_from_slice(&value); + ctx.attachments.push(value); } } } else if mime::slice_equals_lowercase(&h.name, b"content-transfer-encoding") { @@ -215,35 +208,26 @@ fn mime_smtp_process_headers(ctx: &mut MimeStateSMTP) -> (u32, bool) { } } else if mime::slice_equals_lowercase(&h.name, b"content-type") { if ctx.filename.is_empty() { - if let Some(value) = - mime::mime_find_header_token(&h.value, b"name", &mut sections_values) - { - let value = if value.len() > mime::RS_MIME_MAX_TOKEN_LEN { + if let Some(mut value) = mime::mime_find_header_token(&h.value, b"name") { + if value.len() > mime::RS_MIME_MAX_TOKEN_LEN { warnings |= MIME_ANOM_LONG_FILENAME; - &value[..mime::RS_MIME_MAX_TOKEN_LEN] - } else { - value - }; - ctx.filename.extend_from_slice(value); - let mut newname = Vec::new(); - newname.extend_from_slice(value); - ctx.attachments.push(newname); - sections_values.clear(); + value.truncate(mime::RS_MIME_MAX_TOKEN_LEN); + value.shrink_to_fit(); + } + ctx.filename.extend_from_slice(&value); + ctx.attachments.push(value); } } - if let Some(value) = - mime::mime_find_header_token(&h.value, b"boundary", &mut sections_values) - { + if let Some(value) = mime::mime_find_header_token(&h.value, b"boundary") { // start wih 2 additional hyphens let mut boundary = Vec::new(); boundary.push(b'-'); boundary.push(b'-'); - boundary.extend_from_slice(value); + boundary.extend_from_slice(&value); ctx.boundaries.push(boundary); if value.len() > MAX_BOUNDARY_LEN { warnings |= MIME_ANOM_LONG_BOUNDARY; } - sections_values.clear(); } let ct = if let Some(x) = h.value.iter().position(|&x| x == b';') { &h.value[..x]