mirror of https://github.com/OISF/suricata
parent
5f75b9a6e3
commit
a10c1f1dde
@ -0,0 +1,569 @@
|
||||
/* Copyright (C) 2024 Open Information Security Foundation
|
||||
*
|
||||
* You can copy, redistribute or modify this Program under the terms of
|
||||
* the GNU General Public License version 2 as published by the Free
|
||||
* Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* version 2 along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*/
|
||||
|
||||
use crate::common::nom7::take_until_and_consume;
|
||||
use nom7::branch::alt;
|
||||
use nom7::bytes::complete::{tag, take, take_till, take_until, take_while};
|
||||
use nom7::character::complete::char;
|
||||
use nom7::combinator::{complete, opt, rest, value};
|
||||
use nom7::error::{make_error, ErrorKind};
|
||||
use nom7::{Err, IResult};
|
||||
use std;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HeaderTokens<'a> {
|
||||
pub tokens: HashMap<&'a [u8], &'a [u8]>,
|
||||
}
|
||||
|
||||
fn mime_parse_value_delimited(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
let (input, _) = char('"')(input)?;
|
||||
let mut escaping = false;
|
||||
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
|
||||
escaping = false;
|
||||
}
|
||||
}
|
||||
// should fail
|
||||
let (input, value) = take_until("\"")(input)?;
|
||||
let (input, _) = char('"')(input)?;
|
||||
return Ok((input, value));
|
||||
}
|
||||
|
||||
fn mime_parse_value_until_semicolon(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
let (input, value) = alt((take_till(|ch: u8| ch == b';'), rest))(input)?;
|
||||
for i in 0..value.len() {
|
||||
if !is_mime_space(value[value.len() - i - 1]) {
|
||||
return Ok((input, &value[..value.len() - i]));
|
||||
}
|
||||
}
|
||||
return Ok((input, value));
|
||||
}
|
||||
|
||||
#[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])> {
|
||||
// from RFC2047 : like ch.is_ascii_whitespace but without 0x0c FORM-FEED
|
||||
let (input, _) = take_while(is_mime_space)(input)?;
|
||||
let (input, name) = take_until("=")(input)?;
|
||||
let (input, _) = char('=')(input)?;
|
||||
let (input, value) =
|
||||
alt((mime_parse_value_delimited, mime_parse_value_until_semicolon))(input)?;
|
||||
let (input, _) = take_while(is_mime_space)(input)?;
|
||||
let (input, _) = opt(complete(char(';')))(input)?;
|
||||
return Ok((input, (name, value)));
|
||||
}
|
||||
|
||||
fn mime_parse_header_tokens(input: &[u8]) -> IResult<&[u8], HeaderTokens> {
|
||||
let (mut input, _) = take_until_and_consume(b";")(input)?;
|
||||
let mut tokens = HashMap::new();
|
||||
while !input.is_empty() {
|
||||
match mime_parse_header_token(input) {
|
||||
Ok((rem, t)) => {
|
||||
tokens.insert(t.0, t.1);
|
||||
// should never happen
|
||||
debug_validate_bug_on!(input.len() == rem.len());
|
||||
if input.len() == rem.len() {
|
||||
//infinite loop
|
||||
return Err(Err::Error(make_error(input, ErrorKind::Eof)));
|
||||
}
|
||||
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");
|
||||
match t.tokens.get(¤t_section_slice[..]) {
|
||||
Some(value) => {
|
||||
sections_values.extend_from_slice(value);
|
||||
let l = current_section_slice.len();
|
||||
current_section_slice[l - 1] = b'1';
|
||||
}
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const RS_MIME_MAX_TOKEN_LEN: usize = 255;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MimeParserState {
|
||||
Start,
|
||||
Header,
|
||||
HeaderEnd,
|
||||
Chunk,
|
||||
BoundaryWaitingForEol,
|
||||
}
|
||||
|
||||
impl Default for MimeParserState {
|
||||
fn default() -> Self {
|
||||
MimeParserState::Start
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MimeStateHTTP {
|
||||
boundary: Vec<u8>,
|
||||
filename: Vec<u8>,
|
||||
state: MimeParserState,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)]
|
||||
pub enum MimeParserResult {
|
||||
MimeNeedsMore = 0,
|
||||
MimeFileOpen = 1,
|
||||
MimeFileChunk = 2,
|
||||
MimeFileClose = 3,
|
||||
}
|
||||
|
||||
fn mime_parse_skip_line(input: &[u8]) -> IResult<&[u8], MimeParserState> {
|
||||
let (input, _) = take_till(|ch: u8| ch == b'\n')(input)?;
|
||||
let (input, _) = char('\n')(input)?;
|
||||
return Ok((input, MimeParserState::Start));
|
||||
}
|
||||
|
||||
fn mime_parse_boundary_regular<'a>(
|
||||
boundary: &[u8], input: &'a [u8],
|
||||
) -> IResult<&'a [u8], MimeParserState> {
|
||||
let (input, _) = tag(boundary)(input)?;
|
||||
let (input, _) = take_till(|ch: u8| ch == b'\n')(input)?;
|
||||
let (input, _) = char('\n')(input)?;
|
||||
return Ok((input, MimeParserState::Header));
|
||||
}
|
||||
|
||||
// Number of characters after boundary, without end of line, before changing state to streaming
|
||||
const MIME_BOUNDARY_MAX_BEFORE_EOL: usize = 128;
|
||||
const MIME_HEADER_MAX_LINE: usize = 4096;
|
||||
|
||||
fn mime_parse_boundary_missing_eol<'a>(
|
||||
boundary: &[u8], input: &'a [u8],
|
||||
) -> IResult<&'a [u8], MimeParserState> {
|
||||
let (input, _) = tag(boundary)(input)?;
|
||||
let (input, _) = take(MIME_BOUNDARY_MAX_BEFORE_EOL)(input)?;
|
||||
return Ok((input, MimeParserState::BoundaryWaitingForEol));
|
||||
}
|
||||
|
||||
fn mime_parse_boundary<'a>(boundary: &[u8], input: &'a [u8]) -> IResult<&'a [u8], MimeParserState> {
|
||||
let r = mime_parse_boundary_regular(boundary, input);
|
||||
if r.is_ok() {
|
||||
return r;
|
||||
}
|
||||
let r2 = mime_parse_skip_line(input);
|
||||
if r2.is_ok() {
|
||||
return r2;
|
||||
}
|
||||
return mime_parse_boundary_missing_eol(boundary, input);
|
||||
}
|
||||
|
||||
fn mime_consume_until_eol(input: &[u8]) -> IResult<&[u8], bool> {
|
||||
return alt((value(true, mime_parse_skip_line), value(false, rest)))(input);
|
||||
}
|
||||
|
||||
pub fn mime_parse_header_line(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
let (input, name) = take_till(|ch: u8| ch == b':')(input)?;
|
||||
let (input, _) = char(':')(input)?;
|
||||
let (input, _) = take_while(is_mime_space)(input)?;
|
||||
return Ok((input, name));
|
||||
}
|
||||
|
||||
// s2 is already lower case
|
||||
pub fn slice_equals_lowercase(s1: &[u8], s2: &[u8]) -> bool {
|
||||
if s1.len() == s2.len() {
|
||||
for i in 0..s1.len() {
|
||||
if s1[i].to_ascii_lowercase() != s2[i] {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn mime_parse_headers<'a>(
|
||||
ctx: &mut MimeStateHTTP, i: &'a [u8],
|
||||
) -> IResult<&'a [u8], (MimeParserState, bool, bool)> {
|
||||
let mut fileopen = false;
|
||||
let mut errored = false;
|
||||
let mut input = i;
|
||||
while !input.is_empty() {
|
||||
if let Ok((input2, line)) = take_until::<_, &[u8], nom7::error::Error<&[u8]>>("\r\n")(input)
|
||||
{
|
||||
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 !filename.is_empty() {
|
||||
ctx.filename = Vec::with_capacity(filename.len());
|
||||
fileopen = true;
|
||||
for c in filename {
|
||||
// unescape
|
||||
if *c != b'\\' {
|
||||
ctx.filename.push(*c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if value.is_empty() {
|
||||
errored = true;
|
||||
}
|
||||
} else if !line.is_empty() {
|
||||
errored = true;
|
||||
}
|
||||
let (input3, _) = tag("\r\n")(input2)?;
|
||||
input = input3;
|
||||
if line.is_empty() || (line.len() == 1 && line[0] == b'\r') {
|
||||
return Ok((input, (MimeParserState::HeaderEnd, fileopen, errored)));
|
||||
}
|
||||
} else {
|
||||
// guard against too long header lines
|
||||
if input.len() > MIME_HEADER_MAX_LINE {
|
||||
return Ok((
|
||||
input,
|
||||
(MimeParserState::BoundaryWaitingForEol, fileopen, errored),
|
||||
));
|
||||
}
|
||||
if input.len() < i.len() {
|
||||
return Ok((input, (MimeParserState::Header, fileopen, errored)));
|
||||
} // else only an incomplete line, ask for more
|
||||
return Err(Err::Error(make_error(input, ErrorKind::Eof)));
|
||||
}
|
||||
}
|
||||
return Ok((input, (MimeParserState::Header, fileopen, errored)));
|
||||
}
|
||||
|
||||
type NomTakeError<'a> = Err<nom7::error::Error<&'a [u8]>>;
|
||||
|
||||
fn mime_consume_chunk<'a>(boundary: &[u8], input: &'a [u8]) -> IResult<&'a [u8], bool> {
|
||||
let r: Result<(&[u8], &[u8]), NomTakeError> = take_until("\r\n")(input);
|
||||
if let Ok((input, line)) = r {
|
||||
let (next_line, _) = tag("\r\n")(input)?;
|
||||
if next_line.len() < boundary.len() {
|
||||
if next_line == &boundary[..next_line.len()] {
|
||||
if !line.is_empty() {
|
||||
// consume as chunk up to eol (not consuming eol)
|
||||
return Ok((input, false));
|
||||
}
|
||||
// new line beignning like boundary, with nothin to consume as chunk : request more
|
||||
return Err(Err::Error(make_error(input, ErrorKind::Eof)));
|
||||
}
|
||||
// not like boundary : consume everything as chunk
|
||||
return Ok((&input[input.len()..], false));
|
||||
} // else
|
||||
if &next_line[..boundary.len()] == boundary {
|
||||
// end of file with boundary, consume eol but do not consume boundary
|
||||
return Ok((next_line, true));
|
||||
}
|
||||
// not like boundary : consume everything as chunk
|
||||
return Ok((next_line, false));
|
||||
} else {
|
||||
return Ok((&input[input.len()..], false));
|
||||
}
|
||||
}
|
||||
|
||||
pub const MIME_EVENT_FLAG_INVALID_HEADER: u32 = 0x01;
|
||||
pub const MIME_EVENT_FLAG_NO_FILEDATA: u32 = 0x02;
|
||||
|
||||
fn mime_process(ctx: &mut MimeStateHTTP, i: &[u8]) -> (MimeParserResult, u32, u32) {
|
||||
let mut input = i;
|
||||
let mut consumed = 0;
|
||||
let mut warnings = 0;
|
||||
while !input.is_empty() {
|
||||
match ctx.state {
|
||||
MimeParserState::Start => {
|
||||
if let Ok((rem, next)) = mime_parse_boundary(&ctx.boundary, input) {
|
||||
ctx.state = next;
|
||||
consumed += (input.len() - rem.len()) as u32;
|
||||
input = rem;
|
||||
} else {
|
||||
return (MimeParserResult::MimeNeedsMore, consumed, warnings);
|
||||
}
|
||||
}
|
||||
MimeParserState::BoundaryWaitingForEol => {
|
||||
if let Ok((rem, found)) = mime_consume_until_eol(input) {
|
||||
if found {
|
||||
ctx.state = MimeParserState::Header;
|
||||
}
|
||||
consumed += (input.len() - rem.len()) as u32;
|
||||
input = rem;
|
||||
} else {
|
||||
// should never happen
|
||||
return (MimeParserResult::MimeNeedsMore, consumed, warnings);
|
||||
}
|
||||
}
|
||||
MimeParserState::Header => {
|
||||
if let Ok((rem, (next, fileopen, err))) = mime_parse_headers(ctx, input) {
|
||||
ctx.state = next;
|
||||
consumed += (input.len() - rem.len()) as u32;
|
||||
input = rem;
|
||||
if err {
|
||||
warnings |= MIME_EVENT_FLAG_INVALID_HEADER;
|
||||
}
|
||||
if fileopen {
|
||||
return (MimeParserResult::MimeFileOpen, consumed, warnings);
|
||||
}
|
||||
} else {
|
||||
return (MimeParserResult::MimeNeedsMore, consumed, warnings);
|
||||
}
|
||||
}
|
||||
MimeParserState::HeaderEnd => {
|
||||
// check if we start with the boundary
|
||||
// and transition to chunk, or empty file and back to start
|
||||
if input.len() < ctx.boundary.len() {
|
||||
if input == &ctx.boundary[..input.len()] {
|
||||
return (MimeParserResult::MimeNeedsMore, consumed, warnings);
|
||||
}
|
||||
ctx.state = MimeParserState::Chunk;
|
||||
} else if input[..ctx.boundary.len()] == ctx.boundary {
|
||||
ctx.state = MimeParserState::Start;
|
||||
if !ctx.filename.is_empty() {
|
||||
warnings |= MIME_EVENT_FLAG_NO_FILEDATA;
|
||||
}
|
||||
ctx.filename.clear();
|
||||
return (MimeParserResult::MimeFileClose, consumed, warnings);
|
||||
} else {
|
||||
ctx.state = MimeParserState::Chunk;
|
||||
}
|
||||
}
|
||||
MimeParserState::Chunk => {
|
||||
if let Ok((rem, eof)) = mime_consume_chunk(&ctx.boundary, input) {
|
||||
consumed += (input.len() - rem.len()) as u32;
|
||||
if eof {
|
||||
ctx.state = MimeParserState::Start;
|
||||
ctx.filename.clear();
|
||||
return (MimeParserResult::MimeFileClose, consumed, warnings);
|
||||
} else {
|
||||
// + 2 for \r\n
|
||||
if rem.len() < ctx.boundary.len() + 2 {
|
||||
return (MimeParserResult::MimeFileChunk, consumed, warnings);
|
||||
}
|
||||
input = rem;
|
||||
}
|
||||
} else {
|
||||
return (MimeParserResult::MimeNeedsMore, consumed, warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (MimeParserResult::MimeNeedsMore, consumed, warnings);
|
||||
}
|
||||
|
||||
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 value.len() <= RS_MIME_MAX_TOKEN_LEN {
|
||||
let mut r = MimeStateHTTP {
|
||||
boundary: Vec::with_capacity(2 + value.len()),
|
||||
..Default::default()
|
||||
};
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
return Some(r);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeStateInit(input: *const u8, input_len: u32) -> *mut MimeStateHTTP {
|
||||
let slice = build_slice!(input, input_len as usize);
|
||||
|
||||
if let Some(ctx) = mime_state_init(slice) {
|
||||
let boxed = Box::new(ctx);
|
||||
return Box::into_raw(boxed) as *mut _;
|
||||
}
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeParse(
|
||||
ctx: &mut MimeStateHTTP, input: *const u8, input_len: u32, consumed: *mut u32,
|
||||
warnings: *mut u32,
|
||||
) -> MimeParserResult {
|
||||
let slice = build_slice!(input, input_len as usize);
|
||||
let (r, c, w) = mime_process(ctx, slice);
|
||||
*consumed = c;
|
||||
*warnings = w;
|
||||
return r;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeStateGetFilename(
|
||||
ctx: &mut MimeStateHTTP, buffer: *mut *const u8, filename_len: *mut u16,
|
||||
) {
|
||||
if !ctx.filename.is_empty() {
|
||||
*buffer = ctx.filename.as_ptr();
|
||||
if ctx.filename.len() < u16::MAX.into() {
|
||||
*filename_len = ctx.filename.len() as u16;
|
||||
} else {
|
||||
*filename_len = u16::MAX;
|
||||
}
|
||||
} else {
|
||||
*buffer = std::ptr::null_mut();
|
||||
*filename_len = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeStateFree(ctx: &mut MimeStateHTTP) {
|
||||
std::mem::drop(Box::from_raw(ctx));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[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()));
|
||||
|
||||
let delimok = mime_find_header_token(
|
||||
"attachment; filename=\"test2\";".as_bytes(),
|
||||
"filename".as_bytes(),
|
||||
&mut outvec,
|
||||
);
|
||||
assert_eq!(delimok, Some("test2".as_bytes()));
|
||||
|
||||
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()));
|
||||
|
||||
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()));
|
||||
|
||||
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()));
|
||||
|
||||
let badending = mime_find_header_token(
|
||||
"attachment; filename=oksofar; badending".as_bytes(),
|
||||
"filename".as_bytes(),
|
||||
&mut outvec,
|
||||
);
|
||||
assert_eq!(badending, Some("oksofar".as_bytes()));
|
||||
|
||||
let missend = mime_find_header_token(
|
||||
"attachment; filename=test".as_bytes(),
|
||||
"filename".as_bytes(),
|
||||
&mut outvec,
|
||||
);
|
||||
assert_eq!(missend, Some("test".as_bytes()));
|
||||
|
||||
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()));
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,854 @@
|
||||
/* Copyright (C) 2024 Open Information Security Foundation
|
||||
*
|
||||
* You can copy, redistribute or modify this Program under the terms of
|
||||
* the GNU General Public License version 2 as published by the Free
|
||||
* Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* version 2 along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*/
|
||||
|
||||
use super::mime;
|
||||
use crate::core::StreamingBufferConfig;
|
||||
use crate::filecontainer::FileContainer;
|
||||
use digest::generic_array::{typenum::U16, GenericArray};
|
||||
use digest::Digest;
|
||||
use digest::Update;
|
||||
use md5::Md5;
|
||||
use std::ffi::CStr;
|
||||
use std::io;
|
||||
use std::os::raw::c_uchar;
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq)]
|
||||
pub enum MimeSmtpParserState {
|
||||
MimeSmtpStart = 0,
|
||||
MimeSmtpHeader = 1,
|
||||
MimeSmtpBody = 2,
|
||||
MimeSmtpParserError = 3,
|
||||
}
|
||||
|
||||
impl Default for MimeSmtpParserState {
|
||||
fn default() -> Self {
|
||||
MimeSmtpParserState::MimeSmtpStart
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MimeHeader {
|
||||
pub name: Vec<u8>,
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq)]
|
||||
pub enum MimeSmtpMd5State {
|
||||
MimeSmtpMd5Disabled = 0,
|
||||
MimeSmtpMd5Inited = 1,
|
||||
MimeSmtpMd5Started = 2,
|
||||
MimeSmtpMd5Completed = 3,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq)]
|
||||
enum MimeSmtpContentType {
|
||||
Message = 0,
|
||||
PlainText = 1,
|
||||
Html = 2,
|
||||
Unknown = 3,
|
||||
}
|
||||
|
||||
impl Default for MimeSmtpContentType {
|
||||
fn default() -> Self {
|
||||
MimeSmtpContentType::Message
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MimeStateSMTP<'a> {
|
||||
pub(crate) state_flag: MimeSmtpParserState,
|
||||
pub(crate) headers: Vec<MimeHeader>,
|
||||
pub(crate) main_headers_nb: usize,
|
||||
filename: Vec<u8>,
|
||||
pub(crate) attachments: Vec<Vec<u8>>,
|
||||
pub(crate) urls: Vec<Vec<u8>>,
|
||||
boundaries: Vec<Vec<u8>>,
|
||||
encoding: MimeSmtpEncoding,
|
||||
decoder: Option<MimeBase64Decoder>,
|
||||
content_type: MimeSmtpContentType,
|
||||
decoded_line: Vec<u8>,
|
||||
// small buffer for end of line
|
||||
// waiting to see if it is part of the boundary
|
||||
bufeol: [u8; 2],
|
||||
bufeolen: u8,
|
||||
files: &'a mut FileContainer,
|
||||
sbcfg: *const StreamingBufferConfig,
|
||||
md5: md5::Md5,
|
||||
pub(crate) md5_state: MimeSmtpMd5State,
|
||||
pub(crate) md5_result: GenericArray<u8, U16>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MimeBase64Decoder {
|
||||
tmp: [u8; 4],
|
||||
nb: u8,
|
||||
}
|
||||
|
||||
impl MimeBase64Decoder {
|
||||
pub fn new() -> MimeBase64Decoder {
|
||||
MimeBase64Decoder { tmp: [0; 4], nb: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MimeBase64Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mime_smtp_state_init(
|
||||
files: &mut FileContainer, sbcfg: *const StreamingBufferConfig,
|
||||
) -> Option<MimeStateSMTP> {
|
||||
let r = MimeStateSMTP {
|
||||
state_flag: MimeSmtpParserState::MimeSmtpStart,
|
||||
headers: Vec::new(),
|
||||
main_headers_nb: 0,
|
||||
filename: Vec::new(),
|
||||
attachments: Vec::new(),
|
||||
urls: Vec::new(),
|
||||
boundaries: Vec::new(),
|
||||
decoded_line: Vec::new(),
|
||||
encoding: MimeSmtpEncoding::Plain,
|
||||
decoder: None,
|
||||
content_type: MimeSmtpContentType::Message,
|
||||
bufeol: [0; 2],
|
||||
bufeolen: 0,
|
||||
files,
|
||||
sbcfg,
|
||||
md5: Md5::new(),
|
||||
md5_state: MimeSmtpMd5State::MimeSmtpMd5Disabled,
|
||||
md5_result: [0; 16].into(),
|
||||
};
|
||||
return Some(r);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpStateInit(
|
||||
files: &mut FileContainer, sbcfg: *const StreamingBufferConfig,
|
||||
) -> *mut MimeStateSMTP {
|
||||
if let Some(ctx) = mime_smtp_state_init(files, sbcfg) {
|
||||
let boxed = Box::new(ctx);
|
||||
return Box::into_raw(boxed) as *mut _;
|
||||
}
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpStateFree(ctx: &mut MimeStateSMTP) {
|
||||
// Just unbox...
|
||||
std::mem::drop(Box::from_raw(ctx));
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)]
|
||||
pub enum MimeSmtpParserResult {
|
||||
MimeSmtpNeedsMore = 0,
|
||||
MimeSmtpFileOpen = 1,
|
||||
MimeSmtpFileClose = 2,
|
||||
MimeSmtpFileChunk = 3,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq)]
|
||||
pub enum MimeSmtpEncoding {
|
||||
Plain = 0,
|
||||
Base64 = 1,
|
||||
QuotedPrintable = 2,
|
||||
}
|
||||
|
||||
impl Default for MimeSmtpEncoding {
|
||||
fn default() -> Self {
|
||||
MimeSmtpEncoding::Plain
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot use BIT_U32 macros as they do not get exported by cbindgen :-/
|
||||
pub const MIME_ANOM_INVALID_BASE64: u32 = 0x1;
|
||||
pub const MIME_ANOM_INVALID_QP: u32 = 0x2;
|
||||
pub const MIME_ANOM_LONG_LINE: u32 = 0x4;
|
||||
pub const MIME_ANOM_LONG_ENC_LINE: u32 = 0x8;
|
||||
pub const MIME_ANOM_LONG_HEADER_NAME: u32 = 0x10;
|
||||
pub const MIME_ANOM_LONG_HEADER_VALUE: u32 = 0x20;
|
||||
//unused pub const MIME_ANOM_MALFORMED_MSG: u32 = 0x40;
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
}
|
||||
} else if mime::slice_equals_lowercase(&h.name, b"content-transfer-encoding") {
|
||||
if mime::slice_equals_lowercase(&h.value, b"base64") {
|
||||
ctx.encoding = MimeSmtpEncoding::Base64;
|
||||
ctx.decoder = Some(MimeBase64Decoder::new());
|
||||
} else if mime::slice_equals_lowercase(&h.value, b"quoted-printable") {
|
||||
ctx.encoding = MimeSmtpEncoding::QuotedPrintable;
|
||||
}
|
||||
} 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 {
|
||||
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();
|
||||
}
|
||||
}
|
||||
if let Some(value) =
|
||||
mime::mime_find_header_token(&h.value, b"boundary", &mut sections_values)
|
||||
{
|
||||
// start wih 2 additional hyphens
|
||||
let mut boundary = Vec::new();
|
||||
boundary.push(b'-');
|
||||
boundary.push(b'-');
|
||||
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]
|
||||
} else {
|
||||
&h.value
|
||||
};
|
||||
match ct {
|
||||
b"text/plain" => {
|
||||
ctx.content_type = MimeSmtpContentType::PlainText;
|
||||
}
|
||||
b"text/html" => {
|
||||
ctx.content_type = MimeSmtpContentType::Html;
|
||||
}
|
||||
_ => {
|
||||
if ct.starts_with(b"message/") {
|
||||
encap = true;
|
||||
}
|
||||
ctx.content_type = MimeSmtpContentType::Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (warnings, encap);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
// Defined in util-file.h
|
||||
pub fn FileAppendData(
|
||||
c: *mut FileContainer, sbcfg: *const StreamingBufferConfig, data: *const c_uchar,
|
||||
data_len: u32,
|
||||
) -> std::os::raw::c_int;
|
||||
// Defined in util-spm-bs.h
|
||||
pub fn BasicSearchNocaseIndex(
|
||||
data: *const c_uchar, data_len: u32, needle: *const c_uchar, needle_len: u16,
|
||||
) -> u32;
|
||||
}
|
||||
|
||||
fn hex(i: u8) -> Option<u8> {
|
||||
if i.is_ascii_digit() {
|
||||
return Some(i - b'0');
|
||||
}
|
||||
if (b'A'..=b'F').contains(&i) {
|
||||
return Some(i - b'A' + 10);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
const SMTP_MIME_MAX_DECODED_LINE_LENGTH: usize = 8192;
|
||||
|
||||
fn mime_smtp_finish_url(input: &[u8]) -> &[u8] {
|
||||
if let Some(x) = input.iter().position(|&x| {
|
||||
x == b' ' || x == b'"' || x == b'\'' || x == b'<' || x == b'>' || x == b']' || x == b'\t'
|
||||
}) {
|
||||
return &input[..x];
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
fn mime_smtp_extract_urls(urls: &mut Vec<Vec<u8>>, input_start: &[u8]) {
|
||||
//TODO optimize later : use mpm
|
||||
for s in unsafe { MIME_SMTP_CONFIG_EXTRACT_URL_SCHEMES.iter() } {
|
||||
let mut input = input_start;
|
||||
let mut start = unsafe {
|
||||
BasicSearchNocaseIndex(
|
||||
input.as_ptr(),
|
||||
input.len() as u32,
|
||||
s.as_ptr(),
|
||||
s.len() as u16,
|
||||
)
|
||||
};
|
||||
while (start as usize) < input.len() {
|
||||
let url = mime_smtp_finish_url(&input[start as usize..]);
|
||||
let mut urlv = Vec::with_capacity(url.len());
|
||||
if unsafe { !MIME_SMTP_CONFIG_LOG_URL_SCHEME } {
|
||||
urlv.extend_from_slice(&url[s.len()..]);
|
||||
} else {
|
||||
urlv.extend_from_slice(url);
|
||||
}
|
||||
urls.push(urlv);
|
||||
input = &input[start as usize + url.len()..];
|
||||
start = unsafe {
|
||||
BasicSearchNocaseIndex(
|
||||
input.as_ptr(),
|
||||
input.len() as u32,
|
||||
s.as_ptr(),
|
||||
s.len() as u16,
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mime_smtp_find_url_strings(ctx: &mut MimeStateSMTP, input_new: &[u8]) {
|
||||
if unsafe { !MIME_SMTP_CONFIG_EXTRACT_URLS } {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut input = input_new;
|
||||
// use previosly buffered beginning of line if any
|
||||
if !ctx.decoded_line.is_empty() {
|
||||
ctx.decoded_line.extend_from_slice(input_new);
|
||||
input = &ctx.decoded_line;
|
||||
}
|
||||
// no input, no url
|
||||
if input.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if input[input.len() - 1] == b'\n' || input.len() > SMTP_MIME_MAX_DECODED_LINE_LENGTH {
|
||||
// easy case, no buffering to do
|
||||
mime_smtp_extract_urls(&mut ctx.urls, input);
|
||||
if !ctx.decoded_line.is_empty() {
|
||||
ctx.decoded_line.clear()
|
||||
}
|
||||
} else if let Some(x) = input.iter().rev().position(|&x| x == b'\n') {
|
||||
input = &input[..x];
|
||||
mime_smtp_extract_urls(&mut ctx.urls, input);
|
||||
if !ctx.decoded_line.is_empty() {
|
||||
ctx.decoded_line.drain(0..x);
|
||||
} else {
|
||||
ctx.decoded_line.extend_from_slice(&input_new[x..]);
|
||||
}
|
||||
} // else no end of line, already buffered for next input...
|
||||
}
|
||||
|
||||
fn mime_base64_map(input: u8) -> io::Result<u8> {
|
||||
match input {
|
||||
43 => Ok(62), // +
|
||||
47 => Ok(63), // /
|
||||
48 => Ok(52), // 0
|
||||
49 => Ok(53), // 1
|
||||
50 => Ok(54), // 2
|
||||
51 => Ok(55), // 3
|
||||
52 => Ok(56), // 4
|
||||
53 => Ok(57), // 5
|
||||
54 => Ok(58), // 6
|
||||
55 => Ok(59), // 7
|
||||
56 => Ok(60), // 8
|
||||
57 => Ok(61), // 9
|
||||
65 => Ok(0), // A
|
||||
66 => Ok(1), // B
|
||||
67 => Ok(2), // C
|
||||
68 => Ok(3), // D
|
||||
69 => Ok(4), // E
|
||||
70 => Ok(5), // F
|
||||
71 => Ok(6), // G
|
||||
72 => Ok(7), // H
|
||||
73 => Ok(8), // I
|
||||
74 => Ok(9), // J
|
||||
75 => Ok(10), // K
|
||||
76 => Ok(11), // L
|
||||
77 => Ok(12), // M
|
||||
78 => Ok(13), // N
|
||||
79 => Ok(14), // O
|
||||
80 => Ok(15), // P
|
||||
81 => Ok(16), // Q
|
||||
82 => Ok(17), // R
|
||||
83 => Ok(18), // S
|
||||
84 => Ok(19), // T
|
||||
85 => Ok(20), // U
|
||||
86 => Ok(21), // V
|
||||
87 => Ok(22), // W
|
||||
88 => Ok(23), // X
|
||||
89 => Ok(24), // Y
|
||||
90 => Ok(25), // Z
|
||||
97 => Ok(26), // a
|
||||
98 => Ok(27), // b
|
||||
99 => Ok(28), // c
|
||||
100 => Ok(29), // d
|
||||
101 => Ok(30), // e
|
||||
102 => Ok(31), // f
|
||||
103 => Ok(32), // g
|
||||
104 => Ok(33), // h
|
||||
105 => Ok(34), // i
|
||||
106 => Ok(35), // j
|
||||
107 => Ok(36), // k
|
||||
108 => Ok(37), // l
|
||||
109 => Ok(38), // m
|
||||
110 => Ok(39), // n
|
||||
111 => Ok(40), // o
|
||||
112 => Ok(41), // p
|
||||
113 => Ok(42), // q
|
||||
114 => Ok(43), // r
|
||||
115 => Ok(44), // s
|
||||
116 => Ok(45), // t
|
||||
117 => Ok(46), // u
|
||||
118 => Ok(47), // v
|
||||
119 => Ok(48), // w
|
||||
120 => Ok(49), // x
|
||||
121 => Ok(50), // y
|
||||
122 => Ok(51), // z
|
||||
_ => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid base64")),
|
||||
}
|
||||
}
|
||||
|
||||
fn mime_base64_decode(decoder: &mut MimeBase64Decoder, input: &[u8]) -> io::Result<Vec<u8>> {
|
||||
let mut i = input;
|
||||
let maxlen = ((decoder.nb as usize + i.len()) * 3) / 4;
|
||||
let mut r = vec![0; maxlen];
|
||||
let mut offset = 0;
|
||||
while !i.is_empty() {
|
||||
while decoder.nb < 4 && !i.is_empty() {
|
||||
if mime_base64_map(i[0]).is_ok() || i[0] == b'=' {
|
||||
decoder.tmp[decoder.nb as usize] = i[0];
|
||||
decoder.nb += 1;
|
||||
}
|
||||
i = &i[1..];
|
||||
}
|
||||
if decoder.nb == 4 {
|
||||
decoder.tmp[0] = mime_base64_map(decoder.tmp[0])?;
|
||||
decoder.tmp[1] = mime_base64_map(decoder.tmp[1])?;
|
||||
if decoder.tmp[2] == b'=' {
|
||||
r[offset] = (decoder.tmp[0] << 2) | (decoder.tmp[1] >> 4);
|
||||
offset += 1;
|
||||
} else {
|
||||
decoder.tmp[2] = mime_base64_map(decoder.tmp[2])?;
|
||||
if decoder.tmp[3] == b'=' {
|
||||
r[offset] = (decoder.tmp[0] << 2) | (decoder.tmp[1] >> 4);
|
||||
r[offset + 1] = (decoder.tmp[1] << 4) | (decoder.tmp[2] >> 2);
|
||||
offset += 2;
|
||||
} else {
|
||||
decoder.tmp[3] = mime_base64_map(decoder.tmp[3])?;
|
||||
r[offset] = (decoder.tmp[0] << 2) | (decoder.tmp[1] >> 4);
|
||||
r[offset + 1] = (decoder.tmp[1] << 4) | (decoder.tmp[2] >> 2);
|
||||
r[offset + 2] = (decoder.tmp[2] << 6) | decoder.tmp[3];
|
||||
offset += 3;
|
||||
}
|
||||
}
|
||||
decoder.nb = 0;
|
||||
}
|
||||
}
|
||||
r.truncate(offset);
|
||||
return Ok(r);
|
||||
}
|
||||
|
||||
const MAX_LINE_LEN: u32 = 998; // Def in RFC 2045, excluding CRLF sequence
|
||||
const MAX_ENC_LINE_LEN: usize = 76; /* Def in RFC 2045, excluding CRLF sequence */
|
||||
const MAX_HEADER_NAME: usize = 75; /* 75 + ":" = 76 */
|
||||
const MAX_HEADER_VALUE: usize = 2000; /* Default - arbitrary limit */
|
||||
const MAX_BOUNDARY_LEN: usize = 254;
|
||||
|
||||
fn mime_smtp_parse_line(
|
||||
ctx: &mut MimeStateSMTP, i: &[u8], full: &[u8],
|
||||
) -> (MimeSmtpParserResult, u32) {
|
||||
if ctx.md5_state == MimeSmtpMd5State::MimeSmtpMd5Started {
|
||||
Update::update(&mut ctx.md5, full);
|
||||
}
|
||||
let mut warnings = 0;
|
||||
match ctx.state_flag {
|
||||
MimeSmtpParserState::MimeSmtpStart => {
|
||||
if unsafe { MIME_SMTP_CONFIG_BODY_MD5 }
|
||||
&& ctx.md5_state != MimeSmtpMd5State::MimeSmtpMd5Started
|
||||
{
|
||||
ctx.md5 = Md5::new();
|
||||
ctx.md5_state = MimeSmtpMd5State::MimeSmtpMd5Inited;
|
||||
}
|
||||
if i.is_empty() {
|
||||
let (w, encap_msg) = mime_smtp_process_headers(ctx);
|
||||
warnings |= w;
|
||||
if ctx.main_headers_nb == 0 {
|
||||
ctx.main_headers_nb = ctx.headers.len();
|
||||
}
|
||||
if encap_msg {
|
||||
ctx.state_flag = MimeSmtpParserState::MimeSmtpStart;
|
||||
ctx.headers.truncate(ctx.main_headers_nb);
|
||||
return (MimeSmtpParserResult::MimeSmtpNeedsMore, warnings);
|
||||
}
|
||||
ctx.state_flag = MimeSmtpParserState::MimeSmtpBody;
|
||||
return (MimeSmtpParserResult::MimeSmtpFileOpen, warnings);
|
||||
} else if let Ok((value, name)) = mime::mime_parse_header_line(i) {
|
||||
ctx.state_flag = MimeSmtpParserState::MimeSmtpHeader;
|
||||
let mut h = MimeHeader::default();
|
||||
h.name.extend_from_slice(name);
|
||||
h.value.extend_from_slice(value);
|
||||
if h.name.len() > MAX_HEADER_NAME {
|
||||
warnings |= MIME_ANOM_LONG_HEADER_NAME;
|
||||
}
|
||||
if h.value.len() > MAX_HEADER_VALUE {
|
||||
warnings |= MIME_ANOM_LONG_HEADER_VALUE;
|
||||
}
|
||||
ctx.headers.push(h);
|
||||
} // else event ?
|
||||
}
|
||||
MimeSmtpParserState::MimeSmtpHeader => {
|
||||
if i.is_empty() {
|
||||
let (w, encap_msg) = mime_smtp_process_headers(ctx);
|
||||
warnings |= w;
|
||||
if ctx.main_headers_nb == 0 {
|
||||
ctx.main_headers_nb = ctx.headers.len();
|
||||
}
|
||||
if encap_msg {
|
||||
ctx.state_flag = MimeSmtpParserState::MimeSmtpStart;
|
||||
ctx.headers.truncate(ctx.main_headers_nb);
|
||||
return (MimeSmtpParserResult::MimeSmtpNeedsMore, warnings);
|
||||
}
|
||||
ctx.state_flag = MimeSmtpParserState::MimeSmtpBody;
|
||||
return (MimeSmtpParserResult::MimeSmtpFileOpen, warnings);
|
||||
} else if i[0] == b' ' || i[0] == b'\t' {
|
||||
let last = ctx.headers.len() - 1;
|
||||
ctx.headers[last].value.extend_from_slice(&i[1..]);
|
||||
} else if let Ok((value, name)) = mime::mime_parse_header_line(i) {
|
||||
let mut h = MimeHeader::default();
|
||||
h.name.extend_from_slice(name);
|
||||
h.value.extend_from_slice(value);
|
||||
if h.name.len() > MAX_HEADER_NAME {
|
||||
warnings |= MIME_ANOM_LONG_HEADER_NAME;
|
||||
}
|
||||
if h.value.len() > MAX_HEADER_VALUE {
|
||||
warnings |= MIME_ANOM_LONG_HEADER_VALUE;
|
||||
}
|
||||
ctx.headers.push(h);
|
||||
}
|
||||
}
|
||||
MimeSmtpParserState::MimeSmtpBody => {
|
||||
if ctx.md5_state == MimeSmtpMd5State::MimeSmtpMd5Inited {
|
||||
ctx.md5_state = MimeSmtpMd5State::MimeSmtpMd5Started;
|
||||
Update::update(&mut ctx.md5, full);
|
||||
}
|
||||
let boundary = ctx.boundaries.last();
|
||||
if let Some(b) = boundary {
|
||||
if i.len() >= b.len() && &i[..b.len()] == b {
|
||||
if ctx.encoding == MimeSmtpEncoding::Base64
|
||||
&& unsafe { MIME_SMTP_CONFIG_DECODE_BASE64 }
|
||||
{
|
||||
if let Some(ref mut decoder) = &mut ctx.decoder {
|
||||
if decoder.nb > 0 {
|
||||
// flush the base64 buffer with padding
|
||||
let mut v = Vec::new();
|
||||
for _i in 0..4 - decoder.nb {
|
||||
v.push(b'=');
|
||||
}
|
||||
if let Ok(dec) = mime_base64_decode(decoder, &v) {
|
||||
unsafe {
|
||||
FileAppendData(
|
||||
ctx.files,
|
||||
ctx.sbcfg,
|
||||
dec.as_ptr(),
|
||||
dec.len() as u32,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.state_flag = MimeSmtpParserState::MimeSmtpStart;
|
||||
let toclose = !ctx.filename.is_empty();
|
||||
ctx.filename.clear();
|
||||
ctx.headers.truncate(ctx.main_headers_nb);
|
||||
ctx.encoding = MimeSmtpEncoding::Plain;
|
||||
if i.len() >= b.len() + 2 && i[b.len()] == b'-' && i[b.len() + 1] == b'-' {
|
||||
ctx.boundaries.pop();
|
||||
}
|
||||
if toclose {
|
||||
return (MimeSmtpParserResult::MimeSmtpFileClose, 0);
|
||||
}
|
||||
return (MimeSmtpParserResult::MimeSmtpNeedsMore, 0);
|
||||
}
|
||||
}
|
||||
if ctx.filename.is_empty() {
|
||||
if ctx.content_type == MimeSmtpContentType::PlainText
|
||||
|| ctx.content_type == MimeSmtpContentType::Html
|
||||
|| ctx.content_type == MimeSmtpContentType::Message
|
||||
{
|
||||
mime_smtp_find_url_strings(ctx, full);
|
||||
}
|
||||
return (MimeSmtpParserResult::MimeSmtpNeedsMore, 0);
|
||||
}
|
||||
match ctx.encoding {
|
||||
MimeSmtpEncoding::Plain => {
|
||||
mime_smtp_find_url_strings(ctx, full);
|
||||
if ctx.bufeolen > 0 {
|
||||
unsafe {
|
||||
FileAppendData(
|
||||
ctx.files,
|
||||
ctx.sbcfg,
|
||||
ctx.bufeol.as_ptr(),
|
||||
ctx.bufeol.len() as u32,
|
||||
);
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
FileAppendData(ctx.files, ctx.sbcfg, i.as_ptr(), i.len() as u32);
|
||||
}
|
||||
ctx.bufeolen = (full.len() - i.len()) as u8;
|
||||
if ctx.bufeolen > 0 {
|
||||
ctx.bufeol[..ctx.bufeolen as usize].copy_from_slice(&full[i.len()..]);
|
||||
}
|
||||
}
|
||||
MimeSmtpEncoding::Base64 => {
|
||||
if unsafe { MIME_SMTP_CONFIG_DECODE_BASE64 } {
|
||||
if let Some(ref mut decoder) = &mut ctx.decoder {
|
||||
if i.len() > MAX_ENC_LINE_LEN {
|
||||
warnings |= MIME_ANOM_LONG_ENC_LINE;
|
||||
}
|
||||
if let Ok(dec) = mime_base64_decode(decoder, i) {
|
||||
mime_smtp_find_url_strings(ctx, &dec);
|
||||
unsafe {
|
||||
FileAppendData(
|
||||
ctx.files,
|
||||
ctx.sbcfg,
|
||||
dec.as_ptr(),
|
||||
dec.len() as u32,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warnings |= MIME_ANOM_INVALID_BASE64;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MimeSmtpEncoding::QuotedPrintable => {
|
||||
if unsafe { MIME_SMTP_CONFIG_DECODE_QUOTED } {
|
||||
if i.len() > MAX_ENC_LINE_LEN {
|
||||
warnings |= MIME_ANOM_LONG_ENC_LINE;
|
||||
}
|
||||
let mut c = 0;
|
||||
let mut eol_equal = false;
|
||||
let mut quoted_buffer = Vec::with_capacity(i.len());
|
||||
while c < i.len() {
|
||||
if i[c] == b'=' {
|
||||
if c == i.len() - 1 {
|
||||
eol_equal = true;
|
||||
break;
|
||||
} else if c + 2 >= i.len() {
|
||||
// log event ?
|
||||
warnings |= MIME_ANOM_INVALID_QP;
|
||||
break;
|
||||
}
|
||||
if let Some(v) = hex(i[c + 1]) {
|
||||
if let Some(v2) = hex(i[c + 2]) {
|
||||
quoted_buffer.push((v << 4) | v2);
|
||||
} else {
|
||||
warnings |= MIME_ANOM_INVALID_QP;
|
||||
}
|
||||
} else {
|
||||
warnings |= MIME_ANOM_INVALID_QP;
|
||||
}
|
||||
c += 3;
|
||||
} else {
|
||||
quoted_buffer.push(i[c]);
|
||||
c += 1;
|
||||
}
|
||||
}
|
||||
if !eol_equal {
|
||||
quoted_buffer.extend_from_slice(&full[i.len()..]);
|
||||
}
|
||||
mime_smtp_find_url_strings(ctx, "ed_buffer);
|
||||
unsafe {
|
||||
FileAppendData(
|
||||
ctx.files,
|
||||
ctx.sbcfg,
|
||||
quoted_buffer.as_ptr(),
|
||||
quoted_buffer.len() as u32,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (MimeSmtpParserResult::MimeSmtpFileChunk, warnings);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return (MimeSmtpParserResult::MimeSmtpNeedsMore, warnings);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCSmtpMimeParseLine(
|
||||
input: *const u8, input_len: u32, delim_len: u8, warnings: *mut u32, ctx: &mut MimeStateSMTP,
|
||||
) -> MimeSmtpParserResult {
|
||||
let full_line = build_slice!(input, input_len as usize + delim_len as usize);
|
||||
let line = &full_line[..input_len as usize];
|
||||
let (r, w) = mime_smtp_parse_line(ctx, line, full_line);
|
||||
*warnings = w;
|
||||
if input_len > MAX_LINE_LEN {
|
||||
*warnings |= MIME_ANOM_LONG_LINE;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
fn mime_smtp_complete(ctx: &mut MimeStateSMTP) {
|
||||
if ctx.md5_state == MimeSmtpMd5State::MimeSmtpMd5Started {
|
||||
ctx.md5_state = MimeSmtpMd5State::MimeSmtpMd5Completed;
|
||||
ctx.md5_result = ctx.md5.finalize_reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCSmtpMimeComplete(ctx: &mut MimeStateSMTP) {
|
||||
mime_smtp_complete(ctx);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpGetState(ctx: &mut MimeStateSMTP) -> MimeSmtpParserState {
|
||||
return ctx.state_flag;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpGetFilename(
|
||||
ctx: &mut MimeStateSMTP, buffer: *mut *const u8, filename_len: *mut u16,
|
||||
) {
|
||||
if !ctx.filename.is_empty() {
|
||||
*buffer = ctx.filename.as_ptr();
|
||||
if ctx.filename.len() < u16::MAX.into() {
|
||||
*filename_len = ctx.filename.len() as u16;
|
||||
} else {
|
||||
*filename_len = u16::MAX;
|
||||
}
|
||||
} else {
|
||||
*buffer = std::ptr::null_mut();
|
||||
*filename_len = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpGetHeader(
|
||||
ctx: &mut MimeStateSMTP, str: *const std::os::raw::c_char, buffer: *mut *const u8,
|
||||
buffer_len: *mut u32,
|
||||
) -> bool {
|
||||
let name: &CStr = CStr::from_ptr(str); //unsafe
|
||||
for h in &ctx.headers[ctx.main_headers_nb..] {
|
||||
if mime::slice_equals_lowercase(&h.name, name.to_bytes()) {
|
||||
*buffer = h.value.as_ptr();
|
||||
*buffer_len = h.value.len() as u32;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
*buffer = std::ptr::null_mut();
|
||||
*buffer_len = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpGetHeaderName(
|
||||
ctx: &mut MimeStateSMTP, buffer: *mut *const u8, buffer_len: *mut u32, num: u32,
|
||||
) -> bool {
|
||||
if num as usize + ctx.main_headers_nb < ctx.headers.len() {
|
||||
*buffer = ctx.headers[ctx.main_headers_nb + num as usize]
|
||||
.name
|
||||
.as_ptr();
|
||||
*buffer_len = ctx.headers[ctx.main_headers_nb + num as usize].name.len() as u32;
|
||||
return true;
|
||||
}
|
||||
*buffer = std::ptr::null_mut();
|
||||
*buffer_len = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
static mut MIME_SMTP_CONFIG_DECODE_BASE64: bool = true;
|
||||
static mut MIME_SMTP_CONFIG_DECODE_QUOTED: bool = true;
|
||||
static mut MIME_SMTP_CONFIG_BODY_MD5: bool = false;
|
||||
static mut MIME_SMTP_CONFIG_HEADER_VALUE_DEPTH: u32 = 0;
|
||||
static mut MIME_SMTP_CONFIG_EXTRACT_URLS: bool = true;
|
||||
static mut MIME_SMTP_CONFIG_LOG_URL_SCHEME: bool = false;
|
||||
static mut MIME_SMTP_CONFIG_EXTRACT_URL_SCHEMES: Vec<&str> = Vec::new();
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpConfigDecodeBase64(val: std::os::raw::c_int) {
|
||||
MIME_SMTP_CONFIG_DECODE_BASE64 = val != 0;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpConfigDecodeQuoted(val: std::os::raw::c_int) {
|
||||
MIME_SMTP_CONFIG_DECODE_QUOTED = val != 0;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpConfigExtractUrls(val: std::os::raw::c_int) {
|
||||
MIME_SMTP_CONFIG_EXTRACT_URLS = val != 0;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpConfigLogUrlScheme(val: std::os::raw::c_int) {
|
||||
MIME_SMTP_CONFIG_LOG_URL_SCHEME = val != 0;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpConfigBodyMd5(val: std::os::raw::c_int) {
|
||||
MIME_SMTP_CONFIG_BODY_MD5 = val != 0;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpConfigHeaderValueDepth(val: u32) {
|
||||
MIME_SMTP_CONFIG_HEADER_VALUE_DEPTH = val;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpConfigExtractUrlsSchemeReset() {
|
||||
MIME_SMTP_CONFIG_EXTRACT_URL_SCHEMES.clear();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpConfigExtractUrlsSchemeAdd(
|
||||
str: *const std::os::raw::c_char,
|
||||
) -> std::os::raw::c_int {
|
||||
let scheme: &CStr = CStr::from_ptr(str); //unsafe
|
||||
if let Ok(s) = scheme.to_str() {
|
||||
MIME_SMTP_CONFIG_EXTRACT_URL_SCHEMES.push(s);
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
@ -0,0 +1,241 @@
|
||||
/* Copyright (C) 2022 Open Information Security Foundation
|
||||
*
|
||||
* You can copy, redistribute or modify this Program under the terms of
|
||||
* the GNU General Public License version 2 as published by the Free
|
||||
* Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* version 2 along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*/
|
||||
|
||||
use super::mime;
|
||||
use crate::jsonbuilder::{JsonBuilder, JsonError};
|
||||
use crate::mime::smtp::{MimeSmtpMd5State, MimeStateSMTP};
|
||||
use digest::Digest;
|
||||
use digest::Update;
|
||||
use md5::Md5;
|
||||
use std::ffi::CStr;
|
||||
|
||||
fn log_subject_md5(js: &mut JsonBuilder, ctx: &mut MimeStateSMTP) -> Result<(), JsonError> {
|
||||
for h in &ctx.headers[..ctx.main_headers_nb] {
|
||||
if mime::slice_equals_lowercase(&h.name, b"subject") {
|
||||
let hash = format!("{:x}", Md5::new().chain(&h.value).finalize());
|
||||
js.set_string("subject_md5", &hash)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpLogSubjectMd5(
|
||||
js: &mut JsonBuilder, ctx: &mut MimeStateSMTP,
|
||||
) -> bool {
|
||||
return log_subject_md5(js, ctx).is_ok();
|
||||
}
|
||||
|
||||
fn log_body_md5(js: &mut JsonBuilder, ctx: &mut MimeStateSMTP) -> Result<(), JsonError> {
|
||||
if ctx.md5_state == MimeSmtpMd5State::MimeSmtpMd5Completed {
|
||||
let hash = format!("{:x}", ctx.md5_result);
|
||||
js.set_string("body_md5", &hash)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpLogBodyMd5(
|
||||
js: &mut JsonBuilder, ctx: &mut MimeStateSMTP,
|
||||
) -> bool {
|
||||
return log_body_md5(js, ctx).is_ok();
|
||||
}
|
||||
|
||||
fn log_field_array(
|
||||
js: &mut JsonBuilder, ctx: &mut MimeStateSMTP, c: &str, e: &str,
|
||||
) -> Result<(), JsonError> {
|
||||
let mark = js.get_mark();
|
||||
let mut found = false;
|
||||
js.open_array(c)?;
|
||||
|
||||
for h in &ctx.headers[..ctx.main_headers_nb] {
|
||||
if mime::slice_equals_lowercase(&h.name, e.as_bytes()) {
|
||||
found = true;
|
||||
js.append_string(&String::from_utf8_lossy(&h.value))?;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
js.close()?;
|
||||
} else {
|
||||
js.restore_mark(&mark)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpLogFieldArray(
|
||||
js: &mut JsonBuilder, ctx: &mut MimeStateSMTP, email: *const std::os::raw::c_char,
|
||||
config: *const std::os::raw::c_char,
|
||||
) -> bool {
|
||||
let e: &CStr = CStr::from_ptr(email); //unsafe
|
||||
if let Ok(email_field) = e.to_str() {
|
||||
let c: &CStr = CStr::from_ptr(config); //unsafe
|
||||
if let Ok(config_field) = c.to_str() {
|
||||
return log_field_array(js, ctx, config_field, email_field).is_ok();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
enum FieldCommaState {
|
||||
Start = 0, // skip leading spaces
|
||||
Field = 1,
|
||||
Quoted = 2, // do not take comma for split in quote
|
||||
}
|
||||
|
||||
fn log_field_comma(
|
||||
js: &mut JsonBuilder, ctx: &mut MimeStateSMTP, c: &str, e: &str,
|
||||
) -> Result<(), JsonError> {
|
||||
for h in &ctx.headers[..ctx.main_headers_nb] {
|
||||
if mime::slice_equals_lowercase(&h.name, e.as_bytes()) {
|
||||
let mark = js.get_mark();
|
||||
let mut has_not_empty_field = false;
|
||||
js.open_array(c)?;
|
||||
let mut start = 0;
|
||||
let mut state = FieldCommaState::Start;
|
||||
for i in 0..h.value.len() {
|
||||
match state {
|
||||
FieldCommaState::Start => {
|
||||
if h.value[i] == b' ' || h.value[i] == b'\t' {
|
||||
start += 1;
|
||||
} else if h.value[i] == b'"' {
|
||||
state = FieldCommaState::Quoted;
|
||||
} else {
|
||||
state = FieldCommaState::Field;
|
||||
}
|
||||
}
|
||||
FieldCommaState::Field => {
|
||||
if h.value[i] == b',' {
|
||||
if i > start {
|
||||
js.append_string(&String::from_utf8_lossy(&h.value[start..i]))?;
|
||||
has_not_empty_field = true;
|
||||
}
|
||||
start = i + 1;
|
||||
state = FieldCommaState::Start;
|
||||
} else if h.value[i] == b'"' {
|
||||
state = FieldCommaState::Quoted;
|
||||
}
|
||||
}
|
||||
FieldCommaState::Quoted => {
|
||||
if h.value[i] == b'"' {
|
||||
state = FieldCommaState::Field;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if h.value.len() > start {
|
||||
// do not log empty string
|
||||
js.append_string(&String::from_utf8_lossy(&h.value[start..]))?;
|
||||
has_not_empty_field = true;
|
||||
}
|
||||
if has_not_empty_field {
|
||||
js.close()?;
|
||||
} else {
|
||||
js.restore_mark(&mark)?;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpLogFieldComma(
|
||||
js: &mut JsonBuilder, ctx: &mut MimeStateSMTP, email: *const std::os::raw::c_char,
|
||||
config: *const std::os::raw::c_char,
|
||||
) -> bool {
|
||||
let e: &CStr = CStr::from_ptr(email); //unsafe
|
||||
if let Ok(email_field) = e.to_str() {
|
||||
let c: &CStr = CStr::from_ptr(config); //unsafe
|
||||
if let Ok(config_field) = c.to_str() {
|
||||
return log_field_comma(js, ctx, config_field, email_field).is_ok();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn log_field_string(
|
||||
js: &mut JsonBuilder, ctx: &mut MimeStateSMTP, c: &str, e: &str,
|
||||
) -> Result<(), JsonError> {
|
||||
for h in &ctx.headers[..ctx.main_headers_nb] {
|
||||
if mime::slice_equals_lowercase(&h.name, e.as_bytes()) {
|
||||
js.set_string(c, &String::from_utf8_lossy(&h.value))?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpLogFieldString(
|
||||
js: &mut JsonBuilder, ctx: &mut MimeStateSMTP, email: *const std::os::raw::c_char,
|
||||
config: *const std::os::raw::c_char,
|
||||
) -> bool {
|
||||
let e: &CStr = CStr::from_ptr(email); //unsafe
|
||||
if let Ok(email_field) = e.to_str() {
|
||||
let c: &CStr = CStr::from_ptr(config); //unsafe
|
||||
if let Ok(config_field) = c.to_str() {
|
||||
return log_field_string(js, ctx, config_field, email_field).is_ok();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn log_data_header(
|
||||
js: &mut JsonBuilder, ctx: &mut MimeStateSMTP, hname: &str,
|
||||
) -> Result<(), JsonError> {
|
||||
for h in &ctx.headers[..ctx.main_headers_nb] {
|
||||
if mime::slice_equals_lowercase(&h.name, hname.as_bytes()) {
|
||||
js.set_string(hname, &String::from_utf8_lossy(&h.value))?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn log_data(js: &mut JsonBuilder, ctx: &mut MimeStateSMTP) -> Result<(), JsonError> {
|
||||
log_data_header(js, ctx, "from")?;
|
||||
log_field_comma(js, ctx, "to", "to")?;
|
||||
log_field_comma(js, ctx, "cc", "cc")?;
|
||||
|
||||
js.set_string("status", "PARSE_DONE")?;
|
||||
|
||||
if !ctx.attachments.is_empty() {
|
||||
js.open_array("attachment")?;
|
||||
for a in &ctx.attachments {
|
||||
js.append_string(&String::from_utf8_lossy(a))?;
|
||||
}
|
||||
js.close()?;
|
||||
}
|
||||
if !ctx.urls.is_empty() {
|
||||
js.open_array("url")?;
|
||||
for a in ctx.urls.iter().rev() {
|
||||
js.append_string(&String::from_utf8_lossy(a))?;
|
||||
}
|
||||
js.close()?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn SCMimeSmtpLogData(js: &mut JsonBuilder, ctx: &mut MimeStateSMTP) -> bool {
|
||||
return log_data(js, ctx).is_ok();
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,243 +0,0 @@
|
||||
/* Copyright (C) 2012 BAE Systems
|
||||
* Copyright (C) 2021 Open Information Security Foundation
|
||||
*
|
||||
* You can copy, redistribute or modify this Program under the terms of
|
||||
* the GNU General Public License version 2 as published by the Free
|
||||
* Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* version 2 along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file
|
||||
*
|
||||
* \author David Abarbanel <david.abarbanel@baesystems.com>
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MIME_DECODE_H_
|
||||
#define MIME_DECODE_H_
|
||||
|
||||
#include "conf.h"
|
||||
#include "util-base64.h"
|
||||
#include "util-file.h"
|
||||
|
||||
/* Content Flags */
|
||||
#define CTNT_IS_MSG 1
|
||||
#define CTNT_IS_ENV 2
|
||||
#define CTNT_IS_ENCAP 4
|
||||
#define CTNT_IS_BODYPART 8
|
||||
#define CTNT_IS_MULTIPART 16
|
||||
#define CTNT_IS_ATTACHMENT 32
|
||||
#define CTNT_IS_BASE64 64
|
||||
#define CTNT_IS_QP 128
|
||||
#define CTNT_IS_TEXT 256
|
||||
#define CTNT_IS_HTML 512
|
||||
|
||||
/* URL Flags */
|
||||
#define URL_IS_IP4 1
|
||||
#define URL_IS_IP6 2
|
||||
#define URL_IS_EXE 4
|
||||
|
||||
/* Anomaly Flags */
|
||||
#define ANOM_INVALID_BASE64 1 /* invalid base64 chars */
|
||||
#define ANOM_INVALID_QP 2 /* invalid quoted-printable chars */
|
||||
#define ANOM_LONG_HEADER_NAME 4 /* header is abnormally long */
|
||||
#define ANOM_LONG_HEADER_VALUE 8 /* header value is abnormally long
|
||||
* (includes multi-line) */
|
||||
#define ANOM_LONG_LINE 16 /* Lines that exceed 998 octets */
|
||||
#define ANOM_LONG_ENC_LINE 32 /* Lines that exceed 76 octets */
|
||||
#define ANOM_MALFORMED_MSG 64 /* Misc msg format errors found */
|
||||
#define ANOM_LONG_BOUNDARY 128 /* Boundary too long */
|
||||
#define ANOM_LONG_FILENAME 256 /* filename truncated */
|
||||
|
||||
/* Publicly exposed size constants */
|
||||
#define DATA_CHUNK_SIZE 3072 /* Should be divisible by 3 */
|
||||
|
||||
/* Mime Parser Constants */
|
||||
#define HEADER_READY 0x01
|
||||
#define HEADER_STARTED 0x02
|
||||
#define HEADER_DONE 0x03
|
||||
#define BODY_STARTED 0x04
|
||||
#define BODY_DONE 0x05
|
||||
#define BODY_END_BOUND 0x06
|
||||
#define PARSE_DONE 0x07
|
||||
#define PARSE_ERROR 0x08
|
||||
|
||||
/**
|
||||
* \brief Mime Decoder Error Codes
|
||||
*/
|
||||
typedef enum MimeDecRetCode {
|
||||
MIME_DEC_OK = 0,
|
||||
MIME_DEC_MORE = 1,
|
||||
MIME_DEC_ERR_DATA = -1,
|
||||
MIME_DEC_ERR_MEM = -2,
|
||||
MIME_DEC_ERR_PARSE = -3,
|
||||
MIME_DEC_ERR_STATE = -4, /**< parser in error state */
|
||||
MIME_DEC_ERR_OVERFLOW = -5,
|
||||
} MimeDecRetCode;
|
||||
|
||||
/**
|
||||
* \brief Structure for containing configuration options
|
||||
*
|
||||
*/
|
||||
typedef struct MimeDecConfig {
|
||||
bool decode_base64; /**< Decode base64 bodies */
|
||||
bool decode_quoted_printable; /**< Decode quoted-printable bodies */
|
||||
bool extract_urls; /**< Extract and store URLs in data structure */
|
||||
ConfNode *extract_urls_schemes; /**< List of schemes of which to
|
||||
extract urls */
|
||||
bool log_url_scheme; /**< Log the scheme of extracted URLs */
|
||||
bool body_md5; /**< Compute md5 sum of body */
|
||||
uint32_t header_value_depth; /**< Depth of which to store header values
|
||||
(Default is 2000) */
|
||||
} MimeDecConfig;
|
||||
|
||||
/**
|
||||
* \brief This represents a header field name and associated value
|
||||
*/
|
||||
typedef struct MimeDecField {
|
||||
uint8_t *name; /**< Name of the header field */
|
||||
uint32_t name_len; /**< Length of the name */
|
||||
uint32_t value_len; /**< Length of the value */
|
||||
uint8_t *value; /**< Value of the header field */
|
||||
struct MimeDecField *next; /**< Pointer to next field */
|
||||
} MimeDecField;
|
||||
|
||||
/**
|
||||
* \brief This represents a URL value node in a linked list
|
||||
*
|
||||
* Since HTML can sometimes contain a high number of URLs, this
|
||||
* structure only features the URL host name/IP or those that are
|
||||
* pointing to an executable file (see url_flags to determine which).
|
||||
*/
|
||||
typedef struct MimeDecUrl {
|
||||
uint8_t *url; /**< String representation of full or partial URL (lowercase) */
|
||||
uint32_t url_len; /**< Length of the URL string */
|
||||
uint32_t url_flags; /**< Flags indicating type of URL */
|
||||
struct MimeDecUrl *next; /**< Pointer to next URL */
|
||||
} MimeDecUrl;
|
||||
|
||||
/**
|
||||
* \brief This represents the MIME Entity (or also top level message) in a
|
||||
* child-sibling tree
|
||||
*/
|
||||
typedef struct MimeDecEntity {
|
||||
MimeDecField *field_list; /**< Pointer to list of header fields */
|
||||
MimeDecUrl *url_list; /**< Pointer to list of URLs */
|
||||
uint32_t header_flags; /**< Flags indicating header characteristics */
|
||||
uint32_t ctnt_flags; /**< Flags indicating type of content */
|
||||
uint32_t anomaly_flags; /**< Flags indicating an anomaly in the message */
|
||||
uint32_t filename_len; /**< Length of file attachment name */
|
||||
uint8_t *filename; /**< Name of file attachment */
|
||||
uint8_t *ctnt_type; /**< Quick access pointer to short-hand content type field */
|
||||
uint32_t ctnt_type_len; /**< Length of content type field value */
|
||||
uint32_t msg_id_len; /**< Quick access pointer to message Id */
|
||||
uint8_t *msg_id; /**< Quick access pointer to message Id */
|
||||
struct MimeDecEntity *next; /**< Pointer to list of sibling entities */
|
||||
struct MimeDecEntity *child; /**< Pointer to list of child entities */
|
||||
struct MimeDecEntity *last_child; /**< Pointer to tail of the list of child entities */
|
||||
} MimeDecEntity;
|
||||
|
||||
/**
|
||||
* \brief Structure contains boundary and entity for the current node (entity)
|
||||
* in the stack
|
||||
*
|
||||
*/
|
||||
typedef struct MimeDecStackNode {
|
||||
MimeDecEntity *data; /**< Pointer to the entity data structure */
|
||||
uint8_t *bdef; /**< Copy of boundary definition for child entity */
|
||||
uint16_t bdef_len; /**< Boundary length for child entity */
|
||||
bool is_encap; /**< Flag indicating entity is encapsulated in message */
|
||||
struct MimeDecStackNode *next; /**< Pointer to next item on the stack */
|
||||
} MimeDecStackNode;
|
||||
|
||||
/**
|
||||
* \brief Structure holds the top of the stack along with some free reusable nodes
|
||||
*
|
||||
*/
|
||||
typedef struct MimeDecStack {
|
||||
MimeDecStackNode *top; /**< Pointer to the top of the stack */
|
||||
MimeDecStackNode *free_nodes; /**< Pointer to the list of free nodes */
|
||||
uint32_t free_nodes_cnt; /**< Count of free nodes in the list */
|
||||
} MimeDecStack;
|
||||
|
||||
/**
|
||||
* \brief Structure contains a list of value and lengths for robust data processing
|
||||
*
|
||||
*/
|
||||
typedef struct DataValue {
|
||||
uint8_t *value; /**< Copy of data value */
|
||||
uint32_t value_len; /**< Length of data value */
|
||||
struct DataValue *next; /**< Pointer to next value in the list */
|
||||
} DataValue;
|
||||
|
||||
/**
|
||||
* \brief Structure contains the current state of the MIME parser
|
||||
*
|
||||
*/
|
||||
typedef struct MimeDecParseState {
|
||||
MimeDecEntity *msg; /**< Pointer to the top-level message entity */
|
||||
MimeDecStack *stack; /**< Pointer to the top of the entity stack */
|
||||
uint8_t *hname; /**< Copy of the last known header name */
|
||||
uint32_t hlen; /**< Length of the last known header name */
|
||||
uint32_t hvlen; /**< Total length of value list */
|
||||
DataValue *hvalue; /**< Pointer to the incomplete header value list */
|
||||
uint8_t bvremain[B64_BLOCK]; /**< Remainder from base64-decoded line */
|
||||
uint8_t bvr_len; /**< Length of remainder from base64-decoded line */
|
||||
uint8_t data_chunk[DATA_CHUNK_SIZE]; /**< Buffer holding data chunk */
|
||||
SCMd5 *md5_ctx;
|
||||
uint8_t md5[SC_MD5_LEN];
|
||||
bool has_md5;
|
||||
uint8_t state_flag; /**< Flag representing current state of parser */
|
||||
uint32_t data_chunk_len; /**< Length of data chunk */
|
||||
int found_child; /**< Flag indicating a child entity was found */
|
||||
int body_begin; /**< Currently at beginning of body */
|
||||
int body_end; /**< Currently at end of body */
|
||||
uint8_t current_line_delimiter_len; /**< Length of line delimiter */
|
||||
void *data; /**< Pointer to data specific to the caller */
|
||||
int (*DataChunkProcessorFunc) (const uint8_t *chunk, uint32_t len,
|
||||
struct MimeDecParseState *state); /**< Data chunk processing function callback */
|
||||
} MimeDecParseState;
|
||||
|
||||
/* Config functions */
|
||||
void MimeDecSetConfig(MimeDecConfig *config);
|
||||
MimeDecConfig * MimeDecGetConfig(void);
|
||||
|
||||
/* Memory functions */
|
||||
void MimeDecFreeEntity(MimeDecEntity *entity);
|
||||
void MimeDecFreeField(MimeDecField *field);
|
||||
void MimeDecFreeUrl(MimeDecUrl *url);
|
||||
|
||||
/* List functions */
|
||||
MimeDecField * MimeDecAddField(MimeDecEntity *entity);
|
||||
MimeDecField * MimeDecFindField(const MimeDecEntity *entity, const char *name);
|
||||
int MimeDecFindFieldsForEach(const MimeDecEntity *entity, const char *name, int (*DataCallback)(const uint8_t *val, const size_t, void *data), void *data);
|
||||
MimeDecEntity * MimeDecAddEntity(MimeDecEntity *parent);
|
||||
|
||||
/* Helper functions */
|
||||
//MimeDecField * MimeDecFillField(MimeDecEntity *entity, const char *name,
|
||||
// uint32_t nlen, const char *value, uint32_t vlen, int copy_name_value);
|
||||
|
||||
/* Parser functions */
|
||||
MimeDecParseState * MimeDecInitParser(void *data, int (*dcpfunc)(const uint8_t *chunk,
|
||||
uint32_t len, MimeDecParseState *state));
|
||||
void MimeDecDeInitParser(MimeDecParseState *state);
|
||||
int MimeDecParseComplete(MimeDecParseState *state);
|
||||
int MimeDecParseLine(const uint8_t *line, const uint32_t len, const uint8_t delim_len, MimeDecParseState *state);
|
||||
MimeDecEntity * MimeDecParseFullMsg(const uint8_t *buf, uint32_t blen, void *data,
|
||||
int (*DataChunkProcessorFunc)(const uint8_t *chunk, uint32_t len, MimeDecParseState *state));
|
||||
const char *MimeDecParseStateGetStatus(MimeDecParseState *state);
|
||||
|
||||
/* Test functions */
|
||||
void MimeDecRegisterTests(void);
|
||||
|
||||
#endif
|
Loading…
Reference in New Issue