mirror of
https://github.com/zeek/zeek.git
synced 2025-10-17 14:08:20 +00:00
Import zeek/spicy-ldap@57b5eff988
This commit is contained in:
parent
255103b50a
commit
080d7418d7
53 changed files with 2526 additions and 0 deletions
26
src/spicy/spicy-ldap/.cmake-format.json
Normal file
26
src/spicy/spicy-ldap/.cmake-format.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"parse": {
|
||||
"additional_commands": {
|
||||
"spicy_add_analyzer": {
|
||||
"kwargs": {
|
||||
"NAME": "*",
|
||||
"PACKAGE_NAME": "*",
|
||||
"SOURCES": "*",
|
||||
"SCRIPTS": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": {
|
||||
"line_width": 100,
|
||||
"tab_size": 4,
|
||||
"separate_ctrl_name_with_space": true,
|
||||
"max_subgroups_hwrap": 3
|
||||
},
|
||||
"markup": {
|
||||
"enable_markup": false
|
||||
},
|
||||
"lint": {
|
||||
"disabled_codes": ["C0103"]
|
||||
}
|
||||
}
|
35
src/spicy/spicy-ldap/.github/workflows/check.yml
vendored
Normal file
35
src/spicy/spicy-ldap/.github/workflows/check.yml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
Check:
|
||||
strategy:
|
||||
matrix:
|
||||
version:
|
||||
- zeek:6.0
|
||||
- zeek-dev:latest
|
||||
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
container: zeek/${{ matrix.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Prepare
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y -q --no-install-recommends g++ cmake make libpcap-dev
|
||||
- name: Install
|
||||
run: |
|
||||
git config --global --add safe.directory $PWD
|
||||
git clean -fd
|
||||
eval $(zkg env)
|
||||
echo Y | zkg -vvvvv install .
|
||||
|
||||
- name: Show logs
|
||||
if: always()
|
||||
run: |
|
||||
tail -n 1000000 $(zkg config state_dir)/logs/*.log
|
14
src/spicy/spicy-ldap/.github/workflows/pre-commit.yml
vendored
Normal file
14
src/spicy/spicy-ldap/.github/workflows/pre-commit.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
name: pre-commit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: pre-commit/action@v2.0.3
|
7
src/spicy/spicy-ldap/.gitignore
vendored
Normal file
7
src/spicy/spicy-ldap/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
build
|
||||
.idea/
|
||||
cmake-build-debug/
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.tmp
|
||||
tests/.btest*
|
1
src/spicy/spicy-ldap/.mdlrc
Normal file
1
src/spicy/spicy-ldap/.mdlrc
Normal file
|
@ -0,0 +1 @@
|
|||
rules "~MD033", "~MD013", "~MD046", "~MD010"
|
23
src/spicy/spicy-ldap/.pre-commit-config.yaml
Normal file
23
src/spicy/spicy-ldap/.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,23 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/markdownlint/markdownlint
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
|
||||
- repo: https://github.com/cheshirekow/cmake-format-precommit
|
||||
rev: v0.6.13
|
||||
hooks:
|
||||
- id: cmake-format
|
||||
- id: cmake-lint
|
||||
|
||||
exclude: '^tests/Baseline'
|
18
src/spicy/spicy-ldap/CMakeLists.txt
Normal file
18
src/spicy/spicy-ldap/CMakeLists.txt
Normal file
|
@ -0,0 +1,18 @@
|
|||
cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
|
||||
|
||||
project(Messages LANGUAGES C)
|
||||
|
||||
list(PREPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
|
||||
find_package(SpicyPlugin REQUIRED)
|
||||
|
||||
# Set mininum versions that this plugin needs. Make sure to use "x.y.z" format.
|
||||
spicy_require_version("1.2.0")
|
||||
spicy_plugin_require_version("0.99.0")
|
||||
zeek_require_version("3.0.0")
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE)
|
||||
# Default to release build.
|
||||
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "")
|
||||
endif ()
|
||||
|
||||
add_subdirectory(analyzer)
|
29
src/spicy/spicy-ldap/LICENSE
Normal file
29
src/spicy/spicy-ldap/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
Copyright (c) 2020-2021 by the Zeek Project through the International Computer
|
||||
Science Institute. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
(1) Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
(2) Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
(3) Neither the name of the Zeek Project, the International Computer
|
||||
Science Institute, nor the names of contributors may be used to
|
||||
endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
74
src/spicy/spicy-ldap/README.md
Normal file
74
src/spicy/spicy-ldap/README.md
Normal file
|
@ -0,0 +1,74 @@
|
|||
LDAP Analyzer
|
||||
=============
|
||||
|
||||
Here's what it has:
|
||||
|
||||
- ASN.1 structure decoding: this is probably generally useful for more than just the LDAP parser, so it may be of interest for this to be included somehow as part of spicy's standard modules or whatever
|
||||
- everything is working except for the "constructed" forms of `ASN1BitString` and `ASN1OctetString`
|
||||
- LDAP: the LDAP parsing is basically "done once" through a single call to `ASN1Message` (which parses itself recursively) and then the application-level data is also parsed via `&parse-from` a byte array belonging to the outer ASN.1 sequence. This second level of parsing is also done using the ASN.1 data types.
|
||||
- events
|
||||
- `ldap::message` - called for each LDAP message
|
||||
- `ldap::bindreq` - when a bind request is made
|
||||
- `ldap::searchreq` - basic search request information
|
||||
- `ldap::searchres` - called each time a search result is returned
|
||||
- enums
|
||||
- `ProtocolOpcode`
|
||||
- `ResultCode`
|
||||
- `BindAuthType`
|
||||
- `SearchScope`
|
||||
- `SearchDerefAlias`
|
||||
- `FilterType`
|
||||
- Zeek log files
|
||||
- `ldap.log` - contains information about all LDAP messages except those that are search-related. Log lines are grouped by connection ID + message ID
|
||||
- `ts` (time)
|
||||
- `uid` (connection UID)
|
||||
- `id` (connection ID 4-tuple)
|
||||
- `proto` (transport protocol)
|
||||
- `message_id` (LDAP message ID)
|
||||
- `version` (LDAP version for bind requests)
|
||||
- `opcode` (set of 1..n operations from this uid+message_id)
|
||||
- `result` (set of 1..n results from this uid+message_id)
|
||||
- `diagnostic_message` (vector of 0..n diagnostic message strings)
|
||||
- `object` (vector of 0..n "objects," the meaning of which depends on the operation)
|
||||
- `argument` (vector of 0..n "argument," the meaning of which depends on the operation)
|
||||
- `ldap_search.log` - contains information about LDAP searches. Log lines are grouped by connection ID + message ID
|
||||
- `ts` (time)
|
||||
- `uid` (connection UID)
|
||||
- `id` (connection ID 4-tuple)
|
||||
- `proto` (transport protocol)
|
||||
- `message_id` (LDAP message ID)
|
||||
- `scope` (set of 1..n search scopes defined in this uid+message_id)
|
||||
- `deref` (set of 1..n search deref alias options defined in this uid+message_id)
|
||||
- `base_object` (vector of 0..n search base objects specified)
|
||||
- `result_count` (number of result entries returned)
|
||||
- `result` (set of 1..n results from this uid+message_id)
|
||||
- `diagnostic_message` (vector of 0..n diagnostic message strings)
|
||||
- `filter` (search filter string)
|
||||
- `attributes` (vector of 0..n "attributes", the attributes that were returned)
|
||||
- test
|
||||
- basic tests for detecting plugin presence and simple bind and search result/requests
|
||||
|
||||
Here's what it doesn't have, which could be added by future parties interested in expanding it:
|
||||
|
||||
- LDAP [referrals](https://tools.ietf.org/html/rfc4511#section-4.1.10) are not parsed out of the results
|
||||
- [SASL credentials](https://datatracker.ietf.org/doc/html/rfc4511#section-4.2) in bind requests are not being parsed beyond the mechanism string
|
||||
- SASL information in bind responses are not being parsed; for that matter, SASL-based LDAP stuff hasn't been tested much and may have issues
|
||||
- Search filters and attributes: the search filters, reconstructed from the query tree, is represented in string format. The AND and OR filters have a tree structure and are parsed with the `ParseNestedAndOr` unit, whereas the NOT filter consist of one single nested SearchFilter and is parsed with a `ParseNestedNot` unit. The remaining filter types can all be decoded to a string using the `DecodedAttributeValue` unit, which takes the `FilterType` as a parameter. The `FILTER_PRESENT` consists of a single octet string and can be parsed directly. By recursively constructing leafs and nodes in the tree, the final search filter can be represented, e.g. `(&(objectclass=*)(sAMAccountName=xxxxxxxx))`. The returned attributes are represented in a list and returned to the `ldap_search.log` if `option default_log_search_attributes = T;` is set (the default is False).
|
||||
- the details of `SearchResultReference` are not being parsed
|
||||
- the only detail of `ModifyRequest` being parsed is the object name
|
||||
- the details of `AddRequest` are not being parsed
|
||||
- the details of `ModDNRequest` are not being parsed
|
||||
- the details of `CompareRequest` are not being parsed
|
||||
- the details of `AbandonRequest` are not being parsed
|
||||
- the details of `ExtendedRequest` are not being parsed
|
||||
- the details of `ExtendedResponse` are not being parsed
|
||||
- the details of `IntermediateResponse` are not being parsed
|
||||
- [Logging policy](https://docs.zeek.org/en/master/frameworks/logging.html#filtering-log-records) is available.
|
||||
|
||||
Useful Links:
|
||||
|
||||
- <https://luca.ntop.org/Teaching/Appunti/asn1.html>
|
||||
- <https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf>
|
||||
- <https://tools.ietf.org/html/rfc4511#>
|
||||
- <https://ldap.com/ldapv3-wire-protocol-reference-asn1-ber/>
|
||||
- <https://lapo.it/asn1js>
|
5
src/spicy/spicy-ldap/analyzer/CMakeLists.txt
Normal file
5
src/spicy/spicy-ldap/analyzer/CMakeLists.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
spicy_add_analyzer(
|
||||
NAME LDAP
|
||||
PACKAGE_NAME spicy-ldap
|
||||
SOURCES ldap.spicy ldap.evt ldap_zeek.spicy
|
||||
SCRIPTS __load__.zeek main.zeek dpd.sig)
|
2
src/spicy/spicy-ldap/analyzer/__load__.zeek
Normal file
2
src/spicy/spicy-ldap/analyzer/__load__.zeek
Normal file
|
@ -0,0 +1,2 @@
|
|||
@load-sigs ./dpd.sig
|
||||
@load ./main.zeek
|
278
src/spicy/spicy-ldap/analyzer/asn1.spicy
Normal file
278
src/spicy/spicy-ldap/analyzer/asn1.spicy
Normal 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_);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
23
src/spicy/spicy-ldap/analyzer/dpd.sig
Normal file
23
src/spicy/spicy-ldap/analyzer/dpd.sig
Normal 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 "spicy_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 "spicy_LDAP_TCP"
|
||||
}
|
43
src/spicy/spicy-ldap/analyzer/ldap.evt
Normal file
43
src/spicy/spicy-ldap/analyzer/ldap.evt
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
|
||||
|
||||
protocol analyzer spicy::LDAP_TCP over TCP:
|
||||
parse with LDAP::Messages,
|
||||
ports { 389/tcp, 3268/tcp};
|
||||
|
||||
protocol analyzer spicy::LDAP_UDP over UDP:
|
||||
parse with LDAP::Messages,
|
||||
ports { 389/udp };
|
||||
|
||||
import LDAP;
|
||||
import LDAP_Zeek;
|
||||
|
||||
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);
|
871
src/spicy/spicy-ldap/analyzer/ldap.spicy
Normal file
871
src/spicy/spicy-ldap/analyzer/ldap.spicy
Normal file
|
@ -0,0 +1,871 @@
|
|||
# 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 delimeter was not found, we
|
||||
# leave the remainer 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) {
|
||||
#
|
||||
# };
|
14
src/spicy/spicy-ldap/analyzer/ldap_zeek.spicy
Normal file
14
src/spicy/spicy-ldap/analyzer/ldap_zeek.spicy
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
|
||||
|
||||
module LDAP_Zeek;
|
||||
|
||||
import zeek;
|
||||
import LDAP;
|
||||
|
||||
on LDAP::MessageWrapper::%done {
|
||||
zeek::confirm_protocol();
|
||||
}
|
||||
|
||||
on LDAP::MessageWrapper::%error {
|
||||
zeek::reject_protocol("error while parsing LDAP message");
|
||||
}
|
502
src/spicy/spicy-ldap/analyzer/main.zeek
Normal file
502
src/spicy/spicy-ldap/analyzer/main.zeek
Normal file
|
@ -0,0 +1,502 @@
|
|||
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
|
||||
|
||||
module LDAP;
|
||||
|
||||
export {
|
||||
redef enum Log::ID += { LDAP_LOG,
|
||||
LDAP_SEARCH_LOG };
|
||||
|
||||
## 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;
|
||||
|
||||
#############################################################################
|
||||
# This is the format of ldap.log (ldap operations minus search-related)
|
||||
# Each line represents a unique connection+message_id (requests/responses)
|
||||
type Message: 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;
|
||||
|
||||
# transport protocol
|
||||
proto: string &log &optional;
|
||||
|
||||
# Message ID
|
||||
message_id: int &log &optional;
|
||||
|
||||
# LDAP version
|
||||
version: int &log &optional;
|
||||
|
||||
# normalized operations (e.g., bind_request and bind_response to "bind")
|
||||
opcode: set[string] &log &optional;
|
||||
|
||||
# Result code(s)
|
||||
result: set[string] &log &optional;
|
||||
|
||||
# result diagnostic message(s)
|
||||
diagnostic_message: vector of string &log &optional;
|
||||
|
||||
# object(s)
|
||||
object: vector of string &log &optional;
|
||||
|
||||
# argument(s)
|
||||
argument: 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 Search: 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;
|
||||
|
||||
# transport protocol
|
||||
proto: string &log &optional;
|
||||
|
||||
# Message ID
|
||||
message_id: int &log &optional;
|
||||
|
||||
# sets of search scope and deref alias
|
||||
scope: set[string] &log &optional;
|
||||
deref: set[string] &log &optional;
|
||||
|
||||
# base search objects
|
||||
base_object: vector of string &log &optional;
|
||||
|
||||
# number of results returned
|
||||
result_count: count &log &optional;
|
||||
|
||||
# Result code (s)
|
||||
result: set[string] &log &optional;
|
||||
|
||||
# result diagnostic message(s)
|
||||
diagnostic_message: 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;
|
||||
};
|
||||
|
||||
# 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::Message);
|
||||
global log_ldap_search: event(rec: LDAP::Search);
|
||||
|
||||
# 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);
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
#############################################################################
|
||||
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 };
|
||||
|
||||
#############################################################################
|
||||
redef record connection += {
|
||||
ldap_proto: string &optional;
|
||||
ldap_messages: table[int] of Message &optional;
|
||||
ldap_searches: table[int] of Search &optional;
|
||||
};
|
||||
|
||||
#############################################################################
|
||||
event zeek_init() &priority=5 {
|
||||
Log::create_stream(LDAP::LDAP_LOG, [$columns=Message, $ev=log_ldap, $path="ldap", $policy=log_policy]);
|
||||
Log::create_stream(LDAP::LDAP_SEARCH_LOG, [$columns=Search, $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_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];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#############################################################################
|
||||
@if (Version::at_least("5.2.0"))
|
||||
event analyzer_confirmation_info(atype: AllAnalyzers::Tag, info: AnalyzerConfirmationInfo) {
|
||||
if ( atype == Analyzer::ANALYZER_SPICY_LDAP_TCP ) {
|
||||
info$c$ldap_proto = "tcp";
|
||||
}
|
||||
}
|
||||
@else @if (Version::at_least("4.2.0"))
|
||||
event analyzer_confirmation(c: connection, atype: AllAnalyzers::Tag, aid: count) {
|
||||
@else
|
||||
event protocol_confirmation(c: connection, atype: Analyzer::Tag, aid: count) {
|
||||
@endif
|
||||
|
||||
if ( atype == Analyzer::ANALYZER_SPICY_LDAP_TCP ) {
|
||||
c$ldap_proto = "tcp";
|
||||
}
|
||||
|
||||
}
|
||||
@endif
|
||||
#############################################################################
|
||||
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);
|
||||
|
||||
if ( result != LDAP::ResultCode_Undef ) {
|
||||
if ( ! c$ldap_searches[message_id]?$result )
|
||||
c$ldap_searches[message_id]$result = set();
|
||||
add c$ldap_searches[message_id]$result[RESULT_CODES[result]];
|
||||
}
|
||||
|
||||
if ( diagnostic_message != "" ) {
|
||||
if ( ! c$ldap_searches[message_id]?$diagnostic_message )
|
||||
c$ldap_searches[message_id]$diagnostic_message = vector();
|
||||
c$ldap_searches[message_id]$diagnostic_message += diagnostic_message;
|
||||
}
|
||||
|
||||
if (( ! c$ldap_searches[message_id]?$proto ) && c?$ldap_proto)
|
||||
c$ldap_searches[message_id]$proto = c$ldap_proto;
|
||||
|
||||
Log::write(LDAP::LDAP_SEARCH_LOG, c$ldap_searches[message_id]);
|
||||
delete c$ldap_searches[message_id];
|
||||
|
||||
} else if (opcode !in OPCODES_SEARCH) {
|
||||
set_session(c, message_id, opcode);
|
||||
|
||||
if ( ! c$ldap_messages[message_id]?$opcode )
|
||||
c$ldap_messages[message_id]$opcode = set();
|
||||
add c$ldap_messages[message_id]$opcode[PROTOCOL_OPCODES[opcode]];
|
||||
|
||||
if ( result != LDAP::ResultCode_Undef ) {
|
||||
if ( ! c$ldap_messages[message_id]?$result )
|
||||
c$ldap_messages[message_id]$result = set();
|
||||
add c$ldap_messages[message_id]$result[RESULT_CODES[result]];
|
||||
}
|
||||
|
||||
if ( diagnostic_message != "" ) {
|
||||
if ( ! c$ldap_messages[message_id]?$diagnostic_message )
|
||||
c$ldap_messages[message_id]$diagnostic_message = vector();
|
||||
c$ldap_messages[message_id]$diagnostic_message += diagnostic_message;
|
||||
}
|
||||
|
||||
if ( object != "" ) {
|
||||
if ( ! c$ldap_messages[message_id]?$object )
|
||||
c$ldap_messages[message_id]$object = vector();
|
||||
c$ldap_messages[message_id]$object += object;
|
||||
}
|
||||
|
||||
if ( argument != "" ) {
|
||||
if ( ! c$ldap_messages[message_id]?$argument )
|
||||
c$ldap_messages[message_id]$argument = vector();
|
||||
if ("bind simple" in c$ldap_messages[message_id]$opcode && !default_capture_password)
|
||||
c$ldap_messages[message_id]$argument += "REDACTED";
|
||||
else
|
||||
c$ldap_messages[message_id]$argument += argument;
|
||||
}
|
||||
|
||||
if (opcode in OPCODES_FINISHED) {
|
||||
|
||||
if ((BIND_SIMPLE in c$ldap_messages[message_id]$opcode) ||
|
||||
(BIND_SASL in c$ldap_messages[message_id]$opcode)) {
|
||||
# don't have both "bind" and "bind <method>" in the operations list
|
||||
delete c$ldap_messages[message_id]$opcode[PROTOCOL_OPCODES[LDAP::ProtocolOpcode_BIND_REQUEST]];
|
||||
}
|
||||
|
||||
if (( ! c$ldap_messages[message_id]?$proto ) && c?$ldap_proto)
|
||||
c$ldap_messages[message_id]$proto = c$ldap_proto;
|
||||
|
||||
Log::write(LDAP::LDAP_LOG, c$ldap_messages[message_id]);
|
||||
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]?$scope )
|
||||
c$ldap_searches[message_id]$scope = set();
|
||||
add c$ldap_searches[message_id]$scope[SEARCH_SCOPES[scope]];
|
||||
}
|
||||
|
||||
if ( deref != LDAP::SearchDerefAlias_Undef ) {
|
||||
if ( ! c$ldap_searches[message_id]?$deref )
|
||||
c$ldap_searches[message_id]$deref = set();
|
||||
add c$ldap_searches[message_id]$deref[SEARCH_DEREF_ALIASES[deref]];
|
||||
}
|
||||
|
||||
if ( base_object != "" ) {
|
||||
if ( ! c$ldap_searches[message_id]?$base_object )
|
||||
c$ldap_searches[message_id]$base_object = vector();
|
||||
c$ldap_searches[message_id]$base_object += 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]?$opcode )
|
||||
c$ldap_messages[message_id]$opcode = set();
|
||||
|
||||
if (authType == LDAP::BindAuthType_BIND_AUTH_SIMPLE) {
|
||||
add c$ldap_messages[message_id]$opcode[BIND_SIMPLE];
|
||||
} else if (authType == LDAP::BindAuthType_BIND_AUTH_SASL) {
|
||||
add c$ldap_messages[message_id]$opcode[BIND_SASL];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#############################################################################
|
||||
event connection_state_remove(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$opcode) || (BIND_SASL in m$opcode)) {
|
||||
# don't have both "bind" and "bind <method>" in the operations list
|
||||
delete m$opcode[PROTOCOL_OPCODES[LDAP::ProtocolOpcode_BIND_REQUEST]];
|
||||
}
|
||||
|
||||
if (( ! m?$proto ) && c?$ldap_proto)
|
||||
m$proto = c$ldap_proto;
|
||||
|
||||
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) {
|
||||
|
||||
if (( ! s?$proto ) && c?$ldap_proto)
|
||||
s$proto = c$ldap_proto;
|
||||
|
||||
Log::write(LDAP::LDAP_SEARCH_LOG, s);
|
||||
}
|
||||
}
|
||||
delete c$ldap_searches;
|
||||
}
|
||||
|
||||
}
|
78
src/spicy/spicy-ldap/cmake/FindSpicyPlugin.cmake
Normal file
78
src/spicy/spicy-ldap/cmake/FindSpicyPlugin.cmake
Normal file
|
@ -0,0 +1,78 @@
|
|||
# Find the Spicy plugin to get access to the infrastructure it provides.
|
||||
#
|
||||
# While most of the actual CMake logic for building analyzers comes with the Spicy
|
||||
# plugin for Zeek, this code bootstraps us by asking "spicyz" for the plugin's
|
||||
# location. Either make sure that "spicyz" is in PATH, set the environment
|
||||
# variable SPICYZ to point to its location, or set variable ZEEK_SPICY_ROOT
|
||||
# in either CMake or environment to point to its installation or build
|
||||
# directory.
|
||||
#
|
||||
# This exports:
|
||||
#
|
||||
# SPICY_PLUGIN_FOUND True if plugin and all dependencies were found
|
||||
# SPICYZ Path to spicyz
|
||||
# SPICY_PLUGIN_VERSION Version string of plugin
|
||||
# SPICY_PLUGIN_VERSION_NUMBER Numerical version number of plugin
|
||||
|
||||
# Runs `spicyz` with the flags given as second argument and stores the output in the variable named
|
||||
# by the first argument.
|
||||
function (run_spicycz output)
|
||||
execute_process(COMMAND "${SPICYZ}" ${ARGN} OUTPUT_VARIABLE output_
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
string(STRIP "${output_}" output_)
|
||||
set(${output} "${output_}" PARENT_SCOPE)
|
||||
endfunction ()
|
||||
|
||||
# Checks that the Spicy plugin version it at least the given version.
|
||||
function (spicy_plugin_require_version version)
|
||||
string(REGEX MATCH "([0-9]*)\.([0-9]*)\.([0-9]*).*" _ ${version})
|
||||
math(EXPR version_number "${CMAKE_MATCH_1} * 10000 + ${CMAKE_MATCH_2} * 100 + ${CMAKE_MATCH_3}")
|
||||
|
||||
if ("${SPICY_PLUGIN_VERSION_NUMBER}" LESS "${version_number}")
|
||||
message(FATAL_ERROR "Package requires at least Spicy plugin version ${version}, "
|
||||
"have ${SPICY_PLUGIN_VERSION}")
|
||||
endif ()
|
||||
endfunction ()
|
||||
|
||||
###
|
||||
### Main
|
||||
###
|
||||
|
||||
if (NOT SPICYZ)
|
||||
set(SPICYZ "$ENV{SPICYZ}")
|
||||
endif ()
|
||||
|
||||
if (NOT SPICYZ)
|
||||
# Support an in-tree Spicy build.
|
||||
find_program(
|
||||
spicyz spicyz
|
||||
HINTS ${ZEEK_SPICY_ROOT}/bin ${ZEEK_SPICY_ROOT}/build/bin $ENV{ZEEK_SPICY_ROOT}/bin
|
||||
$ENV{ZEEK_SPICY_ROOT}/build/bin ${PROJECT_SOURCE_DIR}/../../build/bin)
|
||||
set(SPICYZ "${spicyz}")
|
||||
endif ()
|
||||
|
||||
message(STATUS "spicyz: ${SPICYZ}")
|
||||
|
||||
if (SPICYZ)
|
||||
set(SPICYZ "${SPICYZ}" CACHE PATH "" FORCE) # make sure it's in the cache
|
||||
|
||||
run_spicycz(SPICY_PLUGIN_VERSION "--version")
|
||||
run_spicycz(SPICY_PLUGIN_VERSION_NUMBER "--version-number")
|
||||
message(STATUS "Zeek plugin version: ${SPICY_PLUGIN_VERSION}")
|
||||
|
||||
run_spicycz(spicy_plugin_path "--print-plugin-path")
|
||||
set(spicy_plugin_cmake_path "${spicy_plugin_path}/cmake")
|
||||
message(STATUS "Zeek plugin CMake path: ${spicy_plugin_cmake_path}")
|
||||
|
||||
list(PREPEND CMAKE_MODULE_PATH "${spicy_plugin_cmake_path}")
|
||||
find_package(Zeek REQUIRED)
|
||||
find_package(Spicy REQUIRED)
|
||||
zeek_print_summary()
|
||||
spicy_print_summary()
|
||||
|
||||
include(ZeekSpicyAnalyzerSupport)
|
||||
endif ()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(SpicyPlugin DEFAULT_MSG SPICYZ ZEEK_FOUND)
|
13
src/spicy/spicy-ldap/tests/analyzer/attributes.zeek
Normal file
13
src/spicy/spicy-ldap/tests/analyzer/attributes.zeek
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
|
||||
|
||||
# @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.
|
||||
|
||||
@load analyzer
|
||||
|
||||
redef LDAP::default_log_search_attributes = T;
|
5
src/spicy/spicy-ldap/tests/analyzer/availability.zeek
Normal file
5
src/spicy/spicy-ldap/tests/analyzer/availability.zeek
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
|
||||
|
||||
# @TEST-EXEC: zeek -NN | grep -q ANALYZER_SPICY_LDAP_TCP
|
||||
#
|
||||
# @TEST-DOC: Check that LDAP (TCP) is analyzer is available.
|
12
src/spicy/spicy-ldap/tests/analyzer/basic.zeek
Normal file
12
src/spicy/spicy-ldap/tests/analyzer/basic.zeek
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
|
||||
|
||||
# @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.
|
||||
|
||||
@load analyzer
|
11
src/spicy/spicy-ldap/tests/analyzer/diff_port.zeek
Normal file
11
src/spicy/spicy-ldap/tests/analyzer/diff_port.zeek
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
|
||||
|
||||
# @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.
|
||||
|
||||
@load analyzer
|
131
src/spicy/spicy-ldap/tests/analyzer/functions.spicy
Normal file
131
src/spicy/spicy-ldap/tests/analyzer/functions.spicy
Normal file
|
@ -0,0 +1,131 @@
|
|||
# @TEST-EXEC: spicyc -j -d -L ${DIST}/analyzer %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();
|
23
src/spicy/spicy-ldap/tests/analyzer/log_policy.zeek
Normal file
23
src/spicy/spicy-ldap/tests/analyzer/log_policy.zeek
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
|
||||
|
||||
# @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.
|
||||
|
||||
@load analyzer
|
||||
|
||||
hook LDAP::log_policy(rec: LDAP::Message, id: Log::ID, filter: Log::Filter)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
hook LDAP::log_policy_search(rec: LDAP::Search, id: Log::ID,
|
||||
filter: Log::Filter)
|
||||
{
|
||||
break;
|
||||
}
|
13
src/spicy/spicy-ldap/tests/analyzer/sasl-encrypted.zeek
Normal file
13
src/spicy/spicy-ldap/tests/analyzer/sasl-encrypted.zeek
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
|
||||
|
||||
# @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.
|
||||
|
||||
@load analyzer
|
|
@ -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_object > 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.
|
|
@ -0,0 +1,12 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp spicy_ldap_tcp 181.520479 258 188 RSTO 0 ShADdR 8 590 4 360 -
|
|
@ -0,0 +1,13 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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 proto message_id version opcode result diagnostic_message object argument
|
||||
#types time string addr port addr port string int int set[string] set[string] vector[string] vector[string] vector[string]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp 1 3 bind simple success - xxxxxxxxxxx@xx.xxx.xxxxx.net REDACTED
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp 3 3 bind simple success - CN=xxxxxxxx\x2cOU=Users\x2cOU=Accounts\x2cDC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net REDACTED
|
|
@ -0,0 +1,12 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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 proto message_id scope deref base_object result_count result diagnostic_message filter attributes
|
||||
#types time string addr port addr port string int set[string] set[string] vector[string] count set[string] vector[string] string vector[string]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp 2 tree always DC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net 1 success - (&(objectclass=*)(sAMAccountName=xxxxxxxx)) sAMAccountName
|
12
src/spicy/spicy-ldap/tests/baseline/analyzer.basic/conn.log
Normal file
12
src/spicy/spicy-ldap/tests/baseline/analyzer.basic/conn.log
Normal 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.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp spicy_ldap_tcp 181.520479 258 188 RSTO 0 ShADdR 8 590 4 360 -
|
13
src/spicy/spicy-ldap/tests/baseline/analyzer.basic/ldap.log
Normal file
13
src/spicy/spicy-ldap/tests/baseline/analyzer.basic/ldap.log
Normal file
|
@ -0,0 +1,13 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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 proto message_id version opcode result diagnostic_message object argument
|
||||
#types time string addr port addr port string int int set[string] set[string] vector[string] vector[string] vector[string]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp 1 3 bind simple success - xxxxxxxxxxx@xx.xxx.xxxxx.net REDACTED
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp 3 3 bind simple success - CN=xxxxxxxx\x2cOU=Users\x2cOU=Accounts\x2cDC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net REDACTED
|
|
@ -0,0 +1,12 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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 proto message_id scope deref base_object result_count result diagnostic_message filter attributes
|
||||
#types time string addr port addr port string int set[string] set[string] vector[string] count set[string] vector[string] string vector[string]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp 2 tree always DC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net 1 success - (&(objectclass=*)(sAMAccountName=xxxxxxxx)) -
|
|
@ -0,0 +1,2 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
|
@ -0,0 +1,12 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 32681 tcp spicy_ldap_tcp 181.520479 258 188 RSTO 0 ShADdR 8 590 4 360 -
|
|
@ -0,0 +1,13 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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 proto message_id version opcode result diagnostic_message object argument
|
||||
#types time string addr port addr port string int int set[string] set[string] vector[string] vector[string] vector[string]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 32681 tcp 1 3 bind simple success - xxxxxxxxxxx@xx.xxx.xxxxx.net REDACTED
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 32681 tcp 3 3 bind simple success - CN=xxxxxxxx\x2cOU=Users\x2cOU=Accounts\x2cDC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net REDACTED
|
|
@ -0,0 +1,12 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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 proto message_id scope deref base_object result_count result diagnostic_message filter attributes
|
||||
#types time string addr port addr port string int set[string] set[string] vector[string] count set[string] vector[string] string vector[string]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 32681 tcp 2 tree always DC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net 1 success - (&(objectclass=*)(sAMAccountName=xxxxxxxx)) -
|
|
@ -0,0 +1,12 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp spicy_ldap_tcp 181.520479 258 188 RSTO 0 ShADdR 8 590 4 360 -
|
|
@ -0,0 +1,2 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
|
@ -0,0 +1,4 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
LDAP::Messages {
|
||||
payload: test string
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 172.31.1.104 3116 172.31.1.101 389 tcp spicy_ldap_tcp 0.813275 1814 2391 S1 0 ShADd 6 2062 4 2559 -
|
|
@ -0,0 +1,12 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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 proto message_id version opcode result diagnostic_message object argument
|
||||
#types time string addr port addr port string int int set[string] set[string] vector[string] vector[string] vector[string]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 172.31.1.104 3116 172.31.1.101 389 tcp 215 3 bind SASL success - - GSS-SPNEGO
|
|
@ -0,0 +1,12 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#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 proto message_id scope deref base_object result_count result diagnostic_message filter attributes
|
||||
#types time string addr port addr port string int set[string] set[string] vector[string] count set[string] vector[string] string vector[string]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 172.31.1.104 3116 172.31.1.101 389 tcp 213 base never - 1 success - (objectclass=*) -
|
|
@ -0,0 +1,12 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
### NOTE: This file has been sorted with diff-sort.
|
||||
#separator \x09
|
||||
#set_separator ,
|
||||
#empty_field (empty)
|
||||
#unset_field -
|
||||
#path ldap_search
|
||||
#open XXXX-XX-XX-XX-XX-XX
|
||||
#fields uid filter base_object
|
||||
#types string string vector[string]
|
||||
#close XXXX-XX-XX-XX-XX-XX
|
||||
CHhAvVGS1DHFjwGM9 (departmentNumber:2.16.840.1.113730.3.3.2.46.1:=>=N4709) DC=matrix\x2cDC=local
|
34
src/spicy/spicy-ldap/tests/btest.cfg
Normal file
34
src/spicy/spicy-ldap/tests/btest.cfg
Normal file
|
@ -0,0 +1,34 @@
|
|||
[btest]
|
||||
MinVersion = 0.66
|
||||
|
||||
TestDirs = analyzer
|
||||
TmpDir = %(testbase)s/.tmp
|
||||
BaselineDir = %(testbase)s/baseline
|
||||
IgnoreDirs = .svn CVS .tmp Baseline Failing traces Traces
|
||||
IgnoreFiles = .DS_Store *.pcap data.* *.dat *.wmv *.der *.tmp *.swp .*.swp #* CMakeLists.txt
|
||||
|
||||
[environment]
|
||||
DIST=%(testbase)s/..
|
||||
PATH=%(testbase)s/../tests/scripts:`spicyz --print-plugin-path`/tests/scripts:%(default_path)s
|
||||
SCRIPTS=`spicyz --print-plugin-path`/tests/Scripts
|
||||
ZEEK_SPICY_MODULE_PATH=%(testbase)s/../build/spicy-modules
|
||||
TEST_DIFF_CANONIFIER=`spicyz --print-plugin-path`/tests/Scripts/canonify-zeek-log-sorted
|
||||
TRACES=%(testbase)s/traces
|
||||
ZEEKPATH=%(testbase)s/..:`zeek-config --zeekpath`
|
||||
ZEEK_SEED_FILE=`spicyz --print-plugin-path`/tests/random.seed
|
||||
|
||||
# Set variables to well-defined state.
|
||||
LANG=C
|
||||
LC_ALL=C
|
||||
TZ=UTC
|
||||
CC=
|
||||
CXX=
|
||||
CFLAGS=
|
||||
CPPFLAGS=
|
||||
CXXFLAGS=
|
||||
LDFLAGS=
|
||||
DYLDFLAGS=
|
||||
|
||||
[environment-installation]
|
||||
ZEEK_SPICY_MODULE_PATH=
|
||||
ZEEKPATH=`%(testbase)s/scripts/zeek-path-install`
|
5
src/spicy/spicy-ldap/tests/scripts/zeek-path-install
Executable file
5
src/spicy/spicy-ldap/tests/scripts/zeek-path-install
Executable file
|
@ -0,0 +1,5 @@
|
|||
#! /bin/sh
|
||||
#
|
||||
# Assembles the Zeek path for testing the installed version (can't do that in btest.cfg directly).
|
||||
|
||||
echo $(spicyz --print-scripts-path):$(zkg config script_dir):$(zeek-config --zeekpath)
|
15
src/spicy/spicy-ldap/tests/traces/README
Normal file
15
src/spicy/spicy-ldap/tests/traces/README
Normal file
|
@ -0,0 +1,15 @@
|
|||
The test suite comes with a set of traces collected from a variety of
|
||||
places that we document below. While these traces are all coming from
|
||||
public sources, please note that they may carry their own licenses.
|
||||
We collect them here for convenience only.
|
||||
|
||||
- [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 ldap-simpleauth.pcap -o ldap-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 cyphertext changed to `\x00`
|
||||
- ldap-issue-32.pcapng: Provided by GH user martinvanhensbergen,
|
||||
<https://github.com/zeek/spicy-ldap/issues/23>
|
BIN
src/spicy/spicy-ldap/tests/traces/issue-32.pcapng
Normal file
BIN
src/spicy/spicy-ldap/tests/traces/issue-32.pcapng
Normal file
Binary file not shown.
BIN
src/spicy/spicy-ldap/tests/traces/ldap-issue-32.pcapng
Normal file
BIN
src/spicy/spicy-ldap/tests/traces/ldap-issue-32.pcapng
Normal file
Binary file not shown.
BIN
src/spicy/spicy-ldap/tests/traces/ldap-krb5-sign-seal-01.pcap
Normal file
BIN
src/spicy/spicy-ldap/tests/traces/ldap-krb5-sign-seal-01.pcap
Normal file
Binary file not shown.
BIN
src/spicy/spicy-ldap/tests/traces/ldap-simpleauth-diff-port.pcap
Normal file
BIN
src/spicy/spicy-ldap/tests/traces/ldap-simpleauth-diff-port.pcap
Normal file
Binary file not shown.
BIN
src/spicy/spicy-ldap/tests/traces/ldap-simpleauth.pcap
Normal file
BIN
src/spicy/spicy-ldap/tests/traces/ldap-simpleauth.pcap
Normal file
Binary file not shown.
21
src/spicy/spicy-ldap/zkg.meta
Normal file
21
src/spicy/spicy-ldap/zkg.meta
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
summary = LDAP analyzer
|
||||
description = An LDAP analyzer based on Spicy
|
||||
script_dir = analyzer
|
||||
plugin_dir = build/spicy-modules
|
||||
build_command = mkdir -p build && cd build && SPICYZ=$(command -v spicyz || echo %(package_base)s/spicy-plugin/build/bin/spicyz) cmake .. && cmake --build .
|
||||
test_command = cd tests && PATH=$(zkg config plugin_dir)/packages/spicy-plugin/bin:$PATH btest -d -j $(nproc)
|
||||
|
||||
# We depend on spicy-plugin>=1.2.2, but currently cannot express that
|
||||
# dependency due to zeek/package-manager#106 which has been fixed in
|
||||
# zkg-2.12.0, but has not been released with zeek yet.
|
||||
# depends = spicy-plugin >=1.2.2
|
||||
|
||||
[template]
|
||||
source = package-template-spicy
|
||||
version = master
|
||||
zkg_version = 2.12.0
|
||||
|
||||
[template_vars]
|
||||
namespace = LDAP
|
||||
name = Messages
|
|
@ -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_object
|
||||
#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
|
Loading…
Add table
Add a link
Reference in a new issue