diff --git a/doc/userguide/rules/index.rst b/doc/userguide/rules/index.rst index c8b586fecaa7..3f3b93156a40 100644 --- a/doc/userguide/rules/index.rst +++ b/doc/userguide/rules/index.rst @@ -49,3 +49,4 @@ Suricata Rules differences-from-snort multi-buffer-matching tag + ldap-keywords diff --git a/doc/userguide/rules/ldap-keywords.rst b/doc/userguide/rules/ldap-keywords.rst new file mode 100644 index 000000000000..c3054819f82d --- /dev/null +++ b/doc/userguide/rules/ldap-keywords.rst @@ -0,0 +1,108 @@ +LDAP Keywords +============= + +.. role:: example-rule-action +.. role:: example-rule-header +.. role:: example-rule-options +.. role:: example-rule-emphasis + +LDAP Request and Response operations +------------------------------------ + +.. table:: **Operation values for ldap.request.operation and ldap.response.operation keywords** + + ==== ================================================ + Code Operation + ==== ================================================ + 0 bind_request + 1 bind_response + 2 unbind_request + 3 search_request + 4 search_result_entry + 5 search_result_done + 19 search_result_reference + 6 modify_request + 7 modify_response + 8 add_request + 9 add_response + 10 del_request + 11 del_response + 12 mod_dn_request + 13 mod_dn_response + 14 compare_request + 15 compare_response + 16 abandon_request + 23 extended_request + 24 extended_response + 25 intermediate_response + ==== ================================================ + +ldap.request.operation +~~~~~~~~~~~~~~~~~~~~~~ + +Suricata has a ``ldap.request.operation`` keyword that can be used in signatures to identify +and filter network packets based on Lightweight Directory Access Protocol request operations. + +Syntax:: + + ldap.request.operation: operation; + +ldap.request.operation uses :ref:`unsigned 8-bit integer `. + +Example +^^^^^^^^ + +Example of a signature that would alert if the packet has an LDAP bind request operation: + +.. container:: example-rule + + alert tcp any any -> any any (msg:"Test LDAP bind request"; :example-rule-emphasis:`ldap.request.operation:0;` sid:1;) + + +ldap.responses.operation +~~~~~~~~~~~~~~~~~~~~~~~~ + +Suricata has a ``ldap.responses.operation`` keyword that can be used in signatures to identify +and filter network packets based on Lightweight Directory Access Protocol response operations. + +Syntax:: + + ldap.responses.operation: operation[,index]; + +ldap.responses.operation uses :ref:`unsigned 8-bit integer `. + +An LDAP request operation can receive multiple responses. By default, the ldap.responses.operation +keyword matches all indices, but it is possible to specify a particular index for matching +and also use flags such as ``all`` and ``any``. + +.. table:: **Index values for ldap.responses.operation keyword** + + ========= ================================================ + Value Description + ========= ================================================ + [default] Match all indexes + all Match only if all indexes match + any Match all indexes + 0>= Match specific index + ========= ================================================ + +Examples +^^^^^^^^ + +Example of a signature that would alert if the packet has an LDAP bind response operation: + +.. container:: example-rule + + alert tcp any any -> any any (msg:"Test LDAP bind response"; :example-rule-emphasis:`ldap.responses.operation:1;` sid:1;) + +Example of a signature that would alert if the packet has an LDAP search_result_done response operation at index 1: + +.. container:: example-rule + + alert tcp any any -> any any (msg:"Test LDAP search response"; :example-rule-emphasis:`ldap.responses.operation:search_result_done,1;` sid:1;) + +Example of a signature that would alert if all the responses are of type search_result_entry: + +.. container:: example-rule + + alert tcp any any -> any any (msg:"Test LDAP search response"; :example-rule-emphasis:`ldap.responses.operation:search_result_entry,all;` sid:1;) \ No newline at end of file diff --git a/rust/src/ldap/detect.rs b/rust/src/ldap/detect.rs new file mode 100644 index 000000000000..b2bfd9928eae --- /dev/null +++ b/rust/src/ldap/detect.rs @@ -0,0 +1,244 @@ +/* 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::ldap::{LdapTransaction, ALPROTO_LDAP}; +use crate::detect::uint::{ + detect_parse_uint_enum, rs_detect_u8_free, rs_detect_u8_match, DetectUintData, +}; +use crate::detect::{ + DetectHelperBufferRegister, DetectHelperKeywordRegister, DetectSignatureSetAppProto, + SCSigTableElmt, SigMatchAppendSMToList, +}; +use crate::ldap::types::{LdapMessage, ProtocolOpCode}; + +use std::ffi::CStr; +use std::os::raw::{c_int, c_void}; +use std::str::FromStr; + +pub const DETECT_LDAP_RESP_ANY: i8 = -1; +pub const DETECT_LDAP_RESP_ALL: i8 = -2; + +#[derive(Debug, PartialEq)] +pub struct DetectLdapRespData { + pub response: DetectUintData, //ldap response code + pub index: i8, +} + +static mut G_LDAP_REQUEST_OPERATION_KW_ID: c_int = 0; +static mut G_LDAP_REQUEST_OPERATION_BUFFER_ID: c_int = 0; +static mut G_LDAP_RESPONSES_OPERATION_KW_ID: c_int = 0; +static mut G_LDAP_RESPONSES_OPERATION_BUFFER_ID: c_int = 0; + +unsafe extern "C" fn ldap_parse_protocol_req_op( + ustr: *const std::os::raw::c_char, +) -> *mut DetectUintData { + let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe + if let Ok(s) = ft_name.to_str() { + if let Some(ctx) = detect_parse_uint_enum::(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +pub fn aux_ldap_parse_protocol_resp_op(s: &str) -> Option { + let parts: Vec<&str> = s.split(',').collect(); + if parts.len() > 2 { + return None; + } + let index = if parts.len() == 2 { + if parts[1] == "all" { + DETECT_LDAP_RESP_ALL + } else if parts[1] == "any" { + DETECT_LDAP_RESP_ANY + } else { + let u8_index = i8::from_str(parts[1]).ok()?; + if u8_index < 0 { + return None; + } + u8_index + } + } else { + DETECT_LDAP_RESP_ANY + }; + if let Some(ctx) = detect_parse_uint_enum::(parts[0]) { + let response = ctx; + return Some(DetectLdapRespData { response, index }); + } + return None; +} + +unsafe extern "C" fn ldap_parse_protocol_resp_op( + ustr: *const std::os::raw::c_char, +) -> *mut DetectUintData { + let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe + if let Ok(s) = ft_name.to_str() { + if let Some(ctx) = aux_ldap_parse_protocol_resp_op(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +unsafe extern "C" fn ldap_detect_request_operation_setup( + de: *mut c_void, s: *mut c_void, raw: *const libc::c_char, +) -> c_int { + if DetectSignatureSetAppProto(s, ALPROTO_LDAP) != 0 { + return -1; + } + let ctx = ldap_parse_protocol_req_op(raw) as *mut c_void; + if ctx.is_null() { + return -1; + } + if SigMatchAppendSMToList( + de, + s, + G_LDAP_REQUEST_OPERATION_KW_ID, + ctx, + G_LDAP_REQUEST_OPERATION_BUFFER_ID, + ) + .is_null() + { + ldap_detect_operation_free(std::ptr::null_mut(), ctx); + return -1; + } + return 0; +} + +unsafe extern "C" fn ldap_detect_responses_operation_setup( + de: *mut c_void, s: *mut c_void, raw: *const libc::c_char, +) -> c_int { + if DetectSignatureSetAppProto(s, ALPROTO_LDAP) != 0 { + return -1; + } + let ctx = ldap_parse_protocol_resp_op(raw) as *mut c_void; + if ctx.is_null() { + return -1; + } + if SigMatchAppendSMToList( + de, + s, + G_LDAP_RESPONSES_OPERATION_KW_ID, + ctx, + G_LDAP_RESPONSES_OPERATION_BUFFER_ID, + ) + .is_null() + { + ldap_detect_operation_free(std::ptr::null_mut(), ctx); + return -1; + } + return 0; +} + +unsafe extern "C" fn ldap_detect_request_operation_match( + _de: *mut c_void, _f: *mut c_void, _flags: u8, _state: *mut c_void, tx: *mut c_void, + _sig: *const c_void, ctx: *const c_void, +) -> c_int { + let tx = cast_pointer!(tx, LdapTransaction); + let ctx = cast_pointer!(ctx, DetectUintData); + if let Some(request) = &tx.request { + let option: u8 = request.protocol_op.to_u8(); + return rs_detect_u8_match(option, ctx); + } + return 0; +} + +unsafe extern "C" fn ldap_detect_responses_operation_match( + _de: *mut c_void, _f: *mut c_void, _flags: u8, _state: *mut c_void, tx: *mut c_void, + _sig: *const c_void, ctx: *const c_void, +) -> c_int { + let tx = cast_pointer!(tx, LdapTransaction); + let ctx = cast_pointer!(ctx, DetectLdapRespData); + + match ctx.index { + DETECT_LDAP_RESP_ANY => { + for response in &tx.responses { + let option: u8 = response.protocol_op.to_u8(); + if rs_detect_u8_match(option, &ctx.response) == 1 { + return 1; + } + } + return 0; + } + DETECT_LDAP_RESP_ALL => { + for response in &tx.responses { + let option: u8 = response.protocol_op.to_u8(); + if rs_detect_u8_match(option, &ctx.response) == 0 { + return 0; + } + } + return 1; + } + _ => { + let index: usize = ctx.index as usize; + if tx.responses.len() <= index { + return 0; + } + let response: &LdapMessage = &tx.responses[index]; + let option: u8 = response.protocol_op.to_u8(); + if rs_detect_u8_match(option, &ctx.response) == 1 { + return 1; + } + return 0; + } + } +} + +unsafe extern "C" fn ldap_detect_operation_free(_de: *mut c_void, ctx: *mut c_void) { + // Just unbox... + let ctx = cast_pointer!(ctx, DetectUintData); + rs_detect_u8_free(ctx); +} + +#[no_mangle] +pub unsafe extern "C" fn ScDetectLdapRegister() { + let kw = SCSigTableElmt { + name: b"ldap.request.operation\0".as_ptr() as *const libc::c_char, + desc: b"match LDAP request operation\0".as_ptr() as *const libc::c_char, + url: b"/rules/ldap-keywords.html#ldap.request.operation\0".as_ptr() as *const libc::c_char, + AppLayerTxMatch: Some(ldap_detect_request_operation_match), + Setup: ldap_detect_request_operation_setup, + Free: Some(ldap_detect_operation_free), + flags: 0, + }; + G_LDAP_REQUEST_OPERATION_KW_ID = DetectHelperKeywordRegister(&kw); + G_LDAP_REQUEST_OPERATION_BUFFER_ID = DetectHelperBufferRegister( + b"ldap.request.operation\0".as_ptr() as *const libc::c_char, + ALPROTO_LDAP, + false, //to client + true, //to server + ); + let kw = SCSigTableElmt { + name: b"ldap.responses.operation\0".as_ptr() as *const libc::c_char, + desc: b"match LDAP responses operation\0".as_ptr() as *const libc::c_char, + url: b"/rules/ldap-keywords.html#ldap.responses.operation\0".as_ptr() + as *const libc::c_char, + AppLayerTxMatch: Some(ldap_detect_responses_operation_match), + Setup: ldap_detect_responses_operation_setup, + Free: Some(ldap_detect_operation_free), + flags: 0, + }; + G_LDAP_RESPONSES_OPERATION_KW_ID = DetectHelperKeywordRegister(&kw); + G_LDAP_RESPONSES_OPERATION_BUFFER_ID = DetectHelperBufferRegister( + b"ldap.responses.operation\0".as_ptr() as *const libc::c_char, + ALPROTO_LDAP, + true, //to client + false, //to server + ); +} diff --git a/rust/src/ldap/ldap.rs b/rust/src/ldap/ldap.rs index 4fb8ccc10b97..894d49ba0bcd 100644 --- a/rust/src/ldap/ldap.rs +++ b/rust/src/ldap/ldap.rs @@ -33,7 +33,7 @@ static LDAP_MAX_TX_DEFAULT: usize = 256; static mut LDAP_MAX_TX: usize = LDAP_MAX_TX_DEFAULT; -static mut ALPROTO_LDAP: AppProto = ALPROTO_UNKNOWN; +pub(super) static mut ALPROTO_LDAP: AppProto = ALPROTO_UNKNOWN; const STARTTLS_OID: &str = "1.3.6.1.4.1.1466.20037"; diff --git a/rust/src/ldap/mod.rs b/rust/src/ldap/mod.rs index 2f18058693e6..723d7e87fb31 100644 --- a/rust/src/ldap/mod.rs +++ b/rust/src/ldap/mod.rs @@ -17,6 +17,7 @@ // written by Giuseppe Longo +pub mod detect; pub mod filters; pub mod ldap; pub mod logger; diff --git a/rust/src/ldap/types.rs b/rust/src/ldap/types.rs index 901201b4a35e..52006bbcb538 100644 --- a/rust/src/ldap/types.rs +++ b/rust/src/ldap/types.rs @@ -300,6 +300,60 @@ pub enum ProtocolOp { Unknown, } +#[derive(Clone, Debug, Default, EnumStringU8)] +#[repr(u8)] +pub enum ProtocolOpCode { + #[default] + BindRequest = 0, + BindResponse = 1, + UnbindRequest = 2, + SearchRequest = 3, + SearchResultEntry = 4, + SearchResultDone = 5, + SearchResultReference = 19, + ModifyRequest = 6, + ModifyResponse = 7, + AddRequest = 8, + AddResponse = 9, + DelRequest = 10, + DelResponse = 11, + ModDnRequest = 12, + ModDnResponse = 13, + CompareRequest = 14, + CompareResponse = 15, + ExtendedRequest = 23, + ExtendedResponse = 24, + IntermediateResponse = 25, +} + +impl ProtocolOp { + pub fn to_u8(&self) -> u8 { + match self { + ProtocolOp::BindRequest(_) => 0, + ProtocolOp::BindResponse(_) => 1, + ProtocolOp::UnbindRequest => 2, + ProtocolOp::SearchRequest(_) => 3, + ProtocolOp::SearchResultEntry(_) => 4, + ProtocolOp::SearchResultDone(_) => 5, + ProtocolOp::SearchResultReference(_) => 19, + ProtocolOp::ModifyRequest(_) => 6, + ProtocolOp::ModifyResponse(_) => 7, + ProtocolOp::AddRequest(_) => 8, + ProtocolOp::AddResponse(_) => 9, + ProtocolOp::DelRequest(_) => 10, + ProtocolOp::DelResponse(_) => 11, + ProtocolOp::ModDnRequest(_) => 12, + ProtocolOp::ModDnResponse(_) => 13, + ProtocolOp::CompareRequest(_) => 14, + ProtocolOp::CompareResponse(_) => 15, + ProtocolOp::ExtendedRequest(_) => 23, + ProtocolOp::ExtendedResponse(_) => 24, + ProtocolOp::IntermediateResponse(_) => 25, + ProtocolOp::Unknown => 255, + } + } +} + impl Display for ProtocolOp { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 9bddf0fd8437..36d31ae41ebb 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -707,6 +707,7 @@ void SigTableSetup(void) ScDetectRfbRegister(); ScDetectSipRegister(); ScDetectTemplateRegister(); + ScDetectLdapRegister(); /* close keyword registration */ DetectBufferTypeCloseRegistration();