From: Alice Akaki Date: Thu, 6 Feb 2025 06:07:05 +0000 (-0400) Subject: detect: add ldap.responses.result_code X-Git-Tag: suricata-8.0.0-beta1~326 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=84605db01d21de421836c98addb3109bd2ea3e4b;p=thirdparty%2Fsuricata.git detect: add ldap.responses.result_code ldap.responses.result_code matches on LDAP result code This keyword maps the following eve fields: ldap.responses[].bind_response.result_code ldap.responses[].search_result_done.result_code ldap.responses[].modify_response.result_code ldap.responses[].add_response.result_code ldap.responses[].del_response.result_code ldap.responses[].mod_dn_response.result_code ldap.responses[].compare_response.result_code ldap.responses[].extended_response.result_code It is an unsigned 32-bit integer Doesn't support prefiltering Ticket: #7532 --- diff --git a/doc/userguide/rules/ldap-keywords.rst b/doc/userguide/rules/ldap-keywords.rst index 1e76c99360..2f545f7139 100644 --- a/doc/userguide/rules/ldap-keywords.rst +++ b/doc/userguide/rules/ldap-keywords.rst @@ -259,3 +259,159 @@ and contains the LDAP distinguished name ``dc=example,dc=com``. .. container:: example-rule alert ldap any any -> any any (msg:"Test LDAPDN and operation"; :example-rule-emphasis:`ldap.responses.operation:search_result_entry,1; ldap.responses.dn; content:"dc=example,dc=com";` sid:1;) + +ldap.responses.result_code +-------------------------- + +Suricata has a ``ldap.responses.result_code`` keyword that can be used in signatures to identify +and filter network packets based on their LDAP result code. + +Syntax:: + + ldap.responses.result_code: code[,index]; + +ldap.responses.result_code uses :ref:`unsigned 32-bit integer `. + +This keyword maps to the following eve fields: + + - ``ldap.responses[].bind_response.result_code`` + - ``ldap.responses[].search_result_done.result_code`` + - ``ldap.responses[].modify_response.result_code`` + - ``ldap.responses[].add_response.result_code`` + - ``ldap.responses[].del_response.result_code`` + - ``ldap.responses[].mod_dn_response.result_code`` + - ``ldap.responses[].compare_response.result_code`` + - ``ldap.responses[].extended_response.result_code`` + +.. table:: **Result code values for ldap.responses.result_code** + + ========= ================================================ + Code Name + ========= ================================================ + 0 success + 1 operations_error + 2 protocol_error + 3 time_limit_exceeded + 4 size_limit_exceeded + 5 compare_false + 6 compare_true + 7 auth_method_not_supported + 8 stronger_auth_required + 10 referral + 11 admin_limit_exceeded + 12 unavailable_critical_extension + 13 confidentiality_required + 14 sasl_bind_in_progress + 16 no_such_attribute + 17 undefined_attribute_type + 18 inappropriate_matching + 19 constraint_violation + 20 attribute_or_value_exists + 21 invalid_attribute_syntax + 32 no_such_object + 33 alias_problem + 34 invalid_dns_syntax + 35 is_leaf + 36 alias_dereferencing_problem + 48 inappropriate_authentication + 49 invalid_credentials + 50 insufficient_access_rights + 51 busy + 52 unavailable + 53 unwilling_to_perform + 54 loop_detect + 60 sort_control_missing + 61 offset_range_error + 64 naming_violation + 65 object_class_violation + 66 not_allowed_on_non_leaf + 67 not_allowed_on_rdn + 68 entry_already_exists + 69 object_class_mods_prohibited + 70 results_too_large + 71 affects_multiple_dsas + 76 control_error + 80 other + 81 server_down + 82 local_error + 83 encoding_error + 84 decoding_error + 85 timeout + 86 auth_unknown + 87 filter_error + 88 user_canceled + 89 param_error + 90 no_memory + 91 connect_error + 92 not_supported + 93 control_not_found + 94 no_results_returned + 95 more_results_to_return + 96 client_loop + 97 referral_limit_exceeded + 100 invalid_response + 101 ambiguous_response + 112 tls_not_supported + 113 intermediate_response + 114 unknown_type + 118 canceled + 119 no_such_operation + 120 too_late + 121 cannot_cancel + 122 assertion_failed + 123 authorization_denied + 4096 e_sync_refresh_required + 16654 no_operation + ========= ================================================ + +More information about LDAP result code values can be found here: +https://ldap.com/ldap-result-code-reference/ + +An LDAP request operation can receive multiple responses. By default, the ldap.responses.result_code +keyword matches with any 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.result_code keyword** + + ========= ================================================ + Value Description + ========= ================================================ + [default] Match with any index + all Match only if all indexes match + any Match with any index + 0>= Match specific index + 0< Match specific index with back to front indexing + ========= ================================================ + +Examples +^^^^^^^^ + +Example of signatures that would alert if the packet has a ``success`` LDAP result code at any index: + +.. container:: example-rule + + alert ldap any any -> any any (msg:"Test LDAP result code"; :example-rule-emphasis:`ldap.responses.result_code:0;` sid:1;) + +.. container:: example-rule + + alert ldap any any -> any any (msg:"Test LDAP result code"; :example-rule-emphasis:`ldap.responses.result_code:success,any;` sid:1;) + +Example of a signature that would alert if the packet has an ``unavailable`` LDAP result code at index 1: + +.. container:: example-rule + + alert ldap any any -> any any (msg:"Test LDAP result code at index 1"; :example-rule-emphasis:`ldap.responses.result_code:unavailable,1;` sid:1;) + +Example of a signature that would alert if all the responses have a ``success`` LDAP result code: + +.. container:: example-rule + + alert ldap any any -> any any (msg:"Test all LDAP responses have success result code"; :example-rule-emphasis:`ldap.responses.result_code:success,all;` sid:1;) + +The keyword ldap.responses.result_code supports back to front indexing with negative numbers, +this means that -1 will represent the last index, -2 the second to last index, and so on. +This is an example of a signature that would alert if a ``success`` result code is found at the last index: + +.. container:: example-rule + + alert ldap any any -> any any (msg:"Test LDAP success at last index"; :example-rule-emphasis:`ldap.responses.result_code:success,-1;` sid:1;) diff --git a/rust/src/ldap/detect.rs b/rust/src/ldap/detect.rs index fcd84c5611..b863643149 100644 --- a/rust/src/ldap/detect.rs +++ b/rust/src/ldap/detect.rs @@ -26,7 +26,7 @@ use crate::detect::{ DetectHelperMultiBufferMpmRegister, DetectSignatureSetAppProto, SCSigTableElmt, SigMatchAppendSMToList, SIGMATCH_INFO_STICKY_BUFFER, SIGMATCH_NOOPT, }; -use crate::ldap::types::{LdapMessage, ProtocolOp, ProtocolOpCode}; +use crate::ldap::types::{LdapMessage, LdapResultCode, ProtocolOp, ProtocolOpCode}; use std::collections::VecDeque; use std::ffi::CStr; @@ -50,6 +50,15 @@ struct DetectLdapRespOpData { pub index: LdapIndex, } +struct DetectLdapRespResultData { + /// Ldap result code + pub du32: DetectUintData, + /// Index can be Any to match with any responses index, + /// All to match if all indices, or an i32 integer + /// Negative values represent back to front indexing. + pub index: LdapIndex, +} + 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; @@ -58,6 +67,8 @@ static mut G_LDAP_RESPONSES_COUNT_KW_ID: c_int = 0; static mut G_LDAP_RESPONSES_COUNT_BUFFER_ID: c_int = 0; static mut G_LDAP_REQUEST_DN_BUFFER_ID: c_int = 0; static mut G_LDAP_RESPONSES_DN_BUFFER_ID: c_int = 0; +static mut G_LDAP_RESPONSES_RESULT_CODE_KW_ID: c_int = 0; +static mut G_LDAP_RESPONSES_RESULT_CODE_BUFFER_ID: c_int = 0; unsafe extern "C" fn ldap_parse_protocol_req_op( ustr: *const std::os::raw::c_char, @@ -402,6 +413,92 @@ unsafe extern "C" fn ldap_tx_get_responses_dn( return true; } +fn aux_ldap_parse_resp_result_code(s: &str) -> Option { + let parts: Vec<&str> = s.split(',').collect(); + if parts.len() > 2 { + return None; + } + + let index = parse_ldap_index(&parts)?; + let du32 = detect_parse_uint_enum::(parts[0])?; + + Some(DetectLdapRespResultData { du32, index }) +} + +unsafe extern "C" fn ldap_parse_responses_result_code( + 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_resp_result_code(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +unsafe extern "C" fn ldap_detect_responses_result_code_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_responses_result_code(raw) as *mut c_void; + if ctx.is_null() { + return -1; + } + if SigMatchAppendSMToList( + de, + s, + G_LDAP_RESPONSES_RESULT_CODE_KW_ID, + ctx, + G_LDAP_RESPONSES_RESULT_CODE_BUFFER_ID, + ) + .is_null() + { + ldap_detect_responses_result_code_free(std::ptr::null_mut(), ctx); + return -1; + } + return 0; +} + +fn get_ldap_result_code(response: &LdapMessage) -> Option { + return match &response.protocol_op { + ProtocolOp::BindResponse(resp) => Some(resp.result.result_code.0), + ProtocolOp::SearchResultDone(resp) => Some(resp.result_code.0), + ProtocolOp::ModifyResponse(resp) => Some(resp.result.result_code.0), + ProtocolOp::AddResponse(resp) => Some(resp.result_code.0), + ProtocolOp::DelResponse(resp) => Some(resp.result_code.0), + ProtocolOp::ModDnResponse(resp) => Some(resp.result_code.0), + ProtocolOp::CompareResponse(resp) => Some(resp.result_code.0), + ProtocolOp::ExtendedResponse(resp) => Some(resp.result.result_code.0), + _ => None, + }; +} + +unsafe extern "C" fn ldap_detect_responses_result_code_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, DetectLdapRespResultData); + + return match_at_index::( + &tx.responses, + &ctx.du32, + get_ldap_result_code, + |code, ctx_value| detect_match_uint(ctx_value, code) as c_int, + &ctx.index, + ); +} + +unsafe extern "C" fn ldap_detect_responses_result_code_free(_de: *mut c_void, ctx: *mut c_void) { + // Just unbox... + let ctx = cast_pointer!(ctx, DetectLdapRespResultData); + std::mem::drop(Box::from_raw(ctx)); +} + #[no_mangle] pub unsafe extern "C" fn SCDetectLdapRegister() { let kw = SCSigTableElmt { @@ -489,4 +586,21 @@ pub unsafe extern "C" fn SCDetectLdapRegister() { false, //to server ldap_detect_responses_dn_get_data, ); + let kw = SCSigTableElmt { + name: b"ldap.responses.result_code\0".as_ptr() as *const libc::c_char, + desc: b"match LDAPResult code\0".as_ptr() as *const libc::c_char, + url: b"/rules/ldap-keywords.html#ldap.responses.result_code\0".as_ptr() + as *const libc::c_char, + AppLayerTxMatch: Some(ldap_detect_responses_result_code_match), + Setup: ldap_detect_responses_result_code_setup, + Free: Some(ldap_detect_responses_result_code_free), + flags: 0, + }; + G_LDAP_RESPONSES_RESULT_CODE_KW_ID = DetectHelperKeywordRegister(&kw); + G_LDAP_RESPONSES_RESULT_CODE_BUFFER_ID = DetectHelperBufferRegister( + b"ldap.responses.result_code\0".as_ptr() as *const libc::c_char, + ALPROTO_LDAP, + true, //to client + false, //to server + ); } diff --git a/rust/src/ldap/logger.rs b/rust/src/ldap/logger.rs index d57f8d8052..7ab372d877 100644 --- a/rust/src/ldap/logger.rs +++ b/rust/src/ldap/logger.rs @@ -17,6 +17,7 @@ // written by Giuseppe Longo +use crate::detect::EnumString; use crate::jsonbuilder::{JsonBuilder, JsonError}; use crate::ldap::filters::*; use crate::ldap::ldap::LdapTransaction; @@ -319,7 +320,11 @@ fn log_intermediate_response( } fn log_ldap_result(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { - js.set_string("result_code", &msg.result_code.to_string())?; + if let Some(rc) = LdapResultCode::from_u(msg.result_code.0) { + js.set_string("result_code", rc.to_str())?; + } else { + js.set_string("result_code", &format!("unknown-{}", msg.result_code.0))?; + } js.set_string("matched_dn", &msg.matched_dn.0)?; js.set_string("message", &msg.diagnostic_message.0)?; Ok(()) diff --git a/rust/src/ldap/types.rs b/rust/src/ldap/types.rs index d495a11206..92b105ce22 100644 --- a/rust/src/ldap/types.rs +++ b/rust/src/ldap/types.rs @@ -43,88 +43,6 @@ impl Display for Operation { #[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); @@ -280,6 +198,85 @@ pub struct IntermediateResponse { pub response_value: Option>, } +#[derive(Clone, Debug, EnumStringU32)] +#[repr(u32)] +pub enum LdapResultCode { + Success = 0, + OperationsError = 1, + ProtocolError = 2, + TimeLimitExceeded = 3, + SizeLimitExceeded = 4, + CompareFalse = 5, + CompareTrue = 6, + AuthMethodNotSupported = 7, + StrongerAuthRequired = 8, + Referral = 10, + AdminLimitExceeded = 11, + UnavailableCriticalExtension = 12, + ConfidentialityRequired = 13, + SaslBindInProgress = 14, + NoSuchAttribute = 16, + UndefinedAttributeType = 17, + InappropriateMatching = 18, + ConstraintViolation = 19, + AttributeOrValueExists = 20, + InvalidAttributeSyntax = 21, + NoSuchObject = 32, + AliasProblem = 33, + InvalidDnsSyntax = 34, + IsLeaf = 35, + AliasDereferencingProblem = 36, + InappropriateAuthentication = 48, + InvalidCredentials = 49, + InsufficientAccessRights = 50, + Busy = 51, + Unavailable = 52, + UnwillingToPerform = 53, + LoopDetect = 54, + SortControlMissing = 60, + OffsetRangeError = 61, + NamingViolation = 64, + ObjectClassViolation = 65, + NotAllowedOnNonLeaf = 66, + NotAllowedOnRdn = 67, + EntryAlreadyExists = 68, + ObjectClassModsProhibited = 69, + ResultsTooLarge = 70, + AffectsMultipleDsas = 71, + ControlError = 76, + Other = 80, + ServerDown = 81, + LocalError = 82, + EncodingError = 83, + DecodingError = 84, + Timeout = 85, + AuthUnknown = 86, + FilterError = 87, + UserCanceled = 88, + ParamError = 89, + NoMemory = 90, + ConnectError = 91, + NotSupported = 92, + ControlNotFound = 93, + NoResultsReturned = 94, + MoreResultsToReturn = 95, + ClientLoop = 96, + ReferralLimitExceeded = 97, + InvalidResponse = 100, + AmbiguousResponse = 101, + TlsNotSupported = 112, + IntermediateResponse = 113, + UnknownType = 114, + Canceled = 118, + NoSuchOperation = 119, + TooLate = 120, + CannotCancel = 121, + AssertionFailed = 122, + AuthorizationDenied = 123, + ESyncRefreshRequired = 4096, + NoOperation = 16654, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub enum ProtocolOp { BindRequest(BindRequest),