]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
detect: add ldap.responses.result_code
authorAlice Akaki <akakialice@gmail.com>
Thu, 6 Feb 2025 06:07:05 +0000 (02:07 -0400)
committerVictor Julien <victor@inliniac.net>
Wed, 5 Mar 2025 14:59:53 +0000 (15:59 +0100)
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

doc/userguide/rules/ldap-keywords.rst
rust/src/ldap/detect.rs
rust/src/ldap/logger.rs
rust/src/ldap/types.rs

index 1e76c99360c66e7068349c3d15db84353c215ab6..2f545f7139f2f1c07b5585cfbfa36e2eeb2b4469 100644 (file)
@@ -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 <rules-integer-keywords>`.
+
+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;)
index fcd84c56111b5054b5f1226c2500f15e78bafedf..b863643149cbe73a51f14f094745d172df1c79d9 100644 (file)
@@ -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<u32>,
+    /// 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<DetectLdapRespResultData> {
+    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::<u32, LdapResultCode>(parts[0])?;
+
+    Some(DetectLdapRespResultData { du32, index })
+}
+
+unsafe extern "C" fn ldap_parse_responses_result_code(
+    ustr: *const std::os::raw::c_char,
+) -> *mut DetectUintData<u32> {
+    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<u32> {
+    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::<LdapMessage, u32>(
+        &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
+    );
 }
index d57f8d80527f0ad146ac37c1cc1b16e9b7c97693..7ab372d877beda887db698f210197c9851d1dce7 100644 (file)
@@ -17,6 +17,7 @@
 
 // written by Giuseppe Longo <giuseppe@glongo.it>
 
+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(())
index d495a112060481394a055366581f783500765b3a..92b105ce22b52b982e82bb0c1f294d552e64c745 100644 (file)
@@ -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<Vec<u8>>,
 }
 
+#[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),