pull/16501/merge
Catena cyber 14 hours ago committed by GitHub
commit ad33f0615d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -41,7 +41,8 @@ fn hex_value(i: u8) -> Option<u8> {
_ => 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");
}
}

@ -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<u8>> {
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<u8>> {
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<u8>> {
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<Vec<u8>> {
let (mut input, _) = take_until_and_consume::<nom8::error::Error<&[u8]>>(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<u8>,
) -> 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(&current_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(&current_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(&current_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<MimeStateHTTP> {
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<MimeStateHTTP> {
// 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");
}
}

@ -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]

Loading…
Cancel
Save