Skip to content

Commit

Permalink
detect: add ldap operation keywords
Browse files Browse the repository at this point in the history
ldap.request.operation matches on Lightweight Directory Access Protocol request operations
ldap.responses.operation matches on Lightweight Directory Access Protocol response operations
Both are unsigned 8-bit integer
Don't support prefiltering

Ticket: #7453
  • Loading branch information
AkakiAlice committed Jan 6, 2025
1 parent def22fa commit 061a50a
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 1 deletion.
1 change: 1 addition & 0 deletions doc/userguide/rules/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ Suricata Rules
differences-from-snort
multi-buffer-matching
tag
ldap-keywords
108 changes: 108 additions & 0 deletions doc/userguide/rules/ldap-keywords.rst
Original file line number Diff line number Diff line change
@@ -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 <rules-integer-keywords>`.

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 <rules-integer-keywords>`.

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;)
244 changes: 244 additions & 0 deletions rust/src/ldap/detect.rs
Original file line number Diff line number Diff line change
@@ -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<u8>, //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<u8> {
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::<u8, ProtocolOpCode>(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<DetectLdapRespData> {
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::<u8, ProtocolOpCode>(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<u8> {
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<u8>);
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<u8>);
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
);
}
2 changes: 1 addition & 1 deletion rust/src/ldap/ldap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
1 change: 1 addition & 0 deletions rust/src/ldap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

// written by Giuseppe Longo <[email protected]>

pub mod detect;
pub mod filters;
pub mod ldap;
pub mod logger;
Expand Down
Loading

0 comments on commit 061a50a

Please sign in to comment.