diff --git a/doc/userguide/upgrade.rst b/doc/userguide/upgrade.rst index 8ff94a445b92..546b1c7dd13a 100644 --- a/doc/userguide/upgrade.rst +++ b/doc/userguide/upgrade.rst @@ -70,6 +70,7 @@ Major changes `. - PF_RING support has been moved to a plugin. See :doc:`PF_RING plugin `. +- LDAP parser and logger have been introduced. Removals ~~~~~~~~ diff --git a/etc/schema.json b/etc/schema.json index 944410a6b2b7..cffb15afd357 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -2376,6 +2376,366 @@ }, "additionalProperties": false }, + "ldap": { + "type": "object", + "optional": true, + "properties": { + "request": { + "type": "object", + "properties": { + "operation": { + "type": "string" + }, + "message_id": { + "type": "integer" + }, + "search_request": { + "type": "object", + "optional": "true", + "properties": { + "base_object": { + "type": "string" + }, + "scope": { + "type": "integer" + }, + "deref_alias": { + "type": "integer" + }, + "size_limit": { + "type": "integer" + }, + "time_limit": { + "type": "integer" + }, + "types_online": { + "type": "boolean" + }, + "attributes": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "bind_request": { + "type": "object", + "optional": "true", + "properties": { + "version": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "sasl": { + "type": "object", + "optional": "true", + "properties": { + "mechanism": { + "type": "string" + }, + "credentials": { + "type": "string", + "optional": "true" + } + } + } + } + }, + "modify_request": { + "type": "object", + "optional": "true", + "properties": { + "object": { + "type": "string" + }, + "changes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "operation": { + "type": "string" + }, + "modification": { + "type": "object", + "properties": { + "attribute_type": { + "type": "string" + }, + "attribute_values": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "add_request": { + "type": "object", + "optional": "true", + "properties": { + "entry": { + "type": "string" + }, + "attributes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "values": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + } + } + } + }, + "del_request": { + "type": "object", + "optional": "true", + "properties": { + "dn": { + "type": "string" + } + } + }, + "mod_dn_request": { + "type": "object", + "optional": "true", + "properties": { + "entry": { + "type": "string" + }, + "new_rdn": { + "type": "string" + }, + "delete_old_rdn": { + "type": "boolean" + }, + "new_superior": { + "type": "string", + "optional": "true" + } + } + }, + "compare_request": { + "type": "object", + "optional": "true", + "properties": { + "entry": { + "type": "string" + }, + "attribute_value_assertion": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + }, + "abandon_request": { + "type": "object", + "optional": "true", + "properties": { + "message_id": { + "type": "integer" + } + } + }, + "extended_request": { + "type": "object", + "optional": "true", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string", + "optional": "true" + } + } + } + }, + "additionalProperties": false + }, + "responses": { + "type": "array", + "optional": "true", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "search_result_done": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "bind_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + }, + "server_sasl_creds": { + "type": "string", + "optional": "true" + } + } + }, + "modify_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "add_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "del_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "mod_dn_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "compare_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "extended_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "intermediate_response": { + "type": "object", + "optional": "true", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + } + } + } + }, "metadata": { "type": "object", "optional": true, @@ -4277,6 +4637,10 @@ "Errors encountered parsing Kerberos v5/UDP protocol", "$ref": "#/$defs/stats_applayer_error" }, + "ldap": { + "description": "Errors encountered parsing LDAP protocol", + "$ref": "#/$defs/stats_applayer_error" + }, "modbus": { "description": "Errors encountered parsing Modbus protocol", "$ref": "#/$defs/stats_applayer_error" @@ -4441,6 +4805,10 @@ "description": "Number of flows for Kerberos v5/UDP protocol", "type": "integer" }, + "ldap": { + "description": "Number of flows for LDAP protocol", + "type": "integer" + }, "modbus": { "description": "Number of flows for Modbus protocol", "type": "integer" @@ -4600,6 +4968,10 @@ "Number of transactions for Kerberos v5/UDP protocol", "type": "integer" }, + "ldap": { + "description": "Number of transactions for LDAP protocol", + "type": "integer" + }, "modbus": { "description": "Number of transactions for Modbus protocol", "type": "integer" diff --git a/rust/Cargo.lock.in b/rust/Cargo.lock.in index f3b1a2eace16..dac92d5a302c 100644 --- a/rust/Cargo.lock.in +++ b/rust/Cargo.lock.in @@ -412,6 +412,17 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "ldap-parser" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798f8bf8793bb624e6b840f1c73949949709f9301dc63ddb236e9230674575c9" +dependencies = [ + "asn1-rs", + "rusticata-macros", + "thiserror", +] + [[package]] name = "libc" version = "0.2.153" @@ -937,6 +948,7 @@ dependencies = [ "ipsec-parser", "kerberos-parser", "lazy_static", + "ldap-parser", "libc", "lzma-rs", "md-5", diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index f750765d13c3..69e13347f967 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -61,6 +61,7 @@ lazy_static = "~1.4.0" base64 = "~0.13.0" bendy = { version = "~0.3.3", default-features = false } asn1-rs = { version = "~0.6.1" } +ldap-parser = { version = "~0.4.0" } # last version to work with MSRV 1.63 time = "=0.3.20" diff --git a/rust/src/ldap/filters.rs b/rust/src/ldap/filters.rs new file mode 100644 index 000000000000..434642ad0fe3 --- /dev/null +++ b/rust/src/ldap/filters.rs @@ -0,0 +1,226 @@ +/* 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. + */ + +// written by Giuseppe Longo + +use crate::ldap::types::LdapString; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Filter { + And(Vec), + Or(Vec), + Not(Box), + EqualityMatch(AttributeValueAssertion), + Substrings(SubstringFilter), + GreaterOrEqual(AttributeValueAssertion), + LessOrEqual(AttributeValueAssertion), + Present(LdapString), + ApproxMatch(AttributeValueAssertion), + ExtensibleMatch(MatchingRuleAssertion), +} + +impl<'a> From> for Filter { + fn from(f: ldap_parser::filter::Filter) -> Self { + match f { + ldap_parser::filter::Filter::And(val) => { + let mut vec = Vec::new(); + for filter in val { + vec.push(filter.into()); + } + Filter::And(vec) + } + ldap_parser::filter::Filter::Or(val) => { + let mut vec = Vec::new(); + for filter in val { + vec.push(filter.into()); + } + Filter::Or(vec) + } + ldap_parser::filter::Filter::Not(val) => { + let f = *val; + let f2: Filter = f.into(); + Filter::Not(Box::from(f2)) + } + ldap_parser::filter::Filter::EqualityMatch(val) => { + Filter::EqualityMatch(AttributeValueAssertion { + attribute_desc: LdapString(val.attribute_desc.0.to_string()), + assertion_value: val.assertion_value.to_vec(), + }) + } + ldap_parser::filter::Filter::Substrings(val) => { + let filter_type = LdapString(val.filter_type.0.to_string()); + let mut substrings: Vec = Vec::new(); + for s in val.substrings { + substrings.push(s.into()); + } + Filter::Substrings(SubstringFilter { + filter_type, + substrings, + }) + } + ldap_parser::filter::Filter::GreaterOrEqual(val) => { + Filter::GreaterOrEqual(AttributeValueAssertion { + attribute_desc: LdapString(val.attribute_desc.0.to_string()), + assertion_value: val.assertion_value.to_vec(), + }) + } + ldap_parser::filter::Filter::LessOrEqual(val) => { + Filter::LessOrEqual(AttributeValueAssertion { + attribute_desc: LdapString(val.attribute_desc.0.to_string()), + assertion_value: val.assertion_value.to_vec(), + }) + } + ldap_parser::filter::Filter::Present(val) => { + Filter::Present(LdapString(val.0.to_string())) + } + ldap_parser::filter::Filter::ApproxMatch(val) => { + Filter::ApproxMatch(AttributeValueAssertion { + attribute_desc: LdapString(val.attribute_desc.0.to_string()), + assertion_value: val.assertion_value.to_vec(), + }) + } + ldap_parser::filter::Filter::ExtensibleMatch(val) => { + let matching_rule = if let Some(mr) = val.matching_rule { + Some(LdapString(mr.0.to_string())) + } else { + None + }; + let rule_type = if let Some(rt) = val.rule_type { + Some(AttributeDescription(rt.0.to_string())) + } else { + None + }; + let assertion_value = AssertionValue(val.assertion_value.0.to_vec()); + let dn_attributes = val.dn_attributes; + Filter::ExtensibleMatch(MatchingRuleAssertion { + matching_rule, + rule_type, + assertion_value, + dn_attributes, + }) + } + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PartialAttribute { + pub attr_type: LdapString, + pub attr_vals: Vec, +} + +impl<'a> From<&ldap_parser::filter::PartialAttribute<'a>> for PartialAttribute { + fn from(value: &ldap_parser::filter::PartialAttribute) -> Self { + let attr_type = LdapString(value.attr_type.0.to_string()); + let attr_vals: Vec = value + .attr_vals + .iter() + .map(|a| AttributeValue(a.0.to_vec())) + .collect(); + + Self { + attr_type, + attr_vals, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Attribute { + pub attr_type: LdapString, + pub attr_vals: Vec, +} + +impl<'a> From<&ldap_parser::filter::Attribute<'a>> for Attribute { + fn from(value: &ldap_parser::filter::Attribute) -> Self { + let attr_type = LdapString(value.attr_type.0.to_string()); + let attr_vals: Vec = value + .attr_vals + .iter() + .map(|a| AttributeValue(a.0.to_vec())) + .collect(); + + Self { + attr_type, + attr_vals, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AttributeValueAssertion { + pub attribute_desc: LdapString, + pub assertion_value: Vec, +} +impl<'a> From<&ldap_parser::filter::AttributeValueAssertion<'a>> for AttributeValueAssertion { + fn from(value: &ldap_parser::filter::AttributeValueAssertion) -> Self { + let attribute_desc = LdapString(value.attribute_desc.0.to_string()); + let assertion_value = value.assertion_value.to_vec(); + Self { + attribute_desc, + assertion_value, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AttributeDescription(pub String); + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MatchingRuleAssertion { + pub matching_rule: Option, + pub rule_type: Option, + pub assertion_value: AssertionValue, + pub dn_attributes: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MatchingRuleId(pub String); + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SubstringFilter { + pub filter_type: LdapString, + pub substrings: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Substring { + Initial(AssertionValue), + Any(AssertionValue), + Final(AssertionValue), +} +impl<'a> From> for Substring { + fn from(value: ldap_parser::filter::Substring) -> Self { + match value { + ldap_parser::filter::Substring::Initial(val) => { + Substring::Initial(AssertionValue(val.0.to_vec())) + } + ldap_parser::filter::Substring::Any(val) => { + Substring::Any(AssertionValue(val.0.to_vec())) + } + ldap_parser::filter::Substring::Final(val) => { + Substring::Final(AssertionValue(val.0.to_vec())) + } + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssertionValue(pub Vec); + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AttributeValue(pub Vec); diff --git a/rust/src/ldap/ldap.rs b/rust/src/ldap/ldap.rs new file mode 100644 index 000000000000..0817b4c9fba5 --- /dev/null +++ b/rust/src/ldap/ldap.rs @@ -0,0 +1,429 @@ +/* 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. + */ + +// written by Giuseppe Longo + +use crate::applayer::{self, *}; +use crate::conf::conf_get; +use crate::core::*; +use nom7 as nom; +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::os::raw::{c_char, c_int, c_void}; + +use crate::ldap::types::*; + +static mut LDAP_MAX_TX: usize = 256; + +static mut ALPROTO_LDAP: AppProto = ALPROTO_UNKNOWN; + +#[derive(AppLayerEvent)] +enum LdapEvent { + TooManyTransactions, + InvalidData, + RequestNotFound, +} + +#[derive(Debug)] +pub struct LdapTransaction { + pub tx_id: u64, + pub request: Option, + pub responses: VecDeque, + complete: bool, + + tx_data: AppLayerTxData, +} + +impl Default for LdapTransaction { + fn default() -> Self { + Self::new() + } +} + +impl LdapTransaction { + pub fn new() -> LdapTransaction { + Self { + tx_id: 0, + request: None, + responses: VecDeque::new(), + complete: false, + tx_data: AppLayerTxData::new(), + } + } +} + +impl Transaction for LdapTransaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +#[derive(Default)] +pub struct LdapState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + tx_index_completed: usize, +} + +impl State for LdapState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&LdapTransaction> { + self.transactions.get(index) + } +} + +impl LdapState { + pub fn new() -> Self { + Self { + state_data: AppLayerStateData::new(), + tx_id: 0, + transactions: VecDeque::new(), + tx_index_completed: 0, + } + } + + // Free a transaction by ID. + fn free_tx(&mut self, tx_id: u64) { + let len = self.transactions.len(); + let mut found = false; + let mut index = 0; + for i in 0..len { + let tx = &self.transactions[i]; + if tx.tx_id == tx_id + 1 { + found = true; + index = i; + break; + } + } + if found { + self.transactions.remove(index); + } + } + + pub fn get_tx(&mut self, tx_id: u64) -> Option<&LdapTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + pub fn new_tx(&mut self) -> LdapTransaction { + let mut tx = LdapTransaction::new(); + self.tx_id += 1; + tx.tx_id = self.tx_id; + if self.transactions.len() > unsafe { LDAP_MAX_TX } { + let mut index = self.tx_index_completed; + for tx_old in &mut self.transactions.range_mut(self.tx_index_completed..) { + index += 1; + if !tx_old.complete { + tx_old.complete = true; + tx_old + .tx_data + .set_event(LdapEvent::TooManyTransactions as u8); + break; + } + } + self.tx_index_completed = index; + } + return tx; + } + + fn set_event(&mut self, e: LdapEvent) { + if let Some(tx) = self.transactions.back_mut() { + tx.tx_data.set_event(e as u8); + } + } + + fn find_request(&mut self, message_id: MessageID) -> Option<&mut LdapTransaction> { + self.transactions.iter_mut().find(|tx| { + tx.request + .as_ref() + .map_or(false, |req| req.message_id == message_id) + }) + } + + fn parse_request(&mut self, input: &[u8]) -> AppLayerResult { + if input.is_empty() { + return AppLayerResult::ok(); + } + + let mut start = input; + while !start.is_empty() { + match ldap_parse_msg(start) { + Ok((rem, msg)) => { + start = rem; + let mut tx = self.new_tx(); + let request = LdapMessage::from(msg); + tx.complete = match request.protocol_op { + ProtocolOp::UnbindRequest => true, + _ => false, + }; + tx.request = Some(request); + self.transactions.push_back(tx); + } + Err(nom::Err::Incomplete(_)) => { + let consumed = input.len() - start.len(); + let needed = start.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + + return AppLayerResult::ok(); + } + + fn parse_response(&mut self, input: &[u8]) -> AppLayerResult { + if input.is_empty() { + return AppLayerResult::ok(); + } + let mut start = input; + while !start.is_empty() { + match ldap_parse_msg(start) { + Ok((rem, msg)) => { + start = rem; + + let response = LdapMessage::from(msg); + if let Some(tx) = self.find_request(response.message_id) { + tx.complete = match response.protocol_op { + ProtocolOp::SearchResultDone(_) + | ProtocolOp::BindResponse(_) + | ProtocolOp::ModifyResponse(_) + | ProtocolOp::AddResponse(_) + | ProtocolOp::DelResponse(_) + | ProtocolOp::ModDnResponse(_) + | ProtocolOp::CompareResponse(_) + | ProtocolOp::ExtendedResponse(_) => true, + _ => false, + }; + tx.responses.push_back(response); + } else if let ProtocolOp::ExtendedResponse(_) = response.protocol_op { + // this is an unsolicited notification, which means + // there is no request + let mut tx = self.new_tx(); + tx.complete = true; + tx.responses.push_back(response); + self.transactions.push_back(tx); + } else { + let mut tx = self.new_tx(); + tx.complete = true; + tx.responses.push_back(response); + self.transactions.push_back(tx); + self.set_event(LdapEvent::RequestNotFound); + }; + } + Err(nom::Err::Incomplete(_)) => { + let consumed = input.len() - start.len(); + let needed = start.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(_) => { + self.set_event(LdapEvent::InvalidData); + return AppLayerResult::err(); + } + } + } + + return AppLayerResult::ok(); + } +} + +fn probe(input: &[u8], direction: Direction, rdir: *mut u8) -> AppProto { + match ldap_parse_msg(input) { + Ok((_, msg)) => { + let ldap_msg = LdapMessage::from(msg); + if ldap_msg.is_unknown() { + return unsafe { ALPROTO_FAILED }; + } + if direction == Direction::ToServer && !ldap_msg.is_request() { + unsafe { + *rdir = Direction::ToClient.into(); + } + } + if direction == Direction::ToClient && !ldap_msg.is_response() { + unsafe { + *rdir = Direction::ToServer.into(); + } + } + return unsafe { ALPROTO_LDAP }; + } + Err(nom::Err::Incomplete(_)) => { + return ALPROTO_UNKNOWN; + } + Err(_e) => { + return unsafe { ALPROTO_FAILED }; + } + } +} + +#[no_mangle] +unsafe extern "C" fn SCLdapProbingParser( + _flow: *const Flow, direction: u8, input: *const u8, input_len: u32, rdir: *mut u8, +) -> AppProto { + if input_len > 1 && !input.is_null() { + let slice = build_slice!(input, input_len as usize); + return probe(slice, direction.into(), rdir); + } + return ALPROTO_UNKNOWN; +} + +#[no_mangle] +extern "C" fn SCLdapStateNew(_orig_state: *mut c_void, _orig_proto: AppProto) -> *mut c_void { + let state = LdapState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +#[no_mangle] +unsafe extern "C" fn SCLdapStateFree(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut LdapState)); +} + +#[no_mangle] +unsafe extern "C" fn SCLdapStateTxFree(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, LdapState); + state.free_tx(tx_id); +} + +#[no_mangle] +unsafe extern "C" fn SCLdapParseRequest( + _flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + if stream_slice.is_empty() { + if AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TS) > 0 { + return AppLayerResult::ok(); + } else { + return AppLayerResult::err(); + } + } + let state = cast_pointer!(state, LdapState); + state.parse_request(stream_slice.as_slice()); + + AppLayerResult::ok() +} + +#[no_mangle] +unsafe extern "C" fn SCLdapParseResponse( + _flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + if stream_slice.is_empty() { + if AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TC) > 0 { + return AppLayerResult::ok(); + } else { + return AppLayerResult::err(); + } + } + let state = cast_pointer!(state, LdapState); + state.parse_response(stream_slice.as_slice()); + + AppLayerResult::ok() +} + +#[no_mangle] +unsafe extern "C" fn SCLdapStateGetTx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, LdapState); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +#[no_mangle] +unsafe extern "C" fn SCLdapStateGetTxCount(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, LdapState); + return state.tx_id; +} + +#[no_mangle] +unsafe extern "C" fn SCLdapTxGetAlstateProgress(tx: *mut c_void, _direction: u8) -> c_int { + let tx = cast_pointer!(tx, LdapTransaction); + if tx.complete { + return 1; + } + return 0; +} + +export_tx_data_get!(SCLdapGetTxData, LdapTransaction); +export_state_data_get!(SCLdapGetStateData, LdapState); + +const PARSER_NAME: &[u8] = b"ldap\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_ldap_register_parser() { + let default_port = CString::new("389").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_TCP, + probe_ts: Some(SCLdapProbingParser), + probe_tc: Some(SCLdapProbingParser), + min_depth: 0, + max_depth: 16, + state_new: SCLdapStateNew, + state_free: SCLdapStateFree, + tx_free: SCLdapStateTxFree, + parse_ts: SCLdapParseRequest, + parse_tc: SCLdapParseResponse, + get_tx_count: SCLdapStateGetTxCount, + get_tx: SCLdapStateGetTx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: SCLdapTxGetAlstateProgress, + get_eventinfo: Some(LdapEvent::get_event_info), + get_eventinfo_byid: Some(LdapEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some(applayer::state_get_tx_iterator::), + get_tx_data: SCLdapGetTxData, + get_state_data: SCLdapGetStateData, + apply_tx_config: None, + flags: APP_LAYER_PARSER_OPT_ACCEPT_GAPS, + get_frame_id_by_name: None, + get_frame_name_by_id: None, + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_LDAP = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + if let Some(val) = conf_get("app-layer.protocols.ldap.max-tx") { + if let Ok(v) = val.parse::() { + LDAP_MAX_TX = v; + } else { + SCLogError!("Invalid value for ldap.max-tx"); + } + } + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_LDAP); + } else { + SCLogDebug!("Protocol detection and parser disabled for LDAP."); + } +} diff --git a/rust/src/ldap/logger.rs b/rust/src/ldap/logger.rs new file mode 100644 index 000000000000..f126f97ef231 --- /dev/null +++ b/rust/src/ldap/logger.rs @@ -0,0 +1,351 @@ +/* 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. + */ + +// written by Giuseppe Longo + +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use crate::ldap::filters::*; +use crate::ldap::ldap::LdapTransaction; +use crate::ldap::types::*; + +fn log_ldap(tx: &LdapTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("ldap")?; + + if let Some(req) = &tx.request { + let protocol_op_str = req.protocol_op.to_string(); + js.open_object("request")?; + js.set_uint("message_id", req.message_id.0.into())?; + js.set_string("operation", &protocol_op_str)?; + + match &req.protocol_op { + ProtocolOp::SearchRequest(msg) => log_search_request(msg, js)?, + ProtocolOp::BindRequest(msg) => log_bind_request(msg, js)?, + ProtocolOp::UnbindRequest => (), + ProtocolOp::ModifyRequest(msg) => log_modify_request(msg, js)?, + ProtocolOp::AddRequest(msg) => log_add_request(msg, js)?, + ProtocolOp::DelRequest(msg) => log_del_request(msg, js)?, + ProtocolOp::ModDnRequest(msg) => log_mod_dn_request(msg, js)?, + ProtocolOp::CompareRequest(msg) => log_compare_request(msg, js)?, + ProtocolOp::ExtendedRequest(msg) => log_extended_request(msg, js)?, + _ => {} + }; + + log_controls(&req.controls, js)?; + + js.close()?; + } + + if !tx.responses.is_empty() { + js.open_array("responses")?; + + for response in &tx.responses { + js.start_object()?; + + let protocol_op_str = response.protocol_op.to_string(); + js.set_string("operation", &protocol_op_str)?; + + if tx.request.is_none() { + js.set_uint("message_id", response.message_id.0.into())?; + } + + match &response.protocol_op { + ProtocolOp::SearchResultEntry(msg) => log_search_result_entry(msg, js)?, + ProtocolOp::SearchResultDone(msg) => log_search_result_done(msg, js)?, + ProtocolOp::BindResponse(msg) => log_bind_response(msg, js)?, + ProtocolOp::ModifyResponse(msg) => log_modify_response(msg, js)?, + ProtocolOp::AddResponse(msg) => log_add_response(msg, js)?, + ProtocolOp::DelResponse(msg) => log_del_response(msg, js)?, + ProtocolOp::ModDnResponse(msg) => log_mod_dn_response(msg, js)?, + ProtocolOp::CompareResponse(msg) => log_compare_response(msg, js)?, + ProtocolOp::ExtendedResponse(msg) => log_extended_response(msg, js)?, + ProtocolOp::IntermediateResponse(msg) => log_intermediate_response(msg, js)?, + _ => {} + } + log_controls(&response.controls, js)?; + js.close()?; + } + js.close()?; + } + + js.close()?; + Ok(()) +} + +fn log_search_request(msg: &SearchRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("search_request")?; + js.set_string("base_object", &msg.base_object.0)?; + js.set_uint("scope", msg.scope.0.into())?; + js.set_uint("deref_alias", msg.deref_aliases.0.into())?; + js.set_uint("size_limit", msg.size_limit.into())?; + js.set_uint("time_limit", msg.time_limit.into())?; + js.set_bool("types_only", msg.types_only)?; + if let Filter::Present(val) = &msg.filter { + js.open_object("filter")?; + js.set_string("type", "present")?; + js.set_string("value", &val.0.to_string())?; + js.close()?; + } + if !msg.attributes.is_empty() { + js.open_array("attributes")?; + for attr in &msg.attributes { + js.append_string(&attr.0)?; + } + js.close()?; + } + + js.close()?; + Ok(()) +} + +fn log_bind_request(msg: &BindRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("bind_request")?; + js.set_uint("version", msg.version.into())?; + js.set_string("name", &msg.name.0)?; + if let AuthenticationChoice::Sasl(sasl) = &msg.authentication { + js.open_object("sasl")?; + js.set_string("mechanism", &sasl.mechanism.0)?; + if let Some(credentials) = &sasl.credentials { + js.set_hex("credentials", credentials)?; + } + js.close()?; + } + js.close()?; + Ok(()) +} + +fn log_modify_request(msg: &ModifyRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("modify_request")?; + js.set_string("object", &msg.object.0)?; + if !msg.changes.is_empty() { + js.open_array("changes")?; + for change in &msg.changes { + js.start_object()?; + js.set_string("operation", &change.operation.to_string())?; + js.open_object("modification")?; + js.set_string("attribute_type", &change.modification.attr_type.0)?; + if !change.modification.attr_vals.is_empty() { + js.open_array("attribute_values")?; + for attr in &change.modification.attr_vals { + js.append_string_from_bytes(&attr.0[..])?; + } + js.close()?; + } + js.close()?; + js.close()?; + } + js.close()?; + } + js.close()?; + Ok(()) +} + +fn log_add_request(msg: &AddRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("add_request")?; + js.set_string("entry", &msg.entry.0)?; + if !msg.attributes.is_empty() { + js.open_array("attributes")?; + for attr in &msg.attributes { + js.start_object()?; + js.set_string("name", &attr.attr_type.0)?; + if !attr.attr_vals.is_empty() { + js.open_array("values")?; + for val in &attr.attr_vals { + js.append_string_from_bytes(&val.0)?; + } + js.close()?; + } + js.close()?; + } + js.close()?; + } + js.close()?; + Ok(()) +} + +fn log_del_request(msg: &LdapDN, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("del_request")?; + js.set_string("dn", &msg.0)?; + js.close()?; + Ok(()) +} + +fn log_mod_dn_request(msg: &ModDnRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("mod_dn_request")?; + js.set_string("entry", &msg.entry.0)?; + js.set_string("new_rdn", &msg.newrdn.0)?; + js.set_bool("delete_old_rdn", msg.deleteoldrdn)?; + if let Some(newsuperior) = &msg.newsuperior { + js.set_string("new_superior", &newsuperior.0)?; + } + js.close()?; + Ok(()) +} + +fn log_compare_request(msg: &CompareRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("compare_request")?; + js.set_string("entry", &msg.entry.0)?; + js.open_object("attribute_value_assertion")?; + js.set_string("description", &msg.ava.attribute_desc.0)?; + js.set_string_from_bytes("value", &msg.ava.assertion_value[..])?; + js.close()?; + js.close()?; + Ok(()) +} + +fn log_extended_request(msg: &ExtendedRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("extended_request")?; + js.set_string("name", &msg.request_name.0)?; + if let Some(value) = &msg.request_value { + js.set_string_from_bytes("value", &value[..])?; + } + js.close()?; + Ok(()) +} + +fn log_search_result_entry(msg: &SearchResultEntry, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("search_result_entry")?; + js.set_string("base_object", &msg.object_name.0)?; + if !msg.attributes.is_empty() { + js.open_array("attributes")?; + for attr in &msg.attributes { + js.start_object()?; + js.set_string("type", &attr.attr_type.0)?; + if !attr.attr_vals.is_empty() { + js.open_array("values")?; + for val in &attr.attr_vals { + js.append_string_from_bytes(&val.0)?; + } + js.close()?; + } + js.close()?; + } + js.close()?; + } + js.close()?; + Ok(()) +} + +fn log_search_result_done(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("search_result_done")?; + log_ldap_result(msg, js)?; + js.close()?; + Ok(()) +} + +fn log_bind_response(msg: &BindResponse, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("bind_response")?; + log_ldap_result(&msg.result, js)?; + if let Some(creds) = &msg.server_sasl_creds { + js.set_hex("server_sasl_creds", creds)?; + }; + js.close()?; + Ok(()) +} + +fn log_modify_response(msg: &ModifyResponse, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("modify_response")?; + log_ldap_result(&msg.result, js)?; + js.close()?; + Ok(()) +} + +fn log_add_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("add_response")?; + log_ldap_result(msg, js)?; + js.close()?; + Ok(()) +} + +fn log_del_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("del_response")?; + log_ldap_result(msg, js)?; + js.close()?; + Ok(()) +} + +fn log_mod_dn_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("mod_dn_response")?; + log_ldap_result(msg, js)?; + js.close()?; + Ok(()) +} + +fn log_compare_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("compare_response")?; + log_ldap_result(msg, js)?; + js.close()?; + Ok(()) +} + +fn log_extended_response(msg: &ExtendedResponse, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("extended_response")?; + log_ldap_result(&msg.result, js)?; + if let Some(name) = &msg.response_name { + js.set_string("name", &name.0)?; + } + if let Some(value) = &msg.response_value { + js.set_string_from_bytes("value", &value[..])?; + } + js.close()?; + Ok(()) +} + +fn log_intermediate_response( + msg: &IntermediateResponse, js: &mut JsonBuilder, +) -> Result<(), JsonError> { + js.open_object("intermediate_response")?; + if let Some(name) = &msg.response_name { + js.set_string("name", &name.0)?; + } + if let Some(value) = &msg.response_value { + js.set_string_from_bytes("value", &value[..])?; + } + js.close()?; + Ok(()) +} + +fn log_ldap_result(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.set_string("result_code", &msg.result_code.to_string())?; + js.set_string("matched_dn", &msg.matched_dn.0)?; + js.set_string("message", &msg.diagnostic_message.0)?; + Ok(()) +} + +fn log_controls(controls: &Option>, js: &mut JsonBuilder) -> Result<(), JsonError> { + if let Some(ctls) = controls { + js.open_array("controls")?; + for ctl in ctls { + js.start_object()?; + js.set_string("control_type", &ctl.control_type.0)?; + js.set_bool("criticality", ctl.criticality)?; + if let Some(ctl_val) = &ctl.control_value { + js.set_hex("control_value", ctl_val)?; + }; + js.close()?; + } + js.close()?; + } + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ldap_logger_log( + tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, LdapTransaction); + log_ldap(tx, js).is_ok() +} diff --git a/rust/src/ldap/mod.rs b/rust/src/ldap/mod.rs new file mode 100644 index 000000000000..2f18058693e6 --- /dev/null +++ b/rust/src/ldap/mod.rs @@ -0,0 +1,23 @@ +/* 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. + */ + +// written by Giuseppe Longo + +pub mod filters; +pub mod ldap; +pub mod logger; +pub mod types; diff --git a/rust/src/ldap/types.rs b/rust/src/ldap/types.rs new file mode 100644 index 000000000000..20898750aa7b --- /dev/null +++ b/rust/src/ldap/types.rs @@ -0,0 +1,645 @@ +/* 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. + */ + +// written by Giuseppe Longo + +use std::convert::From; +use std::fmt; +use std::fmt::{Display, Formatter}; + +use asn1_rs::{FromBer, ParseResult}; +use ldap_parser::error::LdapError; + +use crate::ldap::filters::*; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Operation(pub u32); + +impl Display for Operation { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.0 { + 0 => write!(f, "add"), + 1 => write!(f, "delete"), + 2 => write!(f, "replace"), + _ => write!(f, "{}", self.0), + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +pub struct ResultCode(pub u32); + +impl Display for ResultCode { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.0 { + 0 => write!(f, "success"), + 1 => write!(f, "operations_error"), + 2 => write!(f, "protocol_error"), + 3 => write!(f, "time_limit_exceeded"), + 4 => write!(f, "size_limit_exceeded"), + 5 => write!(f, "compare_false"), + 6 => write!(f, "compare_true"), + 7 => write!(f, "auth_method_not_supported"), + 8 => write!(f, "stronger_auth_required"), + 10 => write!(f, "referral"), + 11 => write!(f, "admin_limit_exceeded"), + 12 => write!(f, "unavailable_critical_extension"), + 13 => write!(f, "confidentiality_required"), + 14 => write!(f, "sasl_bind_in_progress"), + 16 => write!(f, "no_such_attribute"), + 17 => write!(f, "undefined_attribute_type"), + 18 => write!(f, "inappropriate_matching"), + 19 => write!(f, "constraint_violation"), + 20 => write!(f, "attribute_or_value_exists"), + 21 => write!(f, "invalid_attribute_syntax"), + 32 => write!(f, "no_such_object"), + 33 => write!(f, "alias_problem"), + 34 => write!(f, "invalid_dns_syntax"), + 35 => write!(f, "is_leaf"), + 36 => write!(f, "alias_dereferencing_problem"), + 48 => write!(f, "inappropriate_authentication"), + 49 => write!(f, "invalid_credentials"), + 50 => write!(f, "insufficient_access_rights"), + 51 => write!(f, "busy"), + 52 => write!(f, "unavailable"), + 53 => write!(f, "unwilling_to_perform"), + 54 => write!(f, "loop_detect"), + 60 => write!(f, "sort_control_missing"), + 61 => write!(f, "offset_range_error"), + 64 => write!(f, "naming_violation"), + 65 => write!(f, "object_class_violation"), + 66 => write!(f, "not_allowed_on_non_leaf"), + 67 => write!(f, "not_allowed_on_rdn"), + 68 => write!(f, "entry_already_exists"), + 69 => write!(f, "object_class_mods_prohibited"), + 70 => write!(f, "results_too_large"), + 71 => write!(f, "affects_multiple_dsas"), + 76 => write!(f, "control_error"), + 80 => write!(f, "other"), + 81 => write!(f, "server_down"), + 82 => write!(f, "local_error"), + 83 => write!(f, "encoding_error"), + 84 => write!(f, "decoding_error"), + 85 => write!(f, "timeout"), + 86 => write!(f, "auth_unknown"), + 87 => write!(f, "filter_error"), + 88 => write!(f, "user_canceled"), + 89 => write!(f, "param_error"), + 90 => write!(f, "no_memory"), + 91 => write!(f, "connect_error"), + 92 => write!(f, "not_supported"), + 93 => write!(f, "control_not_found"), + 94 => write!(f, "no_results_returned"), + 95 => write!(f, "more_results_to_return"), + 96 => write!(f, "client_loop"), + 97 => write!(f, "referral_limit_exceeded"), + 100 => write!(f, "invalid_response"), + 101 => write!(f, "ambiguous_response"), + 112 => write!(f, "tls_not_supported"), + 113 => write!(f, "intermediate_response"), + 114 => write!(f, "unknown_type"), + 118 => write!(f, "canceled"), + 119 => write!(f, "no_such_operation"), + 120 => write!(f, "too_late"), + 121 => write!(f, "cannot_cancel"), + 122 => write!(f, "assertion_failed"), + 123 => write!(f, "authorization_denied"), + 4096 => write!(f, "e_sync_refresh_required"), + 16654 => write!(f, "no_operation"), + _ => write!(f, "{}", self.0), + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +pub struct MessageID(pub u32); + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct SearchScope(pub u32); + +impl Display for SearchScope { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.0 { + 0 => write!(f, "base_object"), + 1 => write!(f, "single_level"), + 2 => write!(f, "whole_subtree"), + _ => write!(f, "{}", self.0), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct DerefAliases(pub u32); + +impl Display for DerefAliases { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.0 { + 0 => write!(f, "never_deref_aliases"), + 1 => write!(f, "deref_in_searching"), + 2 => write!(f, "deref_finding_base_obj"), + 3 => write!(f, "deref_always"), + _ => write!(f, "{}", self.0), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LdapString(pub String); + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LdapDN(pub String); + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RelativeLdapDN(pub String); + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LdapOID(pub String); + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LdapResult { + pub result_code: ResultCode, + pub matched_dn: LdapDN, + pub diagnostic_message: LdapString, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BindRequest { + pub version: u8, + pub name: LdapDN, + pub authentication: AuthenticationChoice, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SaslCredentials { + pub mechanism: LdapString, + pub credentials: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum AuthenticationChoice { + Simple(Vec), + Sasl(SaslCredentials), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BindResponse { + pub result: LdapResult, + pub server_sasl_creds: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SearchRequest { + pub base_object: LdapDN, + pub scope: SearchScope, + pub deref_aliases: DerefAliases, + pub size_limit: u32, + pub time_limit: u32, + pub types_only: bool, + pub filter: Filter, + pub attributes: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SearchResultEntry { + pub object_name: LdapDN, + pub attributes: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ModifyRequest { + pub object: LdapDN, + pub changes: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ModifyResponse { + pub result: LdapResult, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Change { + pub operation: Operation, + pub modification: PartialAttribute, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AddRequest { + pub entry: LdapDN, + pub attributes: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ModDnRequest { + pub entry: LdapDN, + pub newrdn: RelativeLdapDN, + pub deleteoldrdn: bool, + pub newsuperior: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CompareRequest { + pub entry: LdapDN, + pub ava: AttributeValueAssertion, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExtendedRequest { + pub request_name: LdapOID, + pub request_value: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExtendedResponse { + pub result: LdapResult, + pub response_name: Option, + pub response_value: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IntermediateResponse { + pub response_name: Option, + pub response_value: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ProtocolOp { + BindRequest(BindRequest), + BindResponse(BindResponse), + UnbindRequest, + SearchRequest(SearchRequest), + SearchResultEntry(SearchResultEntry), + SearchResultDone(LdapResult), + SearchResultReference(Vec), + ModifyRequest(ModifyRequest), + ModifyResponse(ModifyResponse), + AddRequest(AddRequest), + AddResponse(LdapResult), + DelRequest(LdapDN), + DelResponse(LdapResult), + ModDnRequest(ModDnRequest), + ModDnResponse(LdapResult), + CompareRequest(CompareRequest), + CompareResponse(LdapResult), + ExtendedRequest(ExtendedRequest), + ExtendedResponse(ExtendedResponse), + IntermediateResponse(IntermediateResponse), + Unknown, +} + +impl Display for ProtocolOp { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ProtocolOp::BindRequest(_) => write!(f, "bind_request"), + ProtocolOp::BindResponse(_) => write!(f, "bind_response"), + ProtocolOp::UnbindRequest => write!(f, "unbind_request"), + ProtocolOp::SearchRequest(_) => write!(f, "search_request"), + ProtocolOp::SearchResultEntry(_) => write!(f, "search_result_entry"), + ProtocolOp::SearchResultDone(_) => write!(f, "search_result_done"), + ProtocolOp::SearchResultReference(_) => write!(f, "search_result_reference"), + ProtocolOp::ModifyRequest(_) => write!(f, "modify_request"), + ProtocolOp::ModifyResponse(_) => write!(f, "modify_response"), + ProtocolOp::AddRequest(_) => write!(f, "add_request"), + ProtocolOp::AddResponse(_) => write!(f, "add_response"), + ProtocolOp::DelRequest(_) => write!(f, "del_request"), + ProtocolOp::DelResponse(_) => write!(f, "del_response"), + ProtocolOp::ModDnRequest(_) => write!(f, "mod_dn_request"), + ProtocolOp::ModDnResponse(_) => write!(f, "mod_dn_response"), + ProtocolOp::CompareRequest(_) => write!(f, "compare_request"), + ProtocolOp::CompareResponse(_) => write!(f, "compare_response"), + ProtocolOp::ExtendedRequest(_) => write!(f, "extended_request"), + ProtocolOp::ExtendedResponse(_) => write!(f, "extended_response"), + ProtocolOp::IntermediateResponse(_) => write!(f, "intermediate_response"), + ProtocolOp::Unknown => write!(f, "unknown"), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LdapMessage { + pub message_id: MessageID, + pub protocol_op: ProtocolOp, + pub controls: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Control { + pub control_type: LdapOID, + pub criticality: bool, + pub control_value: Option>, +} + +impl<'a> From> for LdapMessage { + fn from(ldap_msg: ldap_parser::ldap::LdapMessage) -> Self { + let message_id = MessageID(ldap_msg.message_id.0); + let protocol_op = match ldap_msg.protocol_op { + ldap_parser::ldap::ProtocolOp::BindRequest(msg) => Self::from_bind_request(msg), + ldap_parser::ldap::ProtocolOp::BindResponse(msg) => Self::from_bind_response(msg), + ldap_parser::ldap::ProtocolOp::UnbindRequest => ProtocolOp::UnbindRequest, + ldap_parser::ldap::ProtocolOp::SearchRequest(msg) => Self::from_search_request(msg), + ldap_parser::ldap::ProtocolOp::SearchResultEntry(msg) => { + Self::from_search_result_entry(msg) + } + ldap_parser::ldap::ProtocolOp::SearchResultDone(msg) => { + Self::from_search_result_done(msg) + } + ldap_parser::ldap::ProtocolOp::SearchResultReference(msg) => { + Self::from_search_result_reference(msg) + } + ldap_parser::ldap::ProtocolOp::ModifyRequest(msg) => Self::from_modify_request(msg), + ldap_parser::ldap::ProtocolOp::ModifyResponse(msg) => Self::from_modify_response(msg), + ldap_parser::ldap::ProtocolOp::AddRequest(msg) => Self::from_add_request(msg), + ldap_parser::ldap::ProtocolOp::AddResponse(msg) => Self::from_add_response(msg), + ldap_parser::ldap::ProtocolOp::DelRequest(msg) => Self::from_del_request(msg), + ldap_parser::ldap::ProtocolOp::DelResponse(msg) => Self::from_del_response(msg), + ldap_parser::ldap::ProtocolOp::ModDnRequest(msg) => Self::from_mod_dn_request(msg), + ldap_parser::ldap::ProtocolOp::ModDnResponse(msg) => Self::from_mod_dn_response(msg), + ldap_parser::ldap::ProtocolOp::CompareRequest(msg) => Self::from_compare_request(msg), + ldap_parser::ldap::ProtocolOp::CompareResponse(msg) => Self::from_compare_response(msg), + ldap_parser::ldap::ProtocolOp::ExtendedRequest(msg) => Self::from_extended_request(msg), + ldap_parser::ldap::ProtocolOp::ExtendedResponse(msg) => { + Self::from_extended_response(msg) + } + ldap_parser::ldap::ProtocolOp::IntermediateResponse(msg) => { + Self::from_intermediate_response(msg) + } + _ => ProtocolOp::Unknown, + }; + let controls = ldap_msg.controls.map(|ctls| { + ctls.iter() + .map(|ctl| Control { + control_type: LdapOID(ctl.control_type.0.to_string()), + criticality: ctl.criticality, + control_value: ctl.control_value.as_ref().map(|val| val.to_vec()), + }) + .collect() + }); + + Self { + message_id, + protocol_op, + controls, + } + } +} + +impl LdapMessage { + pub fn is_unknown(&self) -> bool { + match self.protocol_op { + ProtocolOp::Unknown => return true, + _ => return false, + } + } + + pub fn is_request(&self) -> bool { + match self.protocol_op { + ProtocolOp::BindRequest(_) + | ProtocolOp::UnbindRequest + | ProtocolOp::SearchRequest(_) + | ProtocolOp::ModifyRequest(_) + | ProtocolOp::AddRequest(_) + | ProtocolOp::DelRequest(_) + | ProtocolOp::ModDnRequest(_) + | ProtocolOp::CompareRequest(_) + | ProtocolOp::ExtendedRequest(_) => { + return true; + } + _ => { + return false; + } + } + } + + pub fn is_response(&self) -> bool { + match self.protocol_op { + ProtocolOp::BindResponse(_) + | ProtocolOp::SearchResultEntry(_) + | ProtocolOp::SearchResultReference(_) + | ProtocolOp::SearchResultDone(_) + | ProtocolOp::ModifyResponse(_) + | ProtocolOp::AddResponse(_) + | ProtocolOp::DelResponse(_) + | ProtocolOp::ModDnResponse(_) + | ProtocolOp::CompareResponse(_) + | ProtocolOp::ExtendedResponse(_) => { + return true; + } + _ => { + return false; + } + } + } + + fn from_bind_request(msg: ldap_parser::ldap::BindRequest) -> ProtocolOp { + let authentication = match msg.authentication { + ldap_parser::ldap::AuthenticationChoice::Simple(val) => { + AuthenticationChoice::Simple(val.to_vec()) + } + ldap_parser::ldap::AuthenticationChoice::Sasl(val) => { + AuthenticationChoice::Sasl(SaslCredentials { + mechanism: LdapString(val.mechanism.0.to_string()), + credentials: val.credentials.map(|creds| creds.to_vec()), + }) + } + }; + ProtocolOp::BindRequest(BindRequest { + version: msg.version, + name: LdapDN(msg.name.0.to_string()), + authentication, + }) + } + + fn from_bind_response(msg: ldap_parser::ldap::BindResponse) -> ProtocolOp { + ProtocolOp::BindResponse(BindResponse { + result: LdapResult { + result_code: ResultCode(msg.result.result_code.0), + matched_dn: LdapDN(msg.result.matched_dn.0.to_string()), + diagnostic_message: LdapString(msg.result.diagnostic_message.0.to_string()), + }, + server_sasl_creds: msg + .server_sasl_creds + .map(|server_sasl_creds| server_sasl_creds.to_vec()), + }) + } + + fn from_search_request(msg: ldap_parser::ldap::SearchRequest) -> ProtocolOp { + let attributes = msg + .attributes + .iter() + .map(|s| LdapString(s.0.to_string())) + .collect(); + ProtocolOp::SearchRequest(SearchRequest { + base_object: LdapDN(msg.base_object.0.to_string()), + scope: SearchScope(msg.scope.0), + deref_aliases: DerefAliases(msg.deref_aliases.0), + size_limit: msg.size_limit, + time_limit: msg.time_limit, + types_only: msg.types_only, + filter: Filter::from(msg.filter), + attributes, + }) + } + + fn from_search_result_entry(msg: ldap_parser::ldap::SearchResultEntry) -> ProtocolOp { + let attributes = msg.attributes.iter().map(PartialAttribute::from).collect(); + ProtocolOp::SearchResultEntry(SearchResultEntry { + object_name: LdapDN(msg.object_name.0.to_string()), + attributes, + }) + } + + fn from_search_result_done(msg: ldap_parser::ldap::LdapResult) -> ProtocolOp { + ProtocolOp::SearchResultDone(LdapResult { + result_code: ResultCode(msg.result_code.0), + matched_dn: LdapDN(msg.matched_dn.0.to_string()), + diagnostic_message: LdapString(msg.diagnostic_message.0.to_string()), + }) + } + + fn from_search_result_reference(msg: Vec>) -> ProtocolOp { + let strs = msg.iter().map(|s| LdapString(s.0.to_string())).collect(); + ProtocolOp::SearchResultReference(strs) + } + + fn from_modify_request(msg: ldap_parser::ldap::ModifyRequest) -> ProtocolOp { + let changes = msg + .changes + .iter() + .map(|c| Change { + operation: Operation(c.operation.0), + modification: PartialAttribute::from(&c.modification), + }) + .collect(); + ProtocolOp::ModifyRequest(ModifyRequest { + object: LdapDN(msg.object.0.to_string()), + changes, + }) + } + + fn from_modify_response(msg: ldap_parser::ldap::ModifyResponse) -> ProtocolOp { + ProtocolOp::ModifyResponse(ModifyResponse { + result: LdapResult { + result_code: ResultCode(msg.result.result_code.0), + matched_dn: LdapDN(msg.result.matched_dn.0.to_string()), + diagnostic_message: LdapString(msg.result.diagnostic_message.0.to_string()), + }, + }) + } + + fn from_add_request(msg: ldap_parser::ldap::AddRequest) -> ProtocolOp { + let attributes = msg.attributes.iter().map(Attribute::from).collect(); + ProtocolOp::AddRequest(AddRequest { + entry: LdapDN(msg.entry.0.to_string()), + attributes, + }) + } + + fn from_add_response(msg: ldap_parser::ldap::LdapResult) -> ProtocolOp { + ProtocolOp::AddResponse(LdapResult { + result_code: ResultCode(msg.result_code.0), + matched_dn: LdapDN(msg.matched_dn.0.to_string()), + diagnostic_message: LdapString(msg.diagnostic_message.0.to_string()), + }) + } + + fn from_del_request(msg: ldap_parser::ldap::LdapDN<'_>) -> ProtocolOp { + ProtocolOp::DelRequest(LdapDN(msg.0.to_string())) + } + + fn from_del_response(msg: ldap_parser::ldap::LdapResult) -> ProtocolOp { + ProtocolOp::DelResponse(LdapResult { + result_code: ResultCode(msg.result_code.0), + matched_dn: LdapDN(msg.matched_dn.0.to_string()), + diagnostic_message: LdapString(msg.diagnostic_message.0.to_string()), + }) + } + + fn from_mod_dn_request(msg: ldap_parser::ldap::ModDnRequest) -> ProtocolOp { + ProtocolOp::ModDnRequest(ModDnRequest { + entry: LdapDN(msg.entry.0.to_string()), + newrdn: RelativeLdapDN(msg.newrdn.0.to_string()), + deleteoldrdn: msg.deleteoldrdn, + newsuperior: if let Some(newsuperior) = msg.newsuperior { + Some(LdapDN(newsuperior.0.to_string())) + } else { + None + }, + }) + } + + fn from_mod_dn_response(msg: ldap_parser::ldap::LdapResult) -> ProtocolOp { + ProtocolOp::ModDnResponse(LdapResult { + result_code: ResultCode(msg.result_code.0), + matched_dn: LdapDN(msg.matched_dn.0.to_string()), + diagnostic_message: LdapString(msg.diagnostic_message.0.to_string()), + }) + } + + fn from_compare_request(msg: ldap_parser::ldap::CompareRequest) -> ProtocolOp { + ProtocolOp::CompareRequest(CompareRequest { + entry: LdapDN(msg.entry.0.to_string()), + ava: AttributeValueAssertion::from(&msg.ava), + }) + } + + fn from_compare_response(msg: ldap_parser::ldap::LdapResult) -> ProtocolOp { + ProtocolOp::CompareResponse(LdapResult { + result_code: ResultCode(msg.result_code.0), + matched_dn: LdapDN(msg.matched_dn.0.to_string()), + diagnostic_message: LdapString(msg.diagnostic_message.0.to_string()), + }) + } + + fn from_extended_request(msg: ldap_parser::ldap::ExtendedRequest) -> ProtocolOp { + ProtocolOp::ExtendedRequest(ExtendedRequest { + request_name: LdapOID(msg.request_name.0.to_string()), + request_value: msg + .request_value + .map(|request_value| request_value.to_vec()), + }) + } + + fn from_extended_response(msg: ldap_parser::ldap::ExtendedResponse) -> ProtocolOp { + ProtocolOp::ExtendedResponse(ExtendedResponse { + result: LdapResult { + result_code: ResultCode(msg.result.result_code.0), + matched_dn: LdapDN(msg.result.matched_dn.0.to_string()), + diagnostic_message: LdapString(msg.result.diagnostic_message.0.to_string()), + }, + response_name: msg + .response_name + .map(|response_name| LdapOID(response_name.0.to_string())), + response_value: msg + .response_value + .map(|response_value| response_value.to_vec()), + }) + } + + fn from_intermediate_response(msg: ldap_parser::ldap::IntermediateResponse) -> ProtocolOp { + ProtocolOp::IntermediateResponse(IntermediateResponse { + response_name: msg + .response_name + .map(|response_name| LdapOID(response_name.0.to_string())), + response_value: msg + .response_value + .map(|response_value| response_value.to_vec()), + }) + } +} + +pub fn ldap_parse_msg(input: &[u8]) -> ParseResult { + ldap_parser::ldap::LdapMessage::from_ber(input) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 6a9870e53014..0f8f34aa7dc5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -62,6 +62,7 @@ extern crate der_parser; extern crate kerberos_parser; extern crate tls_parser; extern crate x509_parser; +extern crate ldap_parser; #[macro_use] extern crate suricata_derive; @@ -124,6 +125,7 @@ pub mod util; pub mod ffi; pub mod feature; pub mod sdp; +pub mod ldap; #[allow(unused_imports)] pub use suricata_lua_sys; diff --git a/rust/src/util.rs b/rust/src/util.rs index d7109464f773..d3f76e41eb7c 100644 --- a/rust/src/util.rs +++ b/rust/src/util.rs @@ -20,7 +20,68 @@ use std::ffi::CStr; use std::os::raw::c_char; +use nom7::bytes::complete::take_while1; +use nom7::character::complete::char; +use nom7::character::{is_alphabetic, is_alphanumeric}; +use nom7::combinator::verify; +use nom7::multi::many1_count; +use nom7::IResult; + #[no_mangle] pub unsafe extern "C" fn rs_check_utf8(val: *const c_char) -> bool { CStr::from_ptr(val).to_str().is_ok() } + +fn is_alphanumeric_or_hyphen(chr: u8) -> bool { + return is_alphanumeric(chr) || chr == b'-'; +} + +fn parse_domain_label(i: &[u8]) -> IResult<&[u8], ()> { + let (i, _) = verify(take_while1(is_alphanumeric_or_hyphen), |x: &[u8]| { + is_alphabetic(x[0]) && x[x.len() - 1] != b'-' + })(i)?; + return Ok((i, ())); +} + +fn parse_subdomain(input: &[u8]) -> IResult<&[u8], ()> { + let (input, _) = parse_domain_label(input)?; + let (input, _) = char('.')(input)?; + return Ok((input, ())); +} + +fn parse_domain(input: &[u8]) -> IResult<&[u8], ()> { + let (input, _) = many1_count(parse_subdomain)(input)?; + let (input, _) = parse_domain_label(input)?; + return Ok((input, ())); +} + +#[no_mangle] +pub unsafe extern "C" fn SCValidateDomain(input: *const u8, in_len: u32) -> u32 { + let islice = build_slice!(input, in_len as usize); + if let Ok((rem, _)) = parse_domain(islice) { + return (islice.len() - rem.len()) as u32; + } + return 0; +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_parse_domain() { + let buf0: &[u8] = "a-1.oisf.net more".as_bytes(); + let (rem, _) = parse_domain(buf0).unwrap(); + // And we should have 5 bytes left. + assert_eq!(rem.len(), 5); + let buf1: &[u8] = "justatext".as_bytes(); + assert!(parse_domain(buf1).is_err()); + let buf1: &[u8] = "1.com".as_bytes(); + assert!(parse_domain(buf1).is_err()); + let buf1: &[u8] = "a-.com".as_bytes(); + assert!(parse_domain(buf1).is_err()); + let buf1: &[u8] = "a(x)y.com".as_bytes(); + assert!(parse_domain(buf1).is_err()); + } +} diff --git a/src/Makefile.am b/src/Makefile.am index 73dff9563e4c..fc6d6f177357 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -43,6 +43,7 @@ noinst_HEADERS = \ app-layer-ssh.h \ app-layer-ssl.h \ app-layer-tftp.h \ + app-layer-imap.h \ build-info.h \ conf.h \ conf-yaml-loader.h \ @@ -627,6 +628,7 @@ libsuricata_c_a_SOURCES = \ app-layer-ssh.c \ app-layer-ssl.c \ app-layer-tftp.c \ + app-layer-imap.c \ conf.c \ conf-yaml-loader.c \ counters.c \ diff --git a/src/app-layer-ftp.c b/src/app-layer-ftp.c index fc64d1eb20fb..a1a99d4bd701 100644 --- a/src/app-layer-ftp.c +++ b/src/app-layer-ftp.c @@ -965,6 +965,31 @@ static AppProto FTPUserProbingParser( return ALPROTO_FTP; } +static AppProto FTPServerProbingParser( + Flow *f, uint8_t direction, const uint8_t *input, uint32_t len, uint8_t *rdir) +{ + // another check for minimum length + if (len < 5) { + return ALPROTO_UNKNOWN; + } + // begins by 220 + if (input[0] != '2' || input[1] != '2' || input[2] != '0') { + return ALPROTO_FAILED; + } + // followed by space or hypen + if (input[3] != ' ' && input[3] != '-') { + return ALPROTO_FAILED; + } + if (f->alproto_ts == ALPROTO_FTP || (f->todstbytecnt > 4 && f->alproto_ts == ALPROTO_UNKNOWN)) { + // only validates FTP if client side was FTP + // or if client side is unknown despite having received bytes + if (memchr(input + 4, '\n', len - 4) != NULL) { + return ALPROTO_FTP; + } + } + return ALPROTO_UNKNOWN; +} + static int FTPRegisterPatternsForProtocolDetection(void) { if (AppLayerProtoDetectPMRegisterPatternCI( @@ -987,7 +1012,15 @@ static int FTPRegisterPatternsForProtocolDetection(void) IPPROTO_TCP, ALPROTO_FTP, "PORT ", 5, 0, STREAM_TOSERVER) < 0) { return -1; } - + // Only check FTP on known ports as the banner has nothing special beyond + // the response code shared with SMTP. + if (!AppLayerProtoDetectPPParseConfPorts( + "tcp", IPPROTO_TCP, "ftp", ALPROTO_FTP, 0, 5, NULL, FTPServerProbingParser)) { + // STREAM_TOSERVER here means use 21 as flow destination port + // and NULL, FTPServerProbingParser means use probing parser to client + AppLayerProtoDetectPPRegister(IPPROTO_TCP, "21", ALPROTO_FTP, 0, 5, STREAM_TOSERVER, NULL, + FTPServerProbingParser); + } return 0; } diff --git a/src/app-layer-imap.c b/src/app-layer-imap.c new file mode 100644 index 000000000000..689a98a4925f --- /dev/null +++ b/src/app-layer-imap.c @@ -0,0 +1,96 @@ +/* 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. + */ + +/** + * \file + * + * \author Mahmoud Maatuq + * + */ + +#include "app-layer.h" +#include "app-layer-detect-proto.h" +#include "app-layer-imap.h" + +static int IMAPRegisterPatternsForProtocolDetection(void) +{ + if (AppLayerProtoDetectPMRegisterPatternCI( + IPPROTO_TCP, ALPROTO_IMAP, "* OK ", 5, 0, STREAM_TOCLIENT) < 0) { + return -1; + } + + if (AppLayerProtoDetectPMRegisterPatternCI( + IPPROTO_TCP, ALPROTO_IMAP, "* NO ", 5, 0, STREAM_TOCLIENT) < 0) { + return -1; + } + + if (AppLayerProtoDetectPMRegisterPatternCI( + IPPROTO_TCP, ALPROTO_IMAP, "* BAD ", 6, 0, STREAM_TOCLIENT) < 0) { + return -1; + } + + if (AppLayerProtoDetectPMRegisterPatternCI( + IPPROTO_TCP, ALPROTO_IMAP, "* LIST ", 7, 0, STREAM_TOCLIENT) < 0) { + return -1; + } + + if (AppLayerProtoDetectPMRegisterPatternCI( + IPPROTO_TCP, ALPROTO_IMAP, "* ESEARCH ", 10, 0, STREAM_TOCLIENT) < 0) { + return -1; + } + + if (AppLayerProtoDetectPMRegisterPatternCI( + IPPROTO_TCP, ALPROTO_IMAP, "* STATUS ", 9, 0, STREAM_TOCLIENT) < 0) { + return -1; + } + + if (AppLayerProtoDetectPMRegisterPatternCI( + IPPROTO_TCP, ALPROTO_IMAP, "* FLAGS ", 8, 0, STREAM_TOCLIENT) < 0) { + return -1; + } + + /** + * there is no official document that limits the length of the tag + * some practical implementations limit it to 20 characters + * but keeping depth equal to 31 fails unit tests such AppLayerTest10 + * so keeping depth 17 for now to pass unit tests, that might miss some detections + * until we find a better solution for the unit tests. + * + * AppLayerTest10 fails because it expects protocol detection to be completed with only 17 bytes + * as input, and with this new pattern, we would need more bytes to finish protocol detection. + */ + if (AppLayerProtoDetectPMRegisterPatternCI(IPPROTO_TCP, ALPROTO_IMAP, " CAPABILITY", + 17 /*6 for max tag len + space + len(CAPABILITY)*/, 0, STREAM_TOSERVER) < 0) { + return -1; + } + + return 0; +} + +void RegisterIMAPParsers(void) +{ + const char *proto_name = "imap"; + + if (AppLayerProtoDetectConfProtoDetectionEnabled("tcp", proto_name)) { + SCLogDebug("IMAP protocol detection is enabled."); + AppLayerProtoDetectRegisterProtocol(ALPROTO_IMAP, proto_name); + if (IMAPRegisterPatternsForProtocolDetection() < 0) + SCLogError("Failed to register IMAP protocol detection patterns."); + } else { + SCLogDebug("Protocol detector and parser disabled for IMAP."); + } +} diff --git a/src/app-layer-imap.h b/src/app-layer-imap.h new file mode 100644 index 000000000000..497992f71aa3 --- /dev/null +++ b/src/app-layer-imap.h @@ -0,0 +1,28 @@ +/* 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. + */ + +/** + * \file + * + * \author Mahmoud Maatuq + * + */ + +#ifndef SURICATA_APP_LAYER_IMAP_H +#define SURICATA_APP_LAYER_IMAP_H +void RegisterIMAPParsers(void); +#endif diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index cbd42c9015ee..27eb654a01e9 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -54,6 +54,7 @@ #include "app-layer-ike.h" #include "app-layer-rfb.h" #include "app-layer-http2.h" +#include "app-layer-imap.h" struct AppLayerParserThreadCtx_ { void *alproto_local_storage[FLOW_PROTO_MAX][ALPROTO_MAX]; @@ -1723,6 +1724,7 @@ void AppLayerParserRegisterProtocolParsers(void) rs_sip_register_parser(); rs_quic_register_parser(); rs_websocket_register_parser(); + rs_ldap_register_parser(); rs_template_register_parser(); RegisterRFBParsers(); SCMqttRegisterParser(); @@ -1730,19 +1732,7 @@ void AppLayerParserRegisterProtocolParsers(void) rs_rdp_register_parser(); RegisterHTTP2Parsers(); rs_telnet_register_parser(); - - /** IMAP */ - AppLayerProtoDetectRegisterProtocol(ALPROTO_IMAP, "imap"); - if (AppLayerProtoDetectConfProtoDetectionEnabled("tcp", "imap")) { - if (AppLayerProtoDetectPMRegisterPatternCS(IPPROTO_TCP, ALPROTO_IMAP, - "1|20|capability", 12, 0, STREAM_TOSERVER) < 0) - { - FatalError("imap proto registration failure"); - } - } else { - SCLogInfo("Protocol detection and parser disabled for %s protocol.", - "imap"); - } + RegisterIMAPParsers(); /** POP3 */ AppLayerProtoDetectRegisterProtocol(ALPROTO_POP3, "pop3"); diff --git a/src/app-layer-protos.c b/src/app-layer-protos.c index babe6ea83adf..a13e3e8d5d4f 100644 --- a/src/app-layer-protos.c +++ b/src/app-layer-protos.c @@ -61,6 +61,7 @@ const AppProtoStringTuple AppProtoStrings[ALPROTO_MAX] = { { ALPROTO_PGSQL, "pgsql" }, { ALPROTO_TELNET, "telnet" }, { ALPROTO_WEBSOCKET, "websocket" }, + { ALPROTO_LDAP, "ldap" }, { ALPROTO_TEMPLATE, "template" }, { ALPROTO_RDP, "rdp" }, { ALPROTO_HTTP2, "http2" }, diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index 00a5a54811e8..b14f475d8534 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -57,6 +57,7 @@ enum AppProtoEnum { ALPROTO_PGSQL, ALPROTO_TELNET, ALPROTO_WEBSOCKET, + ALPROTO_LDAP, ALPROTO_TEMPLATE, ALPROTO_RDP, ALPROTO_HTTP2, diff --git a/src/app-layer-smtp.c b/src/app-layer-smtp.c index c03ff75cd070..7000b2ec0df6 100644 --- a/src/app-layer-smtp.c +++ b/src/app-layer-smtp.c @@ -215,10 +215,14 @@ enum SMTPCode { SMTP_REPLY_334, SMTP_REPLY_354, + SMTP_REPLY_401, // Unauthorized + SMTP_REPLY_402, // Command not implemented SMTP_REPLY_421, + SMTP_REPLY_435, // Your account has not yet been verified SMTP_REPLY_450, SMTP_REPLY_451, SMTP_REPLY_452, + SMTP_REPLY_454, // Temporary authentication failure SMTP_REPLY_455, SMTP_REPLY_500, @@ -226,6 +230,15 @@ enum SMTPCode { SMTP_REPLY_502, SMTP_REPLY_503, SMTP_REPLY_504, + SMTP_REPLY_511, // Bad email address + SMTP_REPLY_521, // Server does not accept mail + SMTP_REPLY_522, // Recipient has exceeded mailbox limit + SMTP_REPLY_525, // User Account Disabled + SMTP_REPLY_530, // Authentication required + SMTP_REPLY_534, // Authentication mechanism is too weak + SMTP_REPLY_535, // Authentication credentials invalid + SMTP_REPLY_541, // No response from host + SMTP_REPLY_543, // Routing server failure. No available route SMTP_REPLY_550, SMTP_REPLY_551, SMTP_REPLY_552, @@ -234,7 +247,7 @@ enum SMTPCode { SMTP_REPLY_555, }; -SCEnumCharMap smtp_reply_map[ ] = { +SCEnumCharMap smtp_reply_map[] = { { "211", SMTP_REPLY_211 }, { "214", SMTP_REPLY_214 }, { "220", SMTP_REPLY_220 }, @@ -247,10 +260,15 @@ SCEnumCharMap smtp_reply_map[ ] = { { "334", SMTP_REPLY_334 }, { "354", SMTP_REPLY_354 }, + { "401", SMTP_REPLY_401 }, + { "402", SMTP_REPLY_402 }, { "421", SMTP_REPLY_421 }, + { "435", SMTP_REPLY_435 }, { "450", SMTP_REPLY_450 }, { "451", SMTP_REPLY_451 }, { "452", SMTP_REPLY_452 }, + { "454", SMTP_REPLY_454 }, + // { "4.7.0", SMTP_REPLY_454 }, // rfc4954 { "455", SMTP_REPLY_455 }, { "500", SMTP_REPLY_500 }, @@ -258,13 +276,22 @@ SCEnumCharMap smtp_reply_map[ ] = { { "502", SMTP_REPLY_502 }, { "503", SMTP_REPLY_503 }, { "504", SMTP_REPLY_504 }, + { "511", SMTP_REPLY_511 }, + { "521", SMTP_REPLY_521 }, + { "522", SMTP_REPLY_522 }, + { "525", SMTP_REPLY_525 }, + { "530", SMTP_REPLY_530 }, + { "534", SMTP_REPLY_534 }, + { "535", SMTP_REPLY_535 }, + { "541", SMTP_REPLY_541 }, + { "543", SMTP_REPLY_543 }, { "550", SMTP_REPLY_550 }, { "551", SMTP_REPLY_551 }, { "552", SMTP_REPLY_552 }, { "553", SMTP_REPLY_553 }, { "554", SMTP_REPLY_554 }, { "555", SMTP_REPLY_555 }, - { NULL, -1 }, + { NULL, -1 }, }; /* Create SMTP config structure */ @@ -1368,7 +1395,7 @@ static AppLayerResult SMTPParse(uint8_t direction, Flow *f, SMTPState *state, AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TC)))) { SCReturnStruct(APP_LAYER_OK); } else if (input_buf == NULL || input_len == 0) { - SCReturnStruct(APP_LAYER_ERROR); + SCReturnStruct(APP_LAYER_OK); } SMTPInput input = { .buf = input_buf, .len = input_len, .orig_len = input_len, .consumed = 0 }; @@ -1657,6 +1684,46 @@ static int SMTPStateGetEventInfoById(int event_id, const char **event_name, return 0; } +static AppProto SMTPServerProbingParser( + Flow *f, uint8_t direction, const uint8_t *input, uint32_t len, uint8_t *rdir) +{ + // another check for minimum length + if (len < 5) { + return ALPROTO_UNKNOWN; + } + // begins by 220 + if (input[0] != '2' || input[1] != '2' || input[2] != '0') { + return ALPROTO_FAILED; + } + // followed by space or hypen + if (input[3] != ' ' && input[3] != '-') { + return ALPROTO_FAILED; + } + // If client side is SMTP, do not validate domain + // so that server banner can be parsed first. + if (f->alproto_ts == ALPROTO_SMTP) { + if (memchr(input + 4, '\n', len - 4) != NULL) { + return ALPROTO_SMTP; + } + return ALPROTO_UNKNOWN; + } + AppProto r = ALPROTO_UNKNOWN; + if (f->todstbytecnt > 4 && f->alproto_ts == ALPROTO_UNKNOWN) { + // Only validates SMTP if client side is unknown + // despite having received bytes. + r = ALPROTO_SMTP; + } + uint32_t offset = SCValidateDomain(input + 4, len - 4); + if (offset == 0) { + return ALPROTO_FAILED; + } + if (r != ALPROTO_UNKNOWN && memchr(input + 4, '\n', len - 4) != NULL) { + return r; + } + // This should not go forever because of engine limiting probing parsers. + return ALPROTO_UNKNOWN; +} + static int SMTPRegisterPatternsForProtocolDetection(void) { if (AppLayerProtoDetectPMRegisterPatternCI(IPPROTO_TCP, ALPROTO_SMTP, @@ -1674,6 +1741,12 @@ static int SMTPRegisterPatternsForProtocolDetection(void) { return -1; } + if (!AppLayerProtoDetectPPParseConfPorts( + "tcp", IPPROTO_TCP, "smtp", ALPROTO_SMTP, 0, 5, NULL, SMTPServerProbingParser)) { + // STREAM_TOSERVER means here use 25 as flow destination port + AppLayerProtoDetectPPRegister(IPPROTO_TCP, "25,465", ALPROTO_SMTP, 0, 5, STREAM_TOSERVER, + NULL, SMTPServerProbingParser); + } return 0; } diff --git a/src/output.c b/src/output.c index 7b13913c831b..49b2c84ebffb 100644 --- a/src/output.c +++ b/src/output.c @@ -1092,6 +1092,10 @@ void OutputRegisterLoggers(void) OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonEnipLog", "eve-log.enip", OutputJsonLogInitSub, ALPROTO_ENIP, JsonGenericDirFlowLogger, JsonLogThreadInit, JsonLogThreadDeinit, NULL); + /* Ldap JSON logger. */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonLdapLog", "eve-log.ldap", + OutputJsonLogInitSub, ALPROTO_LDAP, JsonGenericDirPacketLogger, JsonLogThreadInit, + JsonLogThreadDeinit, NULL); /* Template JSON logger. */ OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonTemplateLog", "eve-log.template", OutputJsonLogInitSub, ALPROTO_TEMPLATE, JsonGenericDirPacketLogger, JsonLogThreadInit, @@ -1147,6 +1151,7 @@ static EveJsonSimpleAppLayerLogger simple_json_applayer_loggers[ALPROTO_MAX] = { { ALPROTO_PGSQL, JsonPgsqlAddMetadata }, { ALPROTO_TELNET, NULL }, // no logging { ALPROTO_WEBSOCKET, rs_websocket_logger_log }, + { ALPROTO_LDAP, rs_ldap_logger_log }, { ALPROTO_TEMPLATE, rs_template_logger_log }, { ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json }, { ALPROTO_HTTP2, rs_http2_log_json }, diff --git a/suricata.yaml.in b/suricata.yaml.in index de46b1f25362..e753f42e6d26 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -300,6 +300,7 @@ outputs: - rfb - sip - quic + - ldap - arp: enabled: no # Many events can be logged. Disabled by default - dhcp: @@ -1183,6 +1184,11 @@ app-layer: sip: #enabled: yes + ldap: + enabled: yes + # Maximum number of live LDAP transactions per flow + # max-tx: 1024 + # Limit for the maximum number of asn1 frames to decode (default 256) asn1-max-frames: 256