Merge remote-tracking branch 'origin/topic/bbannier/issue-3234'

* origin/topic/bbannier/issue-3234:
  Introduce dedicated `LDAP::Info`
  Remove redundant storing of protocol in LDAP logs
  Use LDAP `RemovalHook` instead of implementing `connection_state_remove`
  Tidy up LDAP code by using local references
  Pluralize container names in LDAP types
  Move LDAP script constants to their own file
  Name `LDAP::Message` and `LDAP::Search` `*Info`
  Make ports for LDAP analyzers fully configurable
  Require have-spicy for tests which log spicy-ldap information
  Fix LDAP analyzer setup for when Spicy analyzers are disabled
  Bump zeek-testing-private
  Integrate spicy-ldap test suite
  Move spicy-ldap into Zeek protocol analyzer tree
  Explicitly use all of spicy-ldap's modules
  Explicitly list `asn1.spicy` as spicy-ldap source
  Remove uses of `zeek` module in spicy-ldap
  Fix typos in spicy-ldap
  Remove project configuration files in spicy-ldap
  Integrate spicy-ldap into build
  Import zeek/spicy-ldap@57b5eff988
This commit is contained in:
Arne Welzel 2023-10-10 20:03:31 +02:00
commit 72df1a0216
53 changed files with 2274 additions and 17 deletions

View file

@ -69,7 +69,8 @@
"kwargs": {
"NAME": "*",
"PACKAGE_NAME": "*",
"SOURCES": "*"
"SOURCES": "*",
"MODULES": "*"
}
}
}

View file

@ -69,3 +69,5 @@ caf = "caf"
helo = "helo"
# Seems we use this in the management framework
requestor = "requestor"
# `inout` is used as a keyword in Spicy, but looks like a typo of `input`.
inout = "inout"

86
CHANGES
View file

@ -1,3 +1,89 @@
6.1.0-dev.528 | 2023-10-10 20:03:31 +0200
* Introduce dedicated `LDAP::Info` (Benjamin Bannier, Corelight)
* Remove redundant storing of protocol in LDAP logs (Benjamin Bannier, Corelight)
* Use LDAP `RemovalHook` instead of implementing `connection_state_remove` (Benjamin Bannier, Corelight)
* Tidy up LDAP code by using local references (Benjamin Bannier, Corelight)
* Pluralize container names in LDAP types (Benjamin Bannier, Corelight)
* Move LDAP script constants to their own file (Benjamin Bannier, Corelight)
* Name `LDAP::Message` and `LDAP::Search` `*Info` (Benjamin Bannier, Corelight)
* Make ports for LDAP analyzers fully configurable (Benjamin Bannier, Corelight)
This moves the ports the LDAP analyzers should be triggered on from the
EVT file to the Zeek module. This gives users full control over which
ports the analyzers are registered for while previously they could only
register them for additional ports (there is no Zeek script equivalent
of `Manager::UnregisterAnalyzerForPort`).
The analyzers could still be triggered via DPD, but this is intentional.
To fully disable analyzers users can use e.g.,
```zeek
event zeek_init()
{
Analyzer::disable_analyzer(Analyzer::ANALYZER_LDAP_TCP);
}
```
* Require have-spicy for tests which log spicy-ldap information (Benjamin Bannier, Corelight)
* Fix LDAP analyzer setup for when Spicy analyzers are disabled (Benjamin Bannier, Corelight)
* Bump zeek-testing-private (Benjamin Bannier, Corelight)
* Integrate spicy-ldap test suite (Benjamin Bannier, Corelight)
* Move spicy-ldap into Zeek protocol analyzer tree (Benjamin Bannier, Corelight)
* Explicitly use all of spicy-ldap's modules (Benjamin Bannier, Corelight)
This fixes building of this multi-module analyzer.
* Explicitly list `asn1.spicy` as spicy-ldap source (Benjamin Bannier, Corelight)
This will lead to the file being installed so it can be consumed by
other parsers.
* Remove uses of `zeek` module in spicy-ldap (Benjamin Bannier, Corelight)
* Fix typos in spicy-ldap (Benjamin Bannier, Corelight)
* Remove project configuration files in spicy-ldap (Benjamin Bannier, Corelight)
* Integrate spicy-ldap into build (Benjamin Bannier, Corelight)
* Import zeek/spicy-ldap@57b5eff9883d1c43896f4278218351a132de9ce1 (Benjamin Bannier, Corelight)
* Merge branch 'topic/timw/arm-build-failure' (Tim Wojtulewicz, Corelight)
* topic/timw/arm-build-failure:
CI: Disable spicy on arm_debian11 and opensuse_leap_15_4 builds
CI: Remove sanitizers resource template
* CI: Disable spicy on arm_debian11 and opensuse_leap_15_4 builds (Tim Wojtulewicz, Corelight)
These two builds are struggling to complete due to OOM issues. Reducing the
number of CPUs didn't help, so this temporarily disables Spicy from
building on those hosts to reduce the memory usage.
* CI: Remove sanitizers resource template (Tim Wojtulewicz, Corelight)
This template was added when the sanitizer builds needed more memory than all
of the other builds. We've since increased the amount of memory that the other
builds use beyond what's requested in this template, so it doesn't make sense
to keep it around.
* Updating zeek-aux submodule [nomail] (Arne Welzel, Corelight)
* NEWS: Polish external plugins message [nomail] [skip ci] (Arne Welzel, Corelight)
6.1.0-dev.502 | 2023-10-09 16:26:58 +0200
* btest/plugins/hooks: Run in bare mode (Arne Welzel, Corelight)

19
NEWS
View file

@ -29,6 +29,25 @@ Breaking Changes
New Functionality
-----------------
- Zeek now includes the LDAP protocol analyzer from the zeek/spicy-ldap
project (https://github.com/zeek/spicy-ldap). This analyzer is enabled by
default. The analyzer's events and its ``ldap.log`` and ``ldap_search.log``
should be considered preliminary and experimental until the arrival of
Zeek's next long-term-stable release (7.0).
If you observe unusually high CPU consumption or other issues due to this
analyzer being enabled by default, the easiest way to disable it is via
the ``Analyzer::disabled_analyzers`` const as follows:
redef Analyzer::disabled_analyzers += {
Analyzer::ANALYZER_LDAP_UDP,
Analyzer::ANALYZER_LDAP_TCP,
};
Please do report issues to us including diagnostic information in case this is
necessary in your environment. We're also open to general feedback about the
structure of the new logs.
- Added a new ``assert`` statement for assertion based testing and asserting
runtime state.

View file

@ -1 +1 @@
6.1.0-dev.502
6.1.0-dev.528

View file

@ -59,6 +59,7 @@
@load base/protocols/imap
@load base/protocols/irc
@load base/protocols/krb
@load base/protocols/ldap
@load base/protocols/modbus
@load base/protocols/mqtt
@load base/protocols/mysql

View file

@ -0,0 +1,5 @@
@if ( have_spicy_analyzers() )
@load-sigs ./dpd.sig
@load ./consts
@load ./main.zeek
@endif

View file

@ -0,0 +1,123 @@
module LDAP;
export {
const PROTOCOL_OPCODES = { [ LDAP::ProtocolOpcode_BIND_REQUEST ] = "bind", [
LDAP::ProtocolOpcode_BIND_RESPONSE ] = "bind", [
LDAP::ProtocolOpcode_UNBIND_REQUEST ] = "unbind", [
LDAP::ProtocolOpcode_SEARCH_REQUEST ] = "search", [
LDAP::ProtocolOpcode_SEARCH_RESULT_ENTRY ] = "search", [
LDAP::ProtocolOpcode_SEARCH_RESULT_DONE ] = "search", [
LDAP::ProtocolOpcode_MODIFY_REQUEST ] = "modify", [
LDAP::ProtocolOpcode_MODIFY_RESPONSE ] = "modify", [
LDAP::ProtocolOpcode_ADD_REQUEST ] = "add", [
LDAP::ProtocolOpcode_ADD_RESPONSE ] = "add", [
LDAP::ProtocolOpcode_DEL_REQUEST ] = "delete", [
LDAP::ProtocolOpcode_DEL_RESPONSE ] = "delete", [
LDAP::ProtocolOpcode_MOD_DN_REQUEST ] = "modify", [
LDAP::ProtocolOpcode_MOD_DN_RESPONSE ] = "modify", [
LDAP::ProtocolOpcode_COMPARE_REQUEST ] = "compare", [
LDAP::ProtocolOpcode_COMPARE_RESPONSE ] = "compare", [
LDAP::ProtocolOpcode_ABANDON_REQUEST ] = "abandon", [
LDAP::ProtocolOpcode_SEARCH_RESULT_REFERENCE ] = "search", [
LDAP::ProtocolOpcode_EXTENDED_REQUEST ] = "extended", [
LDAP::ProtocolOpcode_EXTENDED_RESPONSE ] = "extended", [
LDAP::ProtocolOpcode_INTERMEDIATE_RESPONSE ] = "intermediate" }
&default="unknown";
const BIND_SIMPLE = "bind simple";
const BIND_SASL = "bind SASL";
const RESULT_CODES = { [ LDAP::ResultCode_SUCCESS ] = "success", [
LDAP::ResultCode_OPERATIONS_ERROR ] = "operations error", [
LDAP::ResultCode_PROTOCOL_ERROR ] = "protocol error", [
LDAP::ResultCode_TIME_LIMIT_EXCEEDED ] = "time limit exceeded", [
LDAP::ResultCode_SIZE_LIMIT_EXCEEDED ] = "size limit exceeded", [
LDAP::ResultCode_COMPARE_FALSE ] = "compare false", [
LDAP::ResultCode_COMPARE_TRUE ] = "compare true", [
LDAP::ResultCode_AUTH_METHOD_NOT_SUPPORTED ] =
"auth method not supported", [
LDAP::ResultCode_STRONGER_AUTH_REQUIRED ] =
"stronger auth required", [ LDAP::ResultCode_PARTIAL_RESULTS ] =
"partial results", [ LDAP::ResultCode_REFERRAL ] = "referral", [
LDAP::ResultCode_ADMIN_LIMIT_EXCEEDED ] = "admin limit exceeded", [
LDAP::ResultCode_UNAVAILABLE_CRITICAL_EXTENSION ] =
"unavailable critical extension", [
LDAP::ResultCode_CONFIDENTIALITY_REQUIRED ] =
"confidentiality required", [ LDAP::ResultCode_SASL_BIND_IN_PROGRESS ] =
"SASL bind in progress", [ LDAP::ResultCode_NO_SUCH_ATTRIBUTE ] =
"no such attribute", [ LDAP::ResultCode_UNDEFINED_ATTRIBUTE_TYPE ] =
"undefined attribute type", [
LDAP::ResultCode_INAPPROPRIATE_MATCHING ] =
"inappropriate matching", [ LDAP::ResultCode_CONSTRAINT_VIOLATION ] =
"constraint violation", [ LDAP::ResultCode_ATTRIBUTE_OR_VALUE_EXISTS ] =
"attribute or value exists", [
LDAP::ResultCode_INVALID_ATTRIBUTE_SYNTAX ] =
"invalid attribute syntax", [ LDAP::ResultCode_NO_SUCH_OBJECT ] =
"no such object", [ LDAP::ResultCode_ALIAS_PROBLEM ] =
"alias problem", [ LDAP::ResultCode_INVALID_DNSYNTAX ] =
"invalid DN syntax", [ LDAP::ResultCode_ALIAS_DEREFERENCING_PROBLEM ] =
"alias dereferencing problem", [
LDAP::ResultCode_INAPPROPRIATE_AUTHENTICATION ] =
"inappropriate authentication", [
LDAP::ResultCode_INVALID_CREDENTIALS ] = "invalid credentials", [
LDAP::ResultCode_INSUFFICIENT_ACCESS_RIGHTS ] =
"insufficient access rights", [ LDAP::ResultCode_BUSY ] = "busy", [
LDAP::ResultCode_UNAVAILABLE ] = "unavailable", [
LDAP::ResultCode_UNWILLING_TO_PERFORM ] = "unwilling to perform", [
LDAP::ResultCode_LOOP_DETECT ] = "loop detect", [
LDAP::ResultCode_SORT_CONTROL_MISSING ] = "sort control missing", [
LDAP::ResultCode_OFFSET_RANGE_ERROR ] = "offset range error", [
LDAP::ResultCode_NAMING_VIOLATION ] = "naming violation", [
LDAP::ResultCode_OBJECT_CLASS_VIOLATION ] =
"object class violation", [ LDAP::ResultCode_NOT_ALLOWED_ON_NON_LEAF ] =
"not allowed on non-leaf", [ LDAP::ResultCode_NOT_ALLOWED_ON_RDN ] =
"not allowed on RDN", [ LDAP::ResultCode_ENTRY_ALREADY_EXISTS ] =
"entry already exists", [
LDAP::ResultCode_OBJECT_CLASS_MODS_PROHIBITED ] =
"object class mods prohibited", [ LDAP::ResultCode_RESULTS_TOO_LARGE ] =
"results too large", [ LDAP::ResultCode_AFFECTS_MULTIPLE_DSAS ] =
"affects multiple DSAs", [ LDAP::ResultCode_CONTROL_ERROR ] =
"control error", [ LDAP::ResultCode_OTHER ] = "other", [
LDAP::ResultCode_SERVER_DOWN ] = "server down", [
LDAP::ResultCode_LOCAL_ERROR ] = "local error", [
LDAP::ResultCode_ENCODING_ERROR ] = "encoding error", [
LDAP::ResultCode_DECODING_ERROR ] = "decoding error", [
LDAP::ResultCode_TIMEOUT ] = "timeout", [
LDAP::ResultCode_AUTH_UNKNOWN ] = "auth unknown", [
LDAP::ResultCode_FILTER_ERROR ] = "filter error", [
LDAP::ResultCode_USER_CANCELED ] = "user canceled", [
LDAP::ResultCode_PARAM_ERROR ] = "param error", [
LDAP::ResultCode_NO_MEMORY ] = "no memory", [
LDAP::ResultCode_CONNECT_ERROR ] = "connect error", [
LDAP::ResultCode_NOT_SUPPORTED ] = "not supported", [
LDAP::ResultCode_CONTROL_NOT_FOUND ] = "control not found", [
LDAP::ResultCode_NO_RESULTS_RETURNED ] = "no results returned", [
LDAP::ResultCode_MORE_RESULTS_TO_RETURN ] =
"more results to return", [ LDAP::ResultCode_CLIENT_LOOP ] =
"client loop", [ LDAP::ResultCode_REFERRAL_LIMIT_EXCEEDED ] =
"referral limit exceeded", [ LDAP::ResultCode_INVALID_RESPONSE ] =
"invalid response", [ LDAP::ResultCode_AMBIGUOUS_RESPONSE ] =
"ambiguous response", [ LDAP::ResultCode_TLS_NOT_SUPPORTED ] =
"TLS not supported", [ LDAP::ResultCode_INTERMEDIATE_RESPONSE ] =
"intermediate response", [ LDAP::ResultCode_UNKNOWN_TYPE ] =
"unknown type", [ LDAP::ResultCode_LCUP_INVALID_DATA ] =
"LCUP invalid data", [ LDAP::ResultCode_LCUP_UNSUPPORTED_SCHEME ] =
"LCUP unsupported scheme", [ LDAP::ResultCode_LCUP_RELOAD_REQUIRED ] =
"LCUP reload required", [ LDAP::ResultCode_CANCELED ] =
"canceled", [ LDAP::ResultCode_NO_SUCH_OPERATION ] =
"no such operation", [ LDAP::ResultCode_TOO_LATE ] = "too late", [
LDAP::ResultCode_CANNOT_CANCEL ] = "cannot cancel", [
LDAP::ResultCode_ASSERTION_FAILED ] = "assertion failed", [
LDAP::ResultCode_AUTHORIZATION_DENIED ] = "authorization denied" }
&default="unknown";
const SEARCH_SCOPES = { [ LDAP::SearchScope_SEARCH_BASE ] = "base", [
LDAP::SearchScope_SEARCH_SINGLE ] = "single", [
LDAP::SearchScope_SEARCH_TREE ] = "tree", } &default="unknown";
const SEARCH_DEREF_ALIASES = { [ LDAP::SearchDerefAlias_DEREF_NEVER ] =
"never", [ LDAP::SearchDerefAlias_DEREF_IN_SEARCHING ] =
"searching", [ LDAP::SearchDerefAlias_DEREF_FINDING_BASE ] =
"finding", [ LDAP::SearchDerefAlias_DEREF_ALWAYS ] = "always", }
&default="unknown";
}

View file

@ -0,0 +1,23 @@
signature dpd_ldap_client_udp {
ip-proto == udp
payload /^\x30.\x02\x01.\x60/
}
signature dpd_ldap_server_udp {
ip-proto == udp
payload /^\x30/
requires-reverse-signature dpd_ldap_client_udp
enable "LDAP_UDP"
}
signature dpd_ldap_client_tcp {
ip-proto == tcp
payload /^\x30.\x02\x01.\x60/
}
signature dpd_ldap_server_tcp {
ip-proto == tcp
payload /^\x30/
requires-reverse-signature dpd_ldap_client_tcp
enable "LDAP_TCP"
}

View file

@ -0,0 +1,368 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
@load base/protocols/conn/removal-hooks
@load ./consts
module LDAP;
export {
redef enum Log::ID += { LDAP_LOG, LDAP_SEARCH_LOG };
## TCP ports which should be considered for analysis.
const ports_tcp = { 389/tcp, 3268/tcp } &redef;
## UDP ports which should be considered for analysis.
const ports_udp = { 389/udp } &redef;
## Whether clear text passwords are captured or not.
option default_capture_password = F;
## Whether to log LDAP search attributes or not.
option default_log_search_attributes = F;
## Default logging policy hook for LDAP_LOG.
global log_policy: Log::PolicyHook;
## Default logging policy hook for LDAP_SEARCH_LOG.
global log_policy_search: Log::PolicyHook;
## LDAP finalization hook.
global finalize_ldap: Conn::RemovalHook;
#############################################################################
# This is the format of ldap.log (ldap operations minus search-related)
# Each line represents a unique connection+message_id (requests/responses)
type MessageInfo: record {
# Timestamp for when the event happened.
ts: time &log;
# Unique ID for the connection.
uid: string &log;
# The connection's 4-tuple of endpoint addresses/ports.
id: conn_id &log;
# Message ID
message_id: int &log &optional;
# LDAP version
version: int &log &optional;
# normalized operations (e.g., bind_request and bind_response to "bind")
opcodes: set[string] &log &optional;
# Result code(s)
results: set[string] &log &optional;
# result diagnostic message(s)
diagnostic_messages: vector of string &log &optional;
# object(s)
objects: vector of string &log &optional;
# argument(s)
arguments: vector of string &log &optional;
};
#############################################################################
# This is the format of ldap_search.log (search-related messages only)
# Each line represents a unique connection+message_id (requests/responses)
type SearchInfo: record {
# Timestamp for when the event happened.
ts: time &log;
# Unique ID for the connection.
uid: string &log;
# The connection's 4-tuple of endpoint addresses/ports.
id: conn_id &log;
# Message ID
message_id: int &log &optional;
# sets of search scope and deref alias
scopes: set[string] &log &optional;
derefs: set[string] &log &optional;
# base search objects
base_objects: vector of string &log &optional;
# number of results returned
result_count: count &log &optional;
# Result code (s)
results: set[string] &log &optional;
# result diagnostic message(s)
diagnostic_messages: vector of string &log &optional;
# a string representation of the search filter used in the query
filter: string &log &optional;
# a list of attributes that were returned in the search
attributes: vector of string &log &optional;
};
type State: record {
messages: table[int] of MessageInfo &optional;
searches: table[int] of SearchInfo &optional;
};
# Event that can be handled to access the ldap record as it is sent on
# to the logging framework.
global log_ldap: event(rec: LDAP::MessageInfo);
global log_ldap_search: event(rec: LDAP::SearchInfo);
# Event called for each LDAP message (either direction)
global LDAP::message: event(c: connection,
message_id: int,
opcode: LDAP::ProtocolOpcode,
result: LDAP::ResultCode,
matched_dn: string,
diagnostic_message: string,
object: string,
argument: string);
}
redef record connection += {
ldap: State &optional;
};
redef likely_server_ports += { LDAP::ports_tcp, LDAP::ports_udp };
#############################################################################
global OPCODES_FINISHED: set[LDAP::ProtocolOpcode] = { LDAP::ProtocolOpcode_BIND_RESPONSE,
LDAP::ProtocolOpcode_UNBIND_REQUEST,
LDAP::ProtocolOpcode_SEARCH_RESULT_DONE,
LDAP::ProtocolOpcode_MODIFY_RESPONSE,
LDAP::ProtocolOpcode_ADD_RESPONSE,
LDAP::ProtocolOpcode_DEL_RESPONSE,
LDAP::ProtocolOpcode_MOD_DN_RESPONSE,
LDAP::ProtocolOpcode_COMPARE_RESPONSE,
LDAP::ProtocolOpcode_ABANDON_REQUEST,
LDAP::ProtocolOpcode_EXTENDED_RESPONSE };
global OPCODES_SEARCH: set[LDAP::ProtocolOpcode] = { LDAP::ProtocolOpcode_SEARCH_REQUEST,
LDAP::ProtocolOpcode_SEARCH_RESULT_ENTRY,
LDAP::ProtocolOpcode_SEARCH_RESULT_DONE,
LDAP::ProtocolOpcode_SEARCH_RESULT_REFERENCE };
#############################################################################
event zeek_init() &priority=5 {
Analyzer::register_for_ports(Analyzer::ANALYZER_LDAP_TCP, LDAP::ports_tcp);
Analyzer::register_for_ports(Analyzer::ANALYZER_LDAP_UDP, LDAP::ports_udp);
Log::create_stream(LDAP::LDAP_LOG, [$columns=MessageInfo, $ev=log_ldap, $path="ldap", $policy=log_policy]);
Log::create_stream(LDAP::LDAP_SEARCH_LOG, [$columns=SearchInfo, $ev=log_ldap_search, $path="ldap_search", $policy=log_policy_search]);
}
#############################################################################
function set_session(c: connection, message_id: int, opcode: LDAP::ProtocolOpcode) {
if (! c?$ldap ) {
c$ldap = State();
Conn::register_removal_hook(c, finalize_ldap);
}
if (! c$ldap?$messages )
c$ldap$messages = table();
if (! c$ldap?$searches )
c$ldap$searches = table();
if ((opcode in OPCODES_SEARCH) && (message_id !in c$ldap$searches)) {
c$ldap$searches[message_id] = [$ts=network_time(),
$uid=c$uid,
$id=c$id,
$message_id=message_id,
$result_count=0];
} else if ((opcode !in OPCODES_SEARCH) && (message_id !in c$ldap$messages)) {
c$ldap$messages[message_id] = [$ts=network_time(),
$uid=c$uid,
$id=c$id,
$message_id=message_id];
}
}
#############################################################################
event LDAP::message(c: connection,
message_id: int,
opcode: LDAP::ProtocolOpcode,
result: LDAP::ResultCode,
matched_dn: string,
diagnostic_message: string,
object: string,
argument: string) {
if (opcode == LDAP::ProtocolOpcode_SEARCH_RESULT_DONE) {
set_session(c, message_id, opcode);
local searches = c$ldap$searches[message_id];
if ( result != LDAP::ResultCode_Undef ) {
if ( ! searches?$results )
searches$results = set();
add searches$results[RESULT_CODES[result]];
}
if ( diagnostic_message != "" ) {
if ( ! searches?$diagnostic_messages )
searches$diagnostic_messages = vector();
searches$diagnostic_messages += diagnostic_message;
}
Log::write(LDAP::LDAP_SEARCH_LOG, searches);
delete c$ldap$searches[message_id];
} else if (opcode !in OPCODES_SEARCH) {
set_session(c, message_id, opcode);
local messages = c$ldap$messages[message_id];
if ( ! messages?$opcodes )
messages$opcodes = set();
add messages$opcodes[PROTOCOL_OPCODES[opcode]];
if ( result != LDAP::ResultCode_Undef ) {
if ( ! messages?$results )
messages$results = set();
add messages$results[RESULT_CODES[result]];
}
if ( diagnostic_message != "" ) {
if ( ! messages?$diagnostic_messages )
messages$diagnostic_messages = vector();
messages$diagnostic_messages += diagnostic_message;
}
if ( object != "" ) {
if ( ! messages?$objects )
messages$objects = vector();
messages$objects += object;
}
if ( argument != "" ) {
if ( ! messages?$arguments )
messages$arguments = vector();
if ("bind simple" in messages$opcodes && !default_capture_password)
messages$arguments += "REDACTED";
else
messages$arguments += argument;
}
if (opcode in OPCODES_FINISHED) {
if ((BIND_SIMPLE in messages$opcodes) ||
(BIND_SASL in messages$opcodes)) {
# don't have both "bind" and "bind <method>" in the operations list
delete messages$opcodes[PROTOCOL_OPCODES[LDAP::ProtocolOpcode_BIND_REQUEST]];
}
Log::write(LDAP::LDAP_LOG, messages);
delete c$ldap$messages[message_id];
}
}
}
#############################################################################
event LDAP::searchreq(c: connection,
message_id: int,
base_object: string,
scope: LDAP::SearchScope,
deref: LDAP::SearchDerefAlias,
size_limit: int,
time_limit: int,
types_only: bool,
filter: string,
attributes: vector of string) {
set_session(c, message_id, LDAP::ProtocolOpcode_SEARCH_REQUEST);
if ( scope != LDAP::SearchScope_Undef ) {
if ( ! c$ldap$searches[message_id]?$scopes )
c$ldap$searches[message_id]$scopes = set();
add c$ldap$searches[message_id]$scopes[SEARCH_SCOPES[scope]];
}
if ( deref != LDAP::SearchDerefAlias_Undef ) {
if ( ! c$ldap$searches[message_id]?$derefs )
c$ldap$searches[message_id]$derefs = set();
add c$ldap$searches[message_id]$derefs[SEARCH_DEREF_ALIASES[deref]];
}
if ( base_object != "" ) {
if ( ! c$ldap$searches[message_id]?$base_objects )
c$ldap$searches[message_id]$base_objects = vector();
c$ldap$searches[message_id]$base_objects += base_object;
}
c$ldap$searches[message_id]$filter = filter;
if ( default_log_search_attributes ) {
c$ldap$searches[message_id]$attributes = attributes;
}
}
#############################################################################
event LDAP::searchres(c: connection,
message_id: int,
object_name: string) {
set_session(c, message_id, LDAP::ProtocolOpcode_SEARCH_RESULT_ENTRY);
c$ldap$searches[message_id]$result_count += 1;
}
#############################################################################
event LDAP::bindreq(c: connection,
message_id: int,
version: int,
name: string,
authType: LDAP::BindAuthType,
authInfo: string) {
set_session(c, message_id, LDAP::ProtocolOpcode_BIND_REQUEST);
if ( ! c$ldap$messages[message_id]?$version )
c$ldap$messages[message_id]$version = version;
if ( ! c$ldap$messages[message_id]?$opcodes )
c$ldap$messages[message_id]$opcodes = set();
if (authType == LDAP::BindAuthType_BIND_AUTH_SIMPLE) {
add c$ldap$messages[message_id]$opcodes[BIND_SIMPLE];
} else if (authType == LDAP::BindAuthType_BIND_AUTH_SASL) {
add c$ldap$messages[message_id]$opcodes[BIND_SASL];
}
}
#############################################################################
hook finalize_ldap(c: connection) {
# log any "pending" unlogged LDAP messages/searches
if ( c$ldap?$messages && (|c$ldap$messages| > 0) ) {
for ( [mid], m in c$ldap$messages ) {
if (mid > 0) {
if ((BIND_SIMPLE in m$opcodes) || (BIND_SASL in m$opcodes)) {
# don't have both "bind" and "bind <method>" in the operations list
delete m$opcodes[PROTOCOL_OPCODES[LDAP::ProtocolOpcode_BIND_REQUEST]];
}
Log::write(LDAP::LDAP_LOG, m);
}
}
delete c$ldap$messages;
}
if ( c$ldap?$searches && (|c$ldap$searches| > 0) ) {
for ( [mid], s in c$ldap$searches ) {
if (mid > 0) {
Log::write(LDAP::LDAP_SEARCH_LOG, s);
}
}
delete c$ldap$searches;
}
}

View file

@ -16,6 +16,7 @@ add_subdirectory(ident)
add_subdirectory(imap)
add_subdirectory(irc)
add_subdirectory(krb)
add_subdirectory(ldap)
add_subdirectory(login)
add_subdirectory(mime)
add_subdirectory(modbus)

View file

@ -0,0 +1,5 @@
spicy_add_analyzer(
NAME LDAP
PACKAGE_NAME spicy-ldap
SOURCES ldap.spicy ldap.evt asn1.spicy
MODULES LDAP ASN1)

View file

@ -0,0 +1,278 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
module ASN1;
###############################################################################
# ASN.1 structure decoding
#
# A Layman's Guide to a Subset of ASN.1, BER, and DER
# http://luca.ntop.org/Teaching/Appunti/asn1.html
#
# ASN.1 Tutorial from Computer Networks and Open Systems:
# An Application Development Perspective
# https://www.obj-sys.com/asn1tutorial/asn1only.html
#
# The ASN1JS tool (http://lapo.it/asn1js and https://github.com/lapo-luchini/asn1js)
# is invaluable in debugging ASN.1
###############################################################################
import spicy;
#- ASN.1 data types ----------------------------------------------------------
# https://www.obj-sys.com/asn1tutorial/node124.html
# https://www.obj-sys.com/asn1tutorial/node10.html
public type ASN1Type = enum {
Boolean = 1,
Integer = 2,
BitString = 3,
OctetString = 4,
NullVal = 5,
ObjectIdentifier = 6,
ObjectDescriptor = 7,
InstanceOf = 8,
Real = 9,
Enumerated = 10,
EmbeddedPDV = 11,
UTF8String = 12,
RelativeOID = 13,
Sequence = 16,
Set = 17,
NumericString = 18,
PrintableString = 19,
TeletextString = 20,
VideotextString = 21,
IA5String = 22,
UTCTime = 23,
GeneralizedTime = 24,
GraphicString = 25,
VisibleString = 26,
GeneralString = 27,
UniversalString = 28,
CharacterString = 29,
BMPString = 30
};
#- ASN.1 data classes --------------------------------------------------------
public type ASN1Class = enum {
Universal = 0,
Application = 1,
ContextSpecific = 2,
Private = 3
};
#- ASN.1 tag definition (including length) ------------------------------------
type LengthType = unit {
var len: uint64;
var tag_len: uint8;
data : bitfield(8) {
num: 0..6;
islong: 7;
};
switch ( self.data.islong ) {
0 -> : void {
self.len = self.data.num;
self.tag_len = 1;
}
1 -> : bytes &size=self.data.num
&convert=$$.to_uint(spicy::ByteOrder::Network) {
self.len = $$;
self.tag_len = self.data.num + 1;
}
};
};
type ASN1Tag = unit {
: bitfield(8) {
type_: 0..4 &convert=ASN1Type($$);
constructed: 5 &convert=cast<bool>($$);
class: 6..7 &convert=ASN1Class($$);
};
};
#- ASN.1 bit string -----------------------------------------------------------
# https://www.obj-sys.com/asn1tutorial/node10.html
type ASN1BitString = unit(len: uint64, constructed: bool) {
: uint8; # unused bits
value_bits: bytes &size=(len - 1);
# TODO - constructed form
# https://github.com/zeek/spicy/issues/921
# `bytes` needs << and >> support before we can implement complex bitstrings
#
};
#- ASN.1 octet string ---------------------------------------------------------
# https://www.obj-sys.com/asn1tutorial/node10.html
type ASN1OctetString = unit(len: uint64, constructed: bool) {
value: bytes &size = len;
# TODO - constructed form
};
#- ASN.1 various string types -------------------------------------------------
# https://www.obj-sys.com/asn1tutorial/node124.html
type ASN1String = unit(tag: ASN1Tag, len: uint64) {
var encoding: hilti::Charset;
on %init {
switch ( tag.type_ ) {
# see "Restricted Character String Types" in
# "Generic String Encoding Rules (GSER) for ASN.1 Types"
# (https://datatracker.ietf.org/doc/html/rfc3641#section-3.2)
case ASN1Type::PrintableString,
ASN1Type::GeneralizedTime,
ASN1Type::UTCTime: {
self.encoding = hilti::Charset::ASCII;
}
case ASN1Type::UTF8String,
ASN1Type::GeneralString,
ASN1Type::CharacterString,
ASN1Type::GraphicString,
ASN1Type::IA5String,
ASN1Type::NumericString,
ASN1Type::TeletextString,
ASN1Type::VideotextString,
ASN1Type::VisibleString,
# TODO: RFC3641 mentions special UTF-8 mapping rules for
# BMPString and UniversalString. This *may* not be correct.
ASN1Type::BMPString,
ASN1Type::UniversalString: {
self.encoding = hilti::Charset::UTF8;
}
}
}
value: ASN1OctetString(len, tag.constructed) &convert=$$.value.decode(self.encoding);
} &convert=self.value;
#- ASN.1 OID ------------------------------------------------------------------
# https://www.obj-sys.com/asn1tutorial/node124.html
type ASN1ObjectIdentifierNibble = unit {
data : bitfield(8) {
num: 0..6;
more: 7;
};
} &convert=self.data;
type ASN1ObjectIdentifier = unit(len: uint64) {
var oid: vector<uint64>;
var temp: uint64;
var oidstring: string;
: uint8 if ( len >= 1 ) {
self.temp = $$ / 40;
self.oid.push_back( self.temp );
self.oidstring = "%d" % (self.temp);
self.temp = $$ % 40;
self.oid.push_back( self.temp );
self.oidstring = self.oidstring + ".%d" % (self.temp);
self.temp = 0;
}
sublist: ASN1ObjectIdentifierNibble[len - 1] foreach {
self.temp = ( self.temp<<7 ) | $$.num;
if ( $$.more != 1 ) {
self.oid.push_back(self.temp);
self.oidstring = self.oidstring + ".%d" % (self.temp);
self.temp = 0;
}
}
};
#- ASN.1 message header (tag + length information) ----------------------------
public type ASN1Header = unit {
tag: ASN1Tag;
len: LengthType;
};
#- ASN.1 message body ---------------------------------------------------------
public type ASN1Body = unit(head: ASN1Header, recursive: bool) {
switch ( head.tag.type_ ) {
ASN1Type::Boolean -> bool_value: uint8 &convert=cast<bool>($$) &requires=head.len.len==1;
ASN1Type::Integer,
ASN1Type::Enumerated -> num_value: bytes &size=head.len.len
&convert=$$.to_int(spicy::ByteOrder::Big);
ASN1Type::NullVal -> null_value: bytes &size=0 &requires=head.len.len==0;
ASN1Type::BitString -> bitstr_value: ASN1BitString(head.len.len, head.tag.constructed);
ASN1Type::OctetString -> str_value: ASN1OctetString(head.len.len, head.tag.constructed)
&convert=$$.value.decode(hilti::Charset::ASCII);
ASN1Type::ObjectIdentifier -> str_value: ASN1ObjectIdentifier(head.len.len)
&convert=$$.oidstring;
ASN1Type::BMPString,
ASN1Type::CharacterString,
ASN1Type::GeneralizedTime,
ASN1Type::GeneralString,
ASN1Type::GraphicString,
ASN1Type::IA5String,
ASN1Type::NumericString,
ASN1Type::PrintableString,
ASN1Type::TeletextString,
ASN1Type::UTCTime,
ASN1Type::UTF8String,
ASN1Type::VideotextString,
ASN1Type::VisibleString,
ASN1Type::UniversalString -> str_value: ASN1String(head.tag, head.len.len);
ASN1Type::Sequence, ASN1Type::Set -> seq: ASN1SubMessages(head.len.len) if (recursive);
# TODO: ASN1Type values not handled yet
ASN1Type::ObjectDescriptor,
ASN1Type::InstanceOf,
ASN1Type::Real,
ASN1Type::EmbeddedPDV,
ASN1Type::RelativeOID -> unimplemented_value: bytes &size=head.len.len;
# unknown (to me) ASN.1 enumeration, skip over silently
* -> unimplemented_value: bytes &size=head.len.len;
};
};
#- ASN.1 array of ASN.1 sequence/set sub-messages (up to msgLen bytes) --------
public type ASN1SubMessages = unit(msgLen: uint64) {
submessages: ASN1Message(True)[] &eod;
} &size=msgLen;
#- ASN.1 message with header and body -----------------------------------------
# Universal or Application/ContextSpecific/Private
# - if Universal, body:ASN1Body is parsed
# - else, application_data:bytes stores data array
public type ASN1Message = unit(recursive: bool) {
var application_id: int32;
head: ASN1Header;
switch ( self.head.tag.class ) {
ASN1Class::Universal -> body: ASN1Body(self.head, recursive);
ASN1Class::Application,
ASN1Class::ContextSpecific,
ASN1Class::Private -> application_data: bytes &size=self.head.len.len {
self.application_id = cast<int32>(self.head.tag.type_);
}
};
};

View file

@ -0,0 +1,40 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
protocol analyzer LDAP_TCP over TCP:
parse with LDAP::Messages;
protocol analyzer LDAP_UDP over UDP:
parse with LDAP::Messages;
import LDAP;
on LDAP::Message -> event LDAP::message($conn,
self.messageID,
self.opcode,
self.result.code,
self.result.matchedDN,
self.result.diagnosticMessage,
self.obj,
self.arg);
on LDAP::BindRequest -> event LDAP::bindreq($conn,
message.messageID,
self.version,
self.name,
self.authType,
message.arg);
on LDAP::SearchRequest -> event LDAP::searchreq($conn,
message.messageID,
self.baseObject,
self.scope,
self.deref,
self.sizeLimit,
self.timeLimit,
self.typesOnly,
self.filter,
self.attributes);
on LDAP::SearchResultEntry -> event LDAP::searchres($conn,
message.messageID,
self.objectName);

View file

@ -0,0 +1,875 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
module LDAP;
import ASN1;
import spicy;
# https://tools.ietf.org/html/rfc4511#
# https://ldap.com/ldapv3-wire-protocol-reference-asn1-ber/
# https://lapo.it/asn1js
#- Operation opcode ----------------------------------------------------------
public type ProtocolOpcode = enum {
BIND_REQUEST = 0,
BIND_RESPONSE = 1,
UNBIND_REQUEST = 2,
SEARCH_REQUEST = 3,
SEARCH_RESULT_ENTRY = 4,
SEARCH_RESULT_DONE = 5,
MODIFY_REQUEST = 6,
MODIFY_RESPONSE = 7,
ADD_REQUEST = 8,
ADD_RESPONSE = 9,
DEL_REQUEST = 10,
DEL_RESPONSE = 11,
MOD_DN_REQUEST = 12,
MOD_DN_RESPONSE = 13,
COMPARE_REQUEST = 14,
COMPARE_RESPONSE = 15,
ABANDON_REQUEST = 16,
SEARCH_RESULT_REFERENCE = 19,
EXTENDED_REQUEST = 23,
EXTENDED_RESPONSE = 24,
INTERMEDIATE_RESPONSE = 25,
};
#- Result code ---------------------------------------------------------------
public type ResultCode = enum {
SUCCESS = 0,
OPERATIONS_ERROR = 1,
PROTOCOL_ERROR = 2,
TIME_LIMIT_EXCEEDED = 3,
SIZE_LIMIT_EXCEEDED = 4,
COMPARE_FALSE = 5,
COMPARE_TRUE = 6,
AUTH_METHOD_NOT_SUPPORTED = 7,
STRONGER_AUTH_REQUIRED = 8,
PARTIAL_RESULTS = 9,
REFERRAL = 10,
ADMIN_LIMIT_EXCEEDED = 11,
UNAVAILABLE_CRITICAL_EXTENSION = 12,
CONFIDENTIALITY_REQUIRED = 13,
SASL_BIND_IN_PROGRESS = 14,
NO_SUCH_ATTRIBUTE = 16,
UNDEFINED_ATTRIBUTE_TYPE = 17,
INAPPROPRIATE_MATCHING = 18,
CONSTRAINT_VIOLATION = 19,
ATTRIBUTE_OR_VALUE_EXISTS = 20,
INVALID_ATTRIBUTE_SYNTAX = 21,
NO_SUCH_OBJECT = 32,
ALIAS_PROBLEM = 33,
INVALID_DNSYNTAX = 34,
ALIAS_DEREFERENCING_PROBLEM = 36,
INAPPROPRIATE_AUTHENTICATION = 48,
INVALID_CREDENTIALS = 49,
INSUFFICIENT_ACCESS_RIGHTS = 50,
BUSY = 51,
UNAVAILABLE = 52,
UNWILLING_TO_PERFORM = 53,
LOOP_DETECT = 54,
SORT_CONTROL_MISSING = 60,
OFFSET_RANGE_ERROR = 61,
NAMING_VIOLATION = 64,
OBJECT_CLASS_VIOLATION = 65,
NOT_ALLOWED_ON_NON_LEAF = 66,
NOT_ALLOWED_ON_RDN = 67,
ENTRY_ALREADY_EXISTS = 68,
OBJECT_CLASS_MODS_PROHIBITED = 69,
RESULTS_TOO_LARGE = 70,
AFFECTS_MULTIPLE_DSAS = 71,
CONTROL_ERROR = 76,
OTHER = 80,
SERVER_DOWN = 81,
LOCAL_ERROR = 82,
ENCODING_ERROR = 83,
DECODING_ERROR = 84,
TIMEOUT = 85,
AUTH_UNKNOWN = 86,
FILTER_ERROR = 87,
USER_CANCELED = 88,
PARAM_ERROR = 89,
NO_MEMORY = 90,
CONNECT_ERROR = 91,
NOT_SUPPORTED = 92,
CONTROL_NOT_FOUND = 93,
NO_RESULTS_RETURNED = 94,
MORE_RESULTS_TO_RETURN = 95,
CLIENT_LOOP = 96,
REFERRAL_LIMIT_EXCEEDED = 97,
INVALID_RESPONSE = 100,
AMBIGUOUS_RESPONSE = 101,
TLS_NOT_SUPPORTED = 112,
INTERMEDIATE_RESPONSE = 113,
UNKNOWN_TYPE = 114,
LCUP_INVALID_DATA = 115,
LCUP_UNSUPPORTED_SCHEME = 116,
LCUP_RELOAD_REQUIRED = 117,
CANCELED = 118,
NO_SUCH_OPERATION = 119,
TOO_LATE = 120,
CANNOT_CANCEL = 121,
ASSERTION_FAILED = 122,
AUTHORIZATION_DENIED = 123,
};
#-----------------------------------------------------------------------------
public type Result = unit {
code: ASN1::ASN1Message(True) &convert=cast<ResultCode>(cast<uint8>($$.body.num_value))
&default=ResultCode::Undef;
matchedDN: ASN1::ASN1Message(True) &convert=$$.body.str_value
&default="";
diagnosticMessage: ASN1::ASN1Message(True) &convert=$$.body.str_value
&default="";
# TODO: if we want to parse referral URIs in result
# https://tools.ietf.org/html/rfc4511#section-4.1.10
};
#-----------------------------------------------------------------------------
public type Messages = unit {
: MessageWrapper[];
};
#-----------------------------------------------------------------------------
type SASLLayer = unit {
# For the time being (before we support parsing the SASL layer) this unit
# is used by MessageWrapper below to strip it (SASL) so that the parser
# can attempt to resume parsing afterward. It also sets the success flag
# if '\x30' is found, otherwise backtracks so that we can deal with encrypted
# SASL payloads without raising a parse error.
var success: bool = False;
: bytes &until=b"\x30" {
self.success = True;
}
on %error {
self.backtrack();
}
};
#-----------------------------------------------------------------------------
public type MessageWrapper = unit {
# A wrapper around 'Message'. First, we try to parse a Message unit.
# There are two possible outcomes:
# (1) Success -> We consumed all bytes and successfully parsed a Message unit
# (2) No success -> self.backtrack() is called in the Message unit,
# so effectively we didn't consume any bytes yet.
# The outcome can be determined by checking the `success` variable of the Message unit
# This success variable is different, because this keeps track of the status for the MessageWrapper object
var success: bool = False;
var message: Message;
# Here, we try to parse the message...
: Message &try {
# ... and only if the Message unit successfully parsed, we can set
# the status of this MessageWrapper's success to 'True'
if ( $$.success == True ) {
self.success = True;
self.message = $$;
}
}
# If we failed to parse the message, then we're going to scan the remaining bytes for the '\x30'
# start byte and try to parse a Message starting from that byte. This effectively
# strips the SASL layer if SASL Signing was enabled. Until now, I haven't found A
# better way to scan / determine the exact SASL header length yet, so we'll stick with this
# for the time being. If the entire LDAP packet was encrypted with SASL, then we skip parsing for
# now (in the long run we need to be parsing SASL/GSSAPI instead, in which case encrypted payloads
# are just another message type).
# SASLLayer (see unit above) just consumes bytes &until=b"\x30" or backtracks if it isn't found
# and sets a success flag we can use later to decide if those bytes contain a parsable message.
var sasl_success: bool = False;
: SASLLayer &try if ( self.success == False ) {
if ( $$.success == True ) {
self.sasl_success = True;
}
}
var remainder: bytes;
# SASLLayer consumes the delimiter ('\x30'), and because this is the first byte of a valid LDAP message
# we should re-add it to the remainder if the delimiter was found. If the delimiter was not found, we
# leave the remainder empty, but note that the bytes must be consumed either way to avoid stalling the
# parser and causing an infinite loop error.
: bytes &eod if ( self.success == False ) {
if ( self.sasl_success == True ) {
self.remainder = b"\x30" + $$;
}
}
# Again, try to parse a Message unit. Be aware that in this will sometimes fail if the '\x30' byte is
# also present in the SASL header.
# Also, we could try to do this recursively or try a few iterations, but for now I would suggest
# to try this extra parsing once to get the best cost/benefit tradeoff.
: Message &try &parse-from=self.remainder if ( self.success == False && self.sasl_success == True ) {
if ( $$.success == True ) {
self.success = True;
self.message = $$;
}
}
# If we still didn't manage to parse a message (so the &try resulted in another backtrack()) then
# this is probably an encrypted LDAP message, so skip it
} &convert=self.message;
#-----------------------------------------------------------------------------
public type Message = unit {
var messageID: int64;
var opcode: ProtocolOpcode = ProtocolOpcode::Undef;
var applicationBytes: bytes;
var unsetResultDefault: Result;
var result: Result& = self.unsetResultDefault;
var obj: string = "";
var arg: string = "";
var success: bool = False;
: ASN1::ASN1Message(True) {
if (($$.head.tag.type_ == ASN1::ASN1Type::Sequence) &&
($$.body?.seq) &&
(|$$.body.seq.submessages| >= 2)) {
if ($$.body.seq.submessages[0].body?.num_value) {
self.messageID = $$.body.seq.submessages[0].body.num_value;
}
if ($$.body.seq.submessages[1]?.application_id) {
self.opcode = cast<ProtocolOpcode>(cast<uint8>($$.body.seq.submessages[1].application_id));
self.applicationBytes = $$.body.seq.submessages[1].application_data;
}
}
}
switch ( self.opcode ) {
ProtocolOpcode::BIND_REQUEST -> BIND_REQUEST: BindRequest(self);
ProtocolOpcode::BIND_RESPONSE -> BIND_RESPONSE: BindResponse(self);
ProtocolOpcode::UNBIND_REQUEST -> UNBIND_REQUEST: UnbindRequest(self);
ProtocolOpcode::SEARCH_REQUEST -> SEARCH_REQUEST: SearchRequest(self);
ProtocolOpcode::SEARCH_RESULT_ENTRY -> SEARCH_RESULT_ENTRY: SearchResultEntry(self);
ProtocolOpcode::SEARCH_RESULT_DONE -> SEARCH_RESULT_DONE: SearchResultDone(self);
ProtocolOpcode::MODIFY_REQUEST -> MODIFY_REQUEST: ModifyRequest(self);
ProtocolOpcode::MODIFY_RESPONSE -> MODIFY_RESPONSE: ModifyResponse(self);
ProtocolOpcode::ADD_RESPONSE -> ADD_RESPONSE: AddResponse(self);
ProtocolOpcode::DEL_REQUEST -> DEL_REQUEST: DelRequest(self);
ProtocolOpcode::DEL_RESPONSE -> DEL_RESPONSE: DelResponse(self);
ProtocolOpcode::MOD_DN_RESPONSE -> MOD_DN_RESPONSE: ModDNResponse(self);
ProtocolOpcode::COMPARE_RESPONSE -> COMPARE_RESPONSE: CompareResponse(self);
ProtocolOpcode::ABANDON_REQUEST -> ABANDON_REQUEST: AbandonRequest(self);
# TODO: not yet implemented, redirect to NotImplemented because when we're
# just commenting this out, it will stop processing LDAP Messages in this connection
ProtocolOpcode::ADD_REQUEST -> ADD_REQUEST: NotImplemented(self);
ProtocolOpcode::COMPARE_REQUEST -> COMPARE_REQUEST: NotImplemented(self);
ProtocolOpcode::EXTENDED_REQUEST -> EXTENDED_REQUEST: NotImplemented(self);
ProtocolOpcode::EXTENDED_RESPONSE -> EXTENDED_RESPONSE: NotImplemented(self);
ProtocolOpcode::INTERMEDIATE_RESPONSE -> INTERMEDIATE_RESPONSE: NotImplemented(self);
ProtocolOpcode::MOD_DN_REQUEST -> MOD_DN_REQUEST: NotImplemented(self);
ProtocolOpcode::SEARCH_RESULT_REFERENCE -> SEARCH_RESULT_REFERENCE: NotImplemented(self);
} &parse-from=self.applicationBytes if ( self.opcode );
on %error {
self.backtrack();
}
on %done {
self.success = True;
}
} &requires=((self?.messageID) && (self?.opcode) && (self.opcode != ProtocolOpcode::Undef));
#-----------------------------------------------------------------------------
# Bind Operation
# https://tools.ietf.org/html/rfc4511#section-4.2
public type BindAuthType = enum {
BIND_AUTH_SIMPLE = 0,
BIND_AUTH_SASL = 3,
};
type SaslCredentials = unit() {
mechanism: ASN1::ASN1Message(True) &convert=$$.body.str_value;
# TODO: if we want to parse the (optional) credentials string
};
# TODO(fox-ds): A helper unit for requests for which no handling has been implemented.
# Eventually all uses of this unit should be replaced with actual parsers so this unit can be removed.
type NotImplemented = unit(inout message: Message) {
# Do nothing
};
type BindRequest = unit(inout message: Message) {
version: ASN1::ASN1Message(True) &convert=$$.body.num_value;
name: ASN1::ASN1Message(True) &convert=$$.body.str_value {
message.obj = self.name;
}
var authType: BindAuthType = BindAuthType::Undef;
var authData: bytes = b"";
var simpleCreds: string = "";
: ASN1::ASN1Message(True) {
if ($$?.application_id) {
self.authType = cast<BindAuthType>(cast<uint8>($$.application_id));
self.authData = $$.application_data;
}
if ((self.authType == BindAuthType::BIND_AUTH_SIMPLE) && (|self.authData| > 0)) {
self.simpleCreds = self.authData.decode();
if (|self.simpleCreds| > 0) {
message.arg = self.simpleCreds;
}
}
}
saslCreds: SaslCredentials() &parse-from=self.authData if ((self.authType == BindAuthType::BIND_AUTH_SASL) &&
(|self.authData| > 0)) {
message.arg = self.saslCreds.mechanism;
}
} &requires=((self?.authType) && (self.authType != BindAuthType::Undef));
type BindResponse = unit(inout message: Message) {
: Result {
message.result = $$;
}
# TODO: if we want to parse SASL credentials returned
};
#-----------------------------------------------------------------------------
# Unbind Operation
# https://tools.ietf.org/html/rfc4511#section-4.3
type UnbindRequest = unit(inout message: Message) {
# this page intentionally left blank
};
#-----------------------------------------------------------------------------
# Search Operation
# https://tools.ietf.org/html/rfc4511#section-4.5
public type SearchScope = enum {
SEARCH_BASE = 0,
SEARCH_SINGLE = 1,
SEARCH_TREE = 2,
};
public type SearchDerefAlias = enum {
DEREF_NEVER = 0,
DEREF_IN_SEARCHING = 1,
DEREF_FINDING_BASE = 2,
DEREF_ALWAYS = 3,
};
type FilterType = enum {
FILTER_AND = 0,
FILTER_OR = 1,
FILTER_NOT = 2,
FILTER_EQ = 3,
FILTER_SUBSTR = 4,
FILTER_GE = 5,
FILTER_LE = 6,
FILTER_PRESENT = 7,
FILTER_APPROX = 8,
FILTER_EXT = 9,
FILTER_INVALID = 254,
};
public type AttributeSelection = unit {
var attributes: vector<string>;
# TODO: parse AttributeSelection as per
# https://tools.ietf.org/html/rfc4511#section-4.5.1
# and decide how deep that should be fleshed out.
: ASN1::ASN1Message(True) {
if (($$.head.tag.type_ == ASN1::ASN1Type::Sequence) &&
($$.body?.seq)) {
for (i in $$.body.seq.submessages) {
if (i.body?.str_value) {
self.attributes.push_back(i.body.str_value);
}
}
}
}
};
type AttributeValueAssertion = unit {
var desc: string = "";
var val: string = "";
: ASN1::ASN1Message(True) {
if (($$.head.tag.type_ == ASN1::ASN1Type::Sequence) &&
($$.body?.seq) &&
(|$$.body.seq.submessages| >= 2)) {
if ($$.body.seq.submessages[0].body?.str_value) {
self.desc = $$.body.seq.submessages[0].body.str_value;
}
if ($$.body.seq.submessages[1].body?.str_value) {
self.val = $$.body.seq.submessages[1].body.str_value;
}
}
}
};
# An AND or OR search filter can consist of many sub-searchfilters, so we try to parse these
type ParseNestedAndOr = unit {
searchfilters: SearchFilter[] &eod;
};
type ParseNestedNot = unit {
searchfilter: SearchFilter;
};
# Helper functions to properly format some custom data structures
public function utf16_guid_to_hex_repr(bts: bytes) : string {
# Rather ugly workaround to pretty-print the CLDAP DomainGuid UTF16-LE encoded string
# in the same format as Wireshark (aabbccdd-eeff-gghh-iijj-kkllmmnnoopp)
# We need to have exactly 16 bytes...
if ( |bts| != 16 ) {
# ... and otherwise just return an error code
return "GUID_FORMAT_FAILED";
}
local ret = "";
for ( i in [[3, 2, 1, 0], [5, 4], [7, 6], [8, 9], [10, 11, 12, 13, 14, 15]] ) {
for ( j in i ) {
local bt: uint8 = *bts.at(j);
ret = ret + "%02x" % bt;
if ( j in [0, 4, 6, 9] ) {
ret = ret + "-";
}
}
}
return ret;
}
public function bytes_sid_to_hex_repr(bts: bytes) : string {
local ret = "";
local cnt = 0;
while ( cnt < |bts| ) {
local bt: uint8 = *bts.at(cnt);
ret = ret + "%02x" % bt;
if ( cnt < |bts|-1 ) {
ret = ret + ":";
}
cnt += 1;
}
return ret;
}
public function bytes_sid_to_SID_repr(bts: bytes) : string {
# Example: SID -> S-1-5-21-1153942841-488947194-1912431946
# Needs to be exactly 24 bytes
if ( |bts| != 24 ) {
# ... and otherwise just return an error code
return "SID_FORMAT_FAILED";
}
local ret = "S-";
local cnt = 0;
# Mixed little and big endian, so turn everything to big endian first...
# Byte 1 seems to be skipped when parsing the SID
for ( i in [[0], [2, 3, 4, 5, 6, 7], [11, 10, 9, 8], [15, 14, 13, 12], [19, 18, 17, 16], [23, 22, 21, 20]] ) {
local dec_val_rep: bytes = b"";
for ( j in i ) {
local bt: uint8 = *bts.at(j);
dec_val_rep += bt;
cnt += 1;
}
# ... so we can represent this integer value in big endian
ret = ret + "%u" % dec_val_rep.to_uint(spicy::ByteOrder::Big);
# Only print the dash when we're not at the end
if ( cnt < 23 ) {
ret = ret + "-";
}
}
return ret;
}
public function uint32_to_hex_repr(bts: bytes) : string {
# Needs to be exactly 4 bytes
if ( |bts| != 4 ) {
# ... and otherwise just return an error code
return "HEX_FORMAT_FAILED";
}
# Workaround to print the hex value of an uint32, prepended with '0x'
local ret = "0x";
for ( i in [3, 2, 1, 0] ) {
local bt: uint8 = *bts.at(i);
ret = ret + "%02x" % bt;
}
return ret;
}
# Helper to compute a string representation of a `SearchFilter`.
public function string_representation(search_filter: SearchFilter): string {
local repr: string;
switch ( local fType = search_filter.filterType ) {
# The NOT, AND and OR filter types are trees and may hold many leaf nodes. So recursively get
# the stringPresentations for the leaf nodes and add them all in one final statement.
case FilterType::FILTER_NOT: {
repr = "(!%s)" % search_filter.FILTER_NOT.searchfilter.stringRepresentation;
}
case FilterType::FILTER_AND, FilterType::FILTER_OR: {
local nestedObj: ParseNestedAndOr;
local printChar = "";
if ( fType == FilterType::FILTER_AND ) {
printChar = "&";
nestedObj = search_filter.FILTER_AND;
} else {
printChar = "|";
nestedObj = search_filter.FILTER_OR;
}
# Build the nested AND/OR statement in this loop. When we encounter the first element,
# we open the statement. At the second element, we close our first complete statement. For every
# following statement, we extend the AND/OR statement by wrapping it around the already completed
# statement. Although it is also valid to not do this wrapping, which is logically equivalent, e.g:
#
# (1) (2)
# (?(a=b)(c=d)(e=f)) vs (?(?(a=b)(c=d))(e=f))
#
# the latter version is also shown by Wireshark. So although the parsed structure actually represents
# version (1) of the query, we now choose to print version (2). If this is not desirable, swap the code
# for the following:
#
# # Construct the nested structure, like (1)
# for ( SF in nestedObj.searchfilters ) {
# self.stringRepresentation = self.stringRepresentation + SF.stringRepresentation
# }
# # Close it with brackets and put the correct printChar for AND/OR in the statement
# self.stringRepresentation = "(%s" % printChar + self.stringRepresentation + ")";
#
local i = 0;
for ( searchFilter in nestedObj.searchfilters ) {
switch ( i ) {
case 0: {
repr = "(%s%s%s" % (
printChar,
searchFilter.stringRepresentation,
# If we have exactly one element immediately close the statement since we are done.
|nestedObj.searchfilters| == 1 ? ")" : ""
);
}
case 1: {
repr = repr + searchFilter.stringRepresentation + ")";
}
default: {
repr = "(%s" % printChar + repr + searchFilter.stringRepresentation + ")";
}
}
i += 1;
}
}
# The following FilterTypes are leaf nodes and can thus be represented in a statement
case FilterType::FILTER_EXT: {
# For extended search filters the meaning of the individual fields in
# `DecodedAttributeValue` is slightly different.
repr = "(%s:%s:=%s)" % (search_filter.FILTER_EXT.assertionValueDecoded,
search_filter.FILTER_EXT.attributeDesc.decode(),
search_filter.FILTER_EXT.matchValue);
}
case FilterType::FILTER_APPROX: {
repr = "(%s~=%s)" % (search_filter.FILTER_APPROX.attributeDesc.decode(),
search_filter.FILTER_APPROX.assertionValueDecoded);
}
case FilterType::FILTER_EQ: {
repr = "(%s=%s)" % (search_filter.FILTER_EQ.attributeDesc.decode(),
search_filter.FILTER_EQ.assertionValueDecoded);
}
case FilterType::FILTER_GE: {
repr = "(%s>=%s)" % (search_filter.FILTER_GE.attributeDesc.decode(),
search_filter.FILTER_GE.assertionValueDecoded);
}
case FilterType::FILTER_LE: {
repr = "(%s<=%s)" % (search_filter.FILTER_LE.attributeDesc.decode(),
search_filter.FILTER_LE.assertionValueDecoded);
}
case FilterType::FILTER_SUBSTR: {
repr = "(%s=*%s*)" % (search_filter.FILTER_SUBSTR.attributeDesc.decode(),
search_filter.FILTER_SUBSTR.assertionValueDecoded);
}
case FilterType::FILTER_PRESENT: {
repr = "(%s=*)" % search_filter.FILTER_PRESENT;
}
}
return repr;
}
# Represents an (extended) key-value pair present in SearchFilters
type DecodedAttributeValue = unit(fType: FilterType) {
var assertionValueDecoded: string = "";
: uint8;
attributeDesc_len: uint8;
attributeDesc: bytes &size=self.attributeDesc_len;
# For some reason, two intermediate uint8 values are present in the FILTER_SUBSTR type.
: uint8 if ( fType == FilterType::FILTER_SUBSTR );
: uint8 if ( fType == FilterType::FILTER_SUBSTR );
: uint8;
assertionValue_len: uint8;
assertionValue: bytes &size=self.assertionValue_len;
# Only for the FILTER_EXT type, parse extra fields
: uint8 if ( fType == FilterType::FILTER_EXT );
matchValue_len: uint8 if( fType == FilterType::FILTER_EXT );
matchValue: bytes &size=self.matchValue_len if ( fType == FilterType::FILTER_EXT );
on %done {
switch ( self.attributeDesc ) {
# Special parsing required for some CLDAP attributes,
# see https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/895a7744-aff3-4f64-bcfa-f8c05915d2e9
case b"DomainGuid": {
self.assertionValueDecoded = utf16_guid_to_hex_repr(self.assertionValue);
}
case b"objectSid", b"AAC": {
self.assertionValueDecoded = bytes_sid_to_hex_repr(self.assertionValue);
}
case b"DomainSid": {
self.assertionValueDecoded = bytes_sid_to_SID_repr(self.assertionValue);
}
case b"NtVer": {
self.assertionValueDecoded = uint32_to_hex_repr(self.assertionValue);
}
# By default, decode with UTF-8
default: {
self.assertionValueDecoded = self.assertionValue.decode();
}
}
}
};
type SearchFilter = unit {
var filterType: FilterType = FilterType::Undef;
var filterBytes: bytes = b"";
var filterLen: uint64 = 0;
var stringRepresentation: string = "";
: ASN1::ASN1Message(True) {
if ($$?.application_id) {
self.filterType = cast<FilterType>(cast<uint8>($$.application_id));
self.filterBytes = $$.application_data;
self.filterLen = $$.head.len.len;
} else {
self.filterType = FilterType::FILTER_INVALID;
}
}
switch ( self.filterType ) {
# FilterTypes that hold one or more SearchFilters inside them
FilterType::FILTER_AND -> FILTER_AND: ParseNestedAndOr()
&parse-from=self.filterBytes;
FilterType::FILTER_OR -> FILTER_OR: ParseNestedAndOr()
&parse-from=self.filterBytes;
FilterType::FILTER_NOT -> FILTER_NOT: ParseNestedNot()
&parse-from=self.filterBytes;
# FilterTypes that we can actually convert to a string
FilterType::FILTER_EQ -> FILTER_EQ: DecodedAttributeValue(FilterType::FILTER_EQ)
&parse-from=self.filterBytes;
FilterType::FILTER_SUBSTR -> FILTER_SUBSTR: DecodedAttributeValue(FilterType::FILTER_SUBSTR)
&parse-from=self.filterBytes;
FilterType::FILTER_GE -> FILTER_GE: DecodedAttributeValue(FilterType::FILTER_GE)
&parse-from=self.filterBytes;
FilterType::FILTER_LE -> FILTER_LE: DecodedAttributeValue(FilterType::FILTER_LE)
&parse-from=self.filterBytes;
FilterType::FILTER_APPROX -> FILTER_APPROX: DecodedAttributeValue(FilterType::FILTER_APPROX)
&parse-from=self.filterBytes;
FilterType::FILTER_EXT -> FILTER_EXT: DecodedAttributeValue(FilterType::FILTER_EXT)
&parse-from=self.filterBytes;
FilterType::FILTER_PRESENT -> FILTER_PRESENT: ASN1::ASN1OctetString(self.filterLen, False)
&convert=$$.value.decode(hilti::Charset::ASCII)
&parse-from=self.filterBytes;
};
# So when you're done with recursively parsing the filters, we can now leverage the tree structure to
# recursively get the stringRepresentations for those leafs, which are SearchFilters
on %done {
self.stringRepresentation = string_representation(self);
}
on %error {
self.stringRepresentation = "FILTER_PARSING_ERROR";
}
};
public type SearchRequest = unit(inout message: Message) {
baseObject: ASN1::ASN1Message(True) &convert=$$.body.str_value {
message.obj = self.baseObject;
}
scope: ASN1::ASN1Message(True) &convert=cast<SearchScope>(cast<uint8>($$.body.num_value))
&default=SearchScope::Undef {
message.arg = "%s" % self.scope;
}
deref: ASN1::ASN1Message(True) &convert=cast<SearchDerefAlias>(cast<uint8>($$.body.num_value))
&default=SearchDerefAlias::Undef;
sizeLimit: ASN1::ASN1Message(True) &convert=$$.body.num_value &default=0;
timeLimit: ASN1::ASN1Message(True) &convert=$$.body.num_value &default=0;
typesOnly: ASN1::ASN1Message(True) &convert=$$.body.bool_value &default=False;
filter: SearchFilter &convert=$$.stringRepresentation;
attributes: AttributeSelection &convert=$$.attributes;
};
type SearchResultEntry = unit(inout message: Message) {
objectName: ASN1::ASN1Message(True) &convert=$$.body.str_value {
message.obj = self.objectName;
}
# TODO: if we want to descend down into PartialAttributeList
attributes: ASN1::ASN1Message(True);
};
type SearchResultDone = unit(inout message: Message) {
: Result {
message.result = $$;
}
};
# TODO: implement SearchResultReference
# type SearchResultReference = unit(inout message: Message) {
#
# };
#-----------------------------------------------------------------------------
# Modify Operation
# https://tools.ietf.org/html/rfc4511#section-4.6
type ModifyRequest = unit(inout message: Message) {
objectName: ASN1::ASN1Message(True) &convert=$$.body.str_value {
message.obj = self.objectName;
}
# TODO: parse changes
};
type ModifyResponse = unit(inout message: Message) {
: Result {
message.result = $$;
}
};
#-----------------------------------------------------------------------------
# Add Operation
# https://tools.ietf.org/html/rfc4511#section-4.7
# TODO: implement AddRequest
# type AddRequest = unit(inout message: Message) {
#
#
# };
type AddResponse = unit(inout message: Message) {
: Result {
message.result = $$;
}
};
#-----------------------------------------------------------------------------
# Delete Operation
# https://tools.ietf.org/html/rfc4511#section-4.8
type DelRequest = unit(inout message: Message) {
objectName: ASN1::ASN1Message(True) &convert=$$.body.str_value {
message.obj = self.objectName;
}
};
type DelResponse = unit(inout message: Message) {
: Result {
message.result = $$;
}
};
#-----------------------------------------------------------------------------
# Modify DN Operation
# https://tools.ietf.org/html/rfc4511#section-4.8
# TODO: implement ModDNRequest
# type ModDNRequest = unit(inout message: Message) {
#
# };
type ModDNResponse = unit(inout message: Message) {
: Result {
message.result = $$;
}
};
#-----------------------------------------------------------------------------
# Compare Operation
# https://tools.ietf.org/html/rfc4511#section-4.10
# TODO: implement CompareRequest
# type CompareRequest = unit(inout message: Message) {
#
# };
type CompareResponse = unit(inout message: Message) {
: Result {
message.result = $$;
}
};
#-----------------------------------------------------------------------------
# Abandon Operation
# https://tools.ietf.org/html/rfc4511#section-4.11
type AbandonRequest = unit(inout message: Message) {
messageID: ASN1::ASN1Message(True) &convert=$$.body.num_value {
message.obj = "%d" % (self.messageID);
}
};
#-----------------------------------------------------------------------------
# Extended Operation
# https://tools.ietf.org/html/rfc4511#section-4.12
# TODO: implement ExtendedRequest
# type ExtendedRequest = unit(inout message: Message) {
#
# };
# TODO: implement ExtendedResponse
# type ExtendedResponse = unit(inout message: Message) {
#
# };
#-----------------------------------------------------------------------------
# IntermediateResponse Message
# https://tools.ietf.org/html/rfc4511#section-4.13
# TODO: implement IntermediateResponse
# type IntermediateResponse = unit(inout message: Message) {
#
# };
on LDAP::MessageWrapper::%done {
spicy::accept_input();
}

View file

@ -18,9 +18,11 @@
1 25
1 2811
1 3128
1 3268
1 3306
2 3389
1 3544
2 389
1 4011
2 443
1 445
@ -60,8 +62,8 @@
1 992
1 993
1 995
67 and
66 or
67 port
44 tcp
23 udp
70 and
69 or
70 port
46 tcp
24 udp

View file

@ -406,6 +406,9 @@ scripts/base/init-default.zeek
scripts/base/protocols/krb/main.zeek
scripts/base/protocols/krb/consts.zeek
scripts/base/protocols/krb/files.zeek
scripts/base/protocols/ldap/__load__.zeek
scripts/base/protocols/ldap/consts.zeek
scripts/base/protocols/ldap/main.zeek
scripts/base/protocols/modbus/__load__.zeek
scripts/base/protocols/modbus/consts.zeek
scripts/base/protocols/modbus/main.zeek

View file

@ -20,6 +20,8 @@ known_certs
known_hosts
known_modbus
known_services
ldap
ldap_search
loaded_scripts
modbus
modbus_register_change

View file

@ -360,6 +360,39 @@ connection {
* ts: time, log=T, optional=F
* uid: string, log=T, optional=F
}
* ldap: record LDAP::State, log=F, optional=T
LDAP::State {
* messages: table[int] of record LDAP::MessageInfo, log=F, optional=T
LDAP::MessageInfo {
* arguments: vector of string, log=T, optional=T
* diagnostic_messages: vector of string, log=T, optional=T
* id: record conn_id, log=T, optional=F
conn_id { ... }
* message_id: int, log=T, optional=T
* objects: vector of string, log=T, optional=T
* opcodes: set[string], log=T, optional=T
* results: set[string], log=T, optional=T
* ts: time, log=T, optional=F
* uid: string, log=T, optional=F
* version: int, log=T, optional=T
}
* searches: table[int] of record LDAP::SearchInfo, log=F, optional=T
LDAP::SearchInfo {
* attributes: vector of string, log=T, optional=T
* base_objects: vector of string, log=T, optional=T
* derefs: set[string], log=T, optional=T
* diagnostic_messages: vector of string, log=T, optional=T
* filter: string, log=T, optional=T
* id: record conn_id, log=T, optional=F
conn_id { ... }
* message_id: int, log=T, optional=T
* result_count: count, log=T, optional=T
* results: set[string], log=T, optional=T
* scopes: set[string], log=T, optional=T
* ts: time, log=T, optional=F
* uid: string, log=T, optional=F
}
}
* modbus: record Modbus::Info, log=F, optional=T
Modbus::Info {
* exception: string, log=T, optional=T

View file

@ -7,10 +7,10 @@
#open XXXX-XX-XX-XX-XX-XX
#fields ts fuid uid id.orig_h id.orig_p id.resp_h id.resp_p source depth analyzers mime_type filename duration local_orig is_orig seen_bytes total_bytes missing_bytes overflow_bytes timedout parent_fuid md5 sha1 sha256
#types time string string addr port addr port string count set[string] string string interval bool bool count count count count bool string string string string
XXXXXXXXXX.XXXXXX FgN3AE3of2TRIqaeQe CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-user-cert - 0.000000 F F 1859 - 0 0 F - 7af07aca6d5c6e8e87fe4bb34786edc0 548b9e03bc183d1cd39f93a37985cb3950f8f06f 6bacfa4536150ed996f2b0c05ab6e345a257225f449aeb9d2018ccd88f4ede43
XXXXXXXXXX.XXXXXX Fv2Agc4z5boBOacQi6 CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 1032 - 0 0 F - 9e4ac96474245129d9766700412a1f89 d83c1a7f4d0446bb2081b81a1670f8183451ca24 a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d
XXXXXXXXXX.XXXXXX Ftmyeg2qgI2V38Dt3g CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 897 - 0 0 F - 2e7db2a31d0e3da4b25f49b9542a2e1a 7359755c6df9a0abc3060bce369564c8ec4542a3 3c35cc963eb004451323d3275d05b353235053490d9cd83729a2faf5e7ca1cc0
XXXXXXXXXX.XXXXXX FUFNf84cduA0IJCp07 ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-user-cert - 0.000000 F F 1859 - 0 0 F - 7af07aca6d5c6e8e87fe4bb34786edc0 548b9e03bc183d1cd39f93a37985cb3950f8f06f 6bacfa4536150ed996f2b0c05ab6e345a257225f449aeb9d2018ccd88f4ede43
XXXXXXXXXX.XXXXXX F1H4bd2OKGbLPEdHm4 ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 1032 - 0 0 F - 9e4ac96474245129d9766700412a1f89 d83c1a7f4d0446bb2081b81a1670f8183451ca24 a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d
XXXXXXXXXX.XXXXXX Fgsbci2jxFXYMOHOhi ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 897 - 0 0 F - 2e7db2a31d0e3da4b25f49b9542a2e1a 7359755c6df9a0abc3060bce369564c8ec4542a3 3c35cc963eb004451323d3275d05b353235053490d9cd83729a2faf5e7ca1cc0
XXXXXXXXXX.XXXXXX FgN3AE3of2TRIqaeQe CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-user-cert - 0.000000 F F 1859 - 0 0 F - 7af07aca6d5c6e8e87fe4bb34786edc0 548b9e03bc183d1cd39f93a37985cb3950f8f06f 6bacfa4536150ed996f2b0c05ab6e345a257225f449aeb9d2018ccd88f4ede43
XXXXXXXXXX.XXXXXX Fv2Agc4z5boBOacQi6 CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 1032 - 0 0 F - 9e4ac96474245129d9766700412a1f89 d83c1a7f4d0446bb2081b81a1670f8183451ca24 a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d
XXXXXXXXXX.XXXXXX Ftmyeg2qgI2V38Dt3g CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 897 - 0 0 F - 2e7db2a31d0e3da4b25f49b9542a2e1a 7359755c6df9a0abc3060bce369564c8ec4542a3 3c35cc963eb004451323d3275d05b353235053490d9cd83729a2faf5e7ca1cc0
XXXXXXXXXX.XXXXXX FUFNf84cduA0IJCp07 ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-user-cert - 0.000000 F F 1859 - 0 0 F - 7af07aca6d5c6e8e87fe4bb34786edc0 548b9e03bc183d1cd39f93a37985cb3950f8f06f 6bacfa4536150ed996f2b0c05ab6e345a257225f449aeb9d2018ccd88f4ede43
XXXXXXXXXX.XXXXXX F1H4bd2OKGbLPEdHm4 ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 1032 - 0 0 F - 9e4ac96474245129d9766700412a1f89 d83c1a7f4d0446bb2081b81a1670f8183451ca24 a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d
XXXXXXXXXX.XXXXXX Fgsbci2jxFXYMOHOhi ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 897 - 0 0 F - 2e7db2a31d0e3da4b25f49b9542a2e1a 7359755c6df9a0abc3060bce369564c8ec4542a3 3c35cc963eb004451323d3275d05b353235053490d9cd83729a2faf5e7ca1cc0
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path conn
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration orig_bytes resp_bytes conn_state missed_bytes history orig_pkts orig_ip_bytes resp_pkts resp_ip_bytes tunnel_parents
#types time string addr port addr port enum string interval count count string count string count count count count set[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp ldap_tcp 181.520479 258 188 RSTO 0 ShADdR 8 590 4 360 -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,12 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ldap
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p message_id version opcodes results diagnostic_messages objects arguments
#types time string addr port addr port int int set[string] set[string] vector[string] vector[string] vector[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 1 3 bind simple success - xxxxxxxxxxx@xx.xxx.xxxxx.net REDACTED
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 3 3 bind simple success - CN=xxxxxxxx\x2cOU=Users\x2cOU=Accounts\x2cDC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net REDACTED
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ldap_search
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p message_id scopes derefs base_objects result_count results diagnostic_messages filter attributes
#types time string addr port addr port int set[string] set[string] vector[string] count set[string] vector[string] string vector[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 2 tree always DC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net 1 success - (&(objectclass=*)(sAMAccountName=xxxxxxxx)) sAMAccountName
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path conn
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration orig_bytes resp_bytes conn_state missed_bytes history orig_pkts orig_ip_bytes resp_pkts resp_ip_bytes tunnel_parents
#types time string addr port addr port enum string interval count count string count string count count count count set[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp ldap_tcp 181.520479 258 188 RSTO 0 ShADdR 8 590 4 360 -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,12 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ldap
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p message_id version opcodes results diagnostic_messages objects arguments
#types time string addr port addr port int int set[string] set[string] vector[string] vector[string] vector[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 1 3 bind simple success - xxxxxxxxxxx@xx.xxx.xxxxx.net REDACTED
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 3 3 bind simple success - CN=xxxxxxxx\x2cOU=Users\x2cOU=Accounts\x2cDC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net REDACTED
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ldap_search
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p message_id scopes derefs base_objects result_count results diagnostic_messages filter attributes
#types time string addr port addr port int set[string] set[string] vector[string] count set[string] vector[string] string vector[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 2 tree always DC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net 1 success - (&(objectclass=*)(sAMAccountName=xxxxxxxx)) -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path conn
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration orig_bytes resp_bytes conn_state missed_bytes history orig_pkts orig_ip_bytes resp_pkts resp_ip_bytes tunnel_parents
#types time string addr port addr port enum string interval count count string count string count count count count set[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 32681 tcp ldap_tcp 181.520479 258 188 RSTO 0 ShADdR 8 590 4 360 -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,12 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ldap
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p message_id version opcodes results diagnostic_messages objects arguments
#types time string addr port addr port int int set[string] set[string] vector[string] vector[string] vector[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 32681 1 3 bind simple success - xxxxxxxxxxx@xx.xxx.xxxxx.net REDACTED
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 32681 3 3 bind simple success - CN=xxxxxxxx\x2cOU=Users\x2cOU=Accounts\x2cDC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net REDACTED
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ldap_search
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p message_id scopes derefs base_objects result_count results diagnostic_messages filter attributes
#types time string addr port addr port int set[string] set[string] vector[string] count set[string] vector[string] string vector[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 32681 2 tree always DC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net 1 success - (&(objectclass=*)(sAMAccountName=xxxxxxxx)) -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path conn
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration orig_bytes resp_bytes conn_state missed_bytes history orig_pkts orig_ip_bytes resp_pkts resp_ip_bytes tunnel_parents
#types time string addr port addr port enum string interval count count string count string count count count count set[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp ldap_tcp 181.520479 258 188 RSTO 0 ShADdR 8 590 4 360 -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path conn
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration orig_bytes resp_bytes conn_state missed_bytes history orig_pkts orig_ip_bytes resp_pkts resp_ip_bytes tunnel_parents
#types time string addr port addr port enum string interval count count string count string count count count count set[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 172.31.1.104 3116 172.31.1.101 389 tcp ldap_tcp 0.813275 1814 2391 S1 0 ShADd 6 2062 4 2559 -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ldap
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p message_id version opcodes results diagnostic_messages objects arguments
#types time string addr port addr port int int set[string] set[string] vector[string] vector[string] vector[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 172.31.1.104 3116 172.31.1.101 389 215 3 bind SASL success - - GSS-SPNEGO
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ldap_search
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p message_id scopes derefs base_objects result_count results diagnostic_messages filter attributes
#types time string addr port addr port int set[string] set[string] vector[string] count set[string] vector[string] string vector[string]
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 172.31.1.104 3116 172.31.1.101 389 213 base never - 1 success - (objectclass=*) -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ldap_search
#open XXXX-XX-XX-XX-XX-XX
#fields uid filter base_objects
#types string string vector[string]
CHhAvVGS1DHFjwGM9 (departmentNumber:2.16.840.1.113730.3.3.2.46.1:=>=N4709) DC=matrix\x2cDC=local
#close XXXX-XX-XX-XX-XX-XX

View file

@ -7,3 +7,14 @@ depend on them for tests.
Trace Index/Sources:
- modbus/modbus-eit.trace: Sourced from https://www.netresec.com/?page=PCAP4SICS, credit to https://cs3sthlm.se/. The packets in this trace were pulled from the 4SICS-GeekLounge-151021.pcap file.
- [ldap/simpleauth.pcap](https://github.com/arkime/arkime/blob/main/tests/pcap/ldap-simpleauth.pcap)
- ldap/simpleauth-diff-port.pcap: made with
`tcprewrite -r 3268:32681 -i simpleauth.pcap -o simpleauth-diff-port.pcap`
- ldap/krb5-sign-seal-01.pcap: trace is derived from
<https://wiki.wireshark.org/uploads/__moin_import__/attachments/SampleCaptures/ldap-krb5-sign-seal-01.cap>
- the LDAP flow selected (filtered out the Kerberos packets)
- truncated to 10 packets (where packet 10 contains the SASL encrypted LDAP message)
- one `\x30` byte in the ciphertext changed to `\x00`
- ldap/issue-32.pcapng: Provided by GH user martinvanhensbergen,
<https://github.com/zeek/spicy-ldap/issues/23>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,3 +1,5 @@
# @TEST-REQUIRES: have-spicy
#
# @TEST-EXEC: zeek -r $TRACES/empty.trace >output
# @TEST-EXEC: cat packet_filter.log >>output
# @TEST-EXEC: zeek -r $TRACES/empty.trace -f "port 42" >>output
@ -12,4 +14,3 @@
# @TEST-EXEC: zeek -r $TRACES/empty.trace PacketFilter::enable_auto_protocol_capture_filters=T
# @TEST-EXEC: cat packet_filter.log | zeek-cut filter | sed 's#[()]##g' | tr ' ' '\n' | sort | uniq -c | awk '{print $1, $2}' >output2
# @TEST-EXEC: btest-diff output2

View file

@ -4,6 +4,7 @@
#
# If this test fails, then the "Log Files" documentation page should be updated.
# @TEST-REQUIRES: ${SCRIPTS}/have-spicy
# @TEST-REQUIRES: which python3
# @TEST-EXEC: bash %INPUT
# @TEST-EXEC: btest-diff out

View file

@ -1,5 +1,6 @@
# Test that checks that files.log is generated if the respective option is set.
# @TEST-REQUIRES: ${SCRIPTS}/have-spicy
# @TEST-EXEC: zeek -b -r $TRACES/tls/google-duplicate.trace %INPUT
# @TEST-EXEC: btest-diff files.log

View file

@ -0,0 +1,12 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-REQUIRES: have-spicy
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap/simpleauth.pcap %INPUT
# @TEST-EXEC: cat conn.log | zeek-cut -Cn local_orig local_resp > conn.log2 && mv conn.log2 conn.log
# @TEST-EXEC: btest-diff conn.log
# @TEST-EXEC: btest-diff ldap.log
# @TEST-EXEC: btest-diff ldap_search.log
#
# @TEST-DOC: Test LDAP search attributes with small trace.
redef LDAP::default_log_search_attributes = T;

View file

@ -0,0 +1,6 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-REQUIRES: have-spicy
# @TEST-EXEC: zeek -NN | grep -q ANALYZER_LDAP_TCP
#
# @TEST-DOC: Check that LDAP (TCP) is analyzer is available.

View file

@ -0,0 +1,11 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-REQUIRES: have-spicy
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap/simpleauth.pcap %INPUT >output 2>&1
# @TEST-EXEC: btest-diff output
# @TEST-EXEC: cat conn.log | zeek-cut -Cn local_orig local_resp > conn.log2 && mv conn.log2 conn.log
# @TEST-EXEC: btest-diff conn.log
# @TEST-EXEC: btest-diff ldap.log
# @TEST-EXEC: btest-diff ldap_search.log
#
# @TEST-DOC: Test LDAP analyzer with small trace.

View file

@ -0,0 +1,10 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-REQUIRES: have-spicy
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap/simpleauth-diff-port.pcap %INPUT
# @TEST-EXEC: cat conn.log | zeek-cut -Cn local_orig local_resp > conn.log2 && mv conn.log2 conn.log
# @TEST-EXEC: btest-diff conn.log
# @TEST-EXEC: btest-diff ldap.log
# @TEST-EXEC: btest-diff ldap_search.log
#
# @TEST-DOC: Test LDAP analyzer with small trace.

View file

@ -0,0 +1,134 @@
# This test can only run if we have the LDAP grammar available.
# @TEST-REQUIRES: have-spicy && [ -n ${DIST} ]
#
# @TEST-EXEC: spicyc -j -d -L ${DIST}/src/analyzer/protocol/ldap %INPUT
#
# @TEST-DOC: Validates helper functions in LDAP module.
module test;
import LDAP;
# ----------------------------------------------------------------------------------
# function utf16_guid_to_hex_repr()
# - requires exactly 16 bytes
# Not enough bytes (15)
assert LDAP::utf16_guid_to_hex_repr(b"1234567890ABCDE") == "GUID_FORMAT_FAILED";
# Too much bytes (17)
assert LDAP::utf16_guid_to_hex_repr(b"1234567890ABCDEFG") == "GUID_FORMAT_FAILED";
# Empty
assert LDAP::utf16_guid_to_hex_repr(b"") == "GUID_FORMAT_FAILED";
# 16 times \x00
assert LDAP::utf16_guid_to_hex_repr(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") == "00000000-0000-0000-0000-000000000000";
# 16 times \xff
assert LDAP::utf16_guid_to_hex_repr(b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff") == "ffffffff-ffff-ffff-ffff-ffffffffffff";
# Valid DomainGuidFilter
assert LDAP::utf16_guid_to_hex_repr(b"\x3b\x52\xb3\xb0\x6f\x54\xaf\x4f\x93\xb2\x29\x4a\x38\x50\x98\xf2") == "b0b3523b-546f-4faf-93b2-294a385098f2";
# ----------------------------------------------------------------------------------
# function bytes_sid_to_hex_repr()
# - transforms bytes of arbitrary length to a ':' separated string
# Empty
assert LDAP::bytes_sid_to_hex_repr(b"") == "";
# 10 times \x00
assert LDAP::bytes_sid_to_hex_repr(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") == "00:00:00:00:00:00:00:00:00:00";
# 10 times \xff
assert LDAP::bytes_sid_to_hex_repr(b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff") == "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff";
# Valid `AAC` value
assert LDAP::bytes_sid_to_hex_repr(b"\x80\x00\x00\x00") == "80:00:00:00";
# Valid objectSid
assert LDAP::bytes_sid_to_hex_repr(b"\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00\xd5\x64\xbe\x81\x5d\x68\x9c\x0d\x44\x4a\xae\x74\x01\x02\x00\x00") == "01:05:00:00:00:00:00:05:15:00:00:00:d5:64:be:81:5d:68:9c:0d:44:4a:ae:74:01:02:00:00";
# ----------------------------------------------------------------------------------
# function bytes_sid_to_SID_repr()
# - requires exactly 24 bytes
# Not enough bytes (0 and 10)
assert LDAP::bytes_sid_to_SID_repr(b"") == "SID_FORMAT_FAILED";
assert LDAP::bytes_sid_to_SID_repr(b"1234567890") == "SID_FORMAT_FAILED";
# Too much bytes (25)
assert LDAP::bytes_sid_to_SID_repr(b"1234567890123456789012345") == "SID_FORMAT_FAILED";
# Empty
assert LDAP::bytes_sid_to_SID_repr(b"") == "SID_FORMAT_FAILED";
# Valid SID
assert LDAP::bytes_sid_to_SID_repr(b"\x01\x04\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00\x39\xc5\xc7\x44\xfa\xbd\x24\x1d\x4a\x65\xfd\x71") == "S-1-5-21-1153942841-488947194-1912431946";
# Some random bytes - probably an invalid SID but no error
assert LDAP::bytes_sid_to_SID_repr(b"\x02\x08\x00\x02\x00\x00\x00\x05\x15\x20\x00\x12\xd5\x64\xaf\x84\x5d\x68\x9c\x0d\x44\x4c\xad\x73") == "S-2-8589934597-301998101-2226087125-228354141-1940737092";
# All 1's
assert LDAP::bytes_sid_to_SID_repr(b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff") == "S-255-281474976710655-4294967295-4294967295-4294967295-4294967295";
# ----------------------------------------------------------------------------------
# function uint32_to_hex_repr()
# Not enough bytes (0 and 2)
assert LDAP::uint32_to_hex_repr(b"") == "HEX_FORMAT_FAILED";
assert LDAP::uint32_to_hex_repr(b"12") == "HEX_FORMAT_FAILED";
# Too much bytes (6)
assert LDAP::uint32_to_hex_repr(b"123456") == "HEX_FORMAT_FAILED";
# Empty
assert LDAP::uint32_to_hex_repr(b"") == "HEX_FORMAT_FAILED";
# Valid `NtVer` value
assert LDAP::uint32_to_hex_repr(b"\x16\x00\x00\x00") == "0x00000016";
# 4 times \x00
assert LDAP::uint32_to_hex_repr(b"\x00\x00\x00\x00") == "0x00000000";
# 4 times \xff
assert LDAP::uint32_to_hex_repr(b"\xff\xff\xff\xff") == "0xffffffff";
# ----------------------------------------------------------------------------------
# function string_representation()
function make_nested_repr(filters: vector<string>): string {
local nestedOr: LDAP::ParseNestedAndOr;
nestedOr.searchfilters = vector<LDAP::SearchFilter>();
for (f in filters) {
local or_: LDAP::SearchFilter;
or_.filterType = LDAP::FilterType::FILTER_PRESENT;
or_.FILTER_PRESENT = f;
or_.stringRepresentation = LDAP::string_representation(or_);
nestedOr.searchfilters.push_back(or_);
}
local searchFilter: LDAP::SearchFilter;
searchFilter.filterType = LDAP::FilterType::FILTER_OR;
searchFilter.FILTER_OR = nestedOr;
return LDAP::string_representation(searchFilter);
}
function test_string_representation() {
local repr0 = make_nested_repr(vector());
assert repr0 == "": repr0;
local repr1 = make_nested_repr(vector("foo"));
assert repr1 == "(|(foo=*))": repr1;
local repr2 = make_nested_repr(vector("foo", "bar"));
assert repr2 == "(|(foo=*)(bar=*))": repr2;
local repr3 = make_nested_repr(vector("foo", "bar", "baz"));
assert repr3 == "(|(|(foo=*)(bar=*))(baz=*))": repr3;
}
test_string_representation();

View file

@ -0,0 +1,22 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-REQUIRES: have-spicy
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap/simpleauth.pcap %INPUT >output 2>&1
# @TEST-EXEC: btest-diff output
# @TEST-EXEC: cat conn.log | zeek-cut -Cn local_orig local_resp > conn.log2 && mv conn.log2 conn.log
# @TEST-EXEC: btest-diff conn.log
# @TEST-EXEC: ! test -f ldap.log
# @TEST-EXEC: ! test -f ldap_search.log
#
# @TEST-DOC: Test LDAP analyzer with small trace using logging policies.
hook LDAP::log_policy(rec: LDAP::MessageInfo, id: Log::ID, filter: Log::Filter)
{
break;
}
hook LDAP::log_policy_search(rec: LDAP::SearchInfo, id: Log::ID,
filter: Log::Filter)
{
break;
}

View file

@ -0,0 +1,12 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-REQUIRES: have-spicy
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap/krb5-sign-seal-01.pcap %INPUT
# @TEST-EXEC: cat conn.log | zeek-cut -Cn local_orig local_resp > conn.log2 && mv conn.log2 conn.log
# @TEST-EXEC: btest-diff conn.log
# @TEST-EXEC: btest-diff ldap.log
# @TEST-EXEC: btest-diff ldap_search.log
# @TEST-EXEC: ! test -f weird.log
# @TEST-EXEC: ! test -f dpd.log
#
# @TEST-DOC: Test LDAP analyzer with SASL encrypted payloads.

View file

@ -0,0 +1,10 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-DOC: This test case is a regression test for #23.
#
# @TEST-REQUIRES: have-spicy
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap/issue-32.pcapng %INPUT
# @TEST-EXEC: cat ldap_search.log | zeek-cut -C uid filter base_objects > ldap_search.log2 && mv ldap_search.log2 ldap_search.log
# @TEST-EXEC: btest-diff ldap_search.log
#
# @TEST-DOC: Test LDAP analyzer with small trace.

View file

@ -1 +1 @@
e2bcf374b7f03f95f7280568eab31600d58812ae
1522faf244d4de213a8f17ab4e2c5273d80d99e4