diff --git a/README.md b/README.md index 325ceabf..b9c5a225 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Currently, the following analyzers are included: - DNS [1] - HTTP [1] - IPSec +- LDAP - OpenVPN - PNG - Portable Executable (PE) [2] diff --git a/analyzer/__load__.zeek b/analyzer/__load__.zeek index 061ff90b..99b27b98 100644 --- a/analyzer/__load__.zeek +++ b/analyzer/__load__.zeek @@ -5,6 +5,7 @@ @load ./protocol/dns @load ./protocol/http @load ./protocol/ipsec +@load ./protocol/ldap @load ./protocol/openvpn @load ./protocol/tftp @load ./protocol/wireguard diff --git a/analyzer/protocol/CMakeLists.txt b/analyzer/protocol/CMakeLists.txt index 8f8b51a9..3d2c670e 100644 --- a/analyzer/protocol/CMakeLists.txt +++ b/analyzer/protocol/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(dhcp) add_subdirectory(dns) add_subdirectory(http) add_subdirectory(ipsec) +add_subdirectory(ldap) add_subdirectory(openvpn) add_subdirectory(tftp) add_subdirectory(wireguard) diff --git a/analyzer/protocol/ldap/CMakeLists.txt b/analyzer/protocol/ldap/CMakeLists.txt new file mode 100644 index 00000000..a0ea1632 --- /dev/null +++ b/analyzer/protocol/ldap/CMakeLists.txt @@ -0,0 +1,3 @@ +# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. + +spicy_add_analyzer(ldap ldap.spicy ldap_zeek.spicy ldap.evt) diff --git a/analyzer/protocol/ldap/__load__.zeek b/analyzer/protocol/ldap/__load__.zeek new file mode 100644 index 00000000..6a29e295 --- /dev/null +++ b/analyzer/protocol/ldap/__load__.zeek @@ -0,0 +1,3 @@ +# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. + +@load ./main diff --git a/analyzer/protocol/ldap/asn1.spicy b/analyzer/protocol/ldap/asn1.spicy new file mode 100644 index 00000000..cb06f86f --- /dev/null +++ b/analyzer/protocol/ldap/asn1.spicy @@ -0,0 +1,286 @@ +# 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 { + var type_: ASN1Type; + var class: ASN1Class; + var constructed: bool; + + : bitfield(8) { + num: 0..4; + constructed: 5; + class: 6..7; + } { + self.type_ = ASN1Type($$.num); + self.class = ASN1Class($$.class); + self.constructed = cast($$.constructed); + } +}; + +#- 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 value: string = ""; + + : ASN1OctetString(len, tag.constructed) { + 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.value = $$.value.decode(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.value = $$.value.decode(hilti::Charset::UTF8); + } + } + } +}; + +#- 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; + 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($$) &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) + &convert=$$.value; + + 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(self.head.tag.type_); + } + + }; +}; diff --git a/analyzer/protocol/ldap/ldap.evt b/analyzer/protocol/ldap/ldap.evt new file mode 100644 index 00000000..2a97f22d --- /dev/null +++ b/analyzer/protocol/ldap/ldap.evt @@ -0,0 +1,39 @@ +# 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}; + +# TODO: LDAP can also use UDP transport + +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); + +on ldap::SearchResultEntry -> event ldap::searchres($conn, + message.messageID, + self.objectName); diff --git a/analyzer/protocol/ldap/ldap.spicy b/analyzer/protocol/ldap/ldap.spicy new file mode 100644 index 00000000..9e0a51fd --- /dev/null +++ b/analyzer/protocol/ldap/ldap.spicy @@ -0,0 +1,536 @@ +# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. + +module ldap; + +import asn1; + +# 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, + # TODO: remove once zeek/spicy-plugin#35 is fixed + NOT_SET = 255 +}; + +#- 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, + # TODO: remove once zeek/spicy-plugin#35 is fixed + NOT_SET = 255 +}; + +#----------------------------------------------------------------------------- +public type Result = unit { + code: asn1::ASN1Message(True) &convert=cast(cast($$.body.num_value)) + &default=ResultCode::NOT_SET; + 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 { + : Message[]; +}; + +public type Message = unit { + var messageID: int64; + var opcode: ProtocolOpcode = ProtocolOpcode::NOT_SET; + var applicationBytes: bytes; + var unsetResultDefault: Result; + var result: Result& = self.unsetResultDefault; + var obj: string = ""; + var arg: string = ""; + + : 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(cast($$.body.seq.submessages[1].application_id)); + self.applicationBytes = $$.body.seq.submessages[1].application_data; + } + } + } + + switch ( self.opcode ) { + ProtocolOpcode::BIND_REQUEST -> BIND_REQUEST: BindRequest(self) &parse-from=self.applicationBytes; + ProtocolOpcode::BIND_RESPONSE -> BIND_RESPONSE: BindResponse(self) &parse-from=self.applicationBytes; + ProtocolOpcode::UNBIND_REQUEST -> UNBIND_REQUEST: UnbindRequest(self) &parse-from=self.applicationBytes; + ProtocolOpcode::SEARCH_REQUEST -> SEARCH_REQUEST: SearchRequest(self) &parse-from=self.applicationBytes; + ProtocolOpcode::SEARCH_RESULT_ENTRY -> SEARCH_RESULT_ENTRY: SearchResultEntry(self) &parse-from=self.applicationBytes; + ProtocolOpcode::SEARCH_RESULT_DONE -> SEARCH_RESULT_DONE: SearchResultDone(self) &parse-from=self.applicationBytes; + ProtocolOpcode::MODIFY_REQUEST -> MODIFY_REQUEST: ModifyRequest(self) &parse-from=self.applicationBytes; + ProtocolOpcode::MODIFY_RESPONSE -> MODIFY_RESPONSE: ModifyResponse(self) &parse-from=self.applicationBytes; + ProtocolOpcode::ADD_RESPONSE -> ADD_RESPONSE: AddResponse(self) &parse-from=self.applicationBytes; + ProtocolOpcode::DEL_REQUEST -> DEL_REQUEST: DelRequest(self) &parse-from=self.applicationBytes; + ProtocolOpcode::DEL_RESPONSE -> DEL_RESPONSE: DelResponse(self) &parse-from=self.applicationBytes; + ProtocolOpcode::MOD_DN_RESPONSE -> MOD_DN_RESPONSE: ModDNResponse(self) &parse-from=self.applicationBytes; + ProtocolOpcode::COMPARE_RESPONSE -> COMPARE_RESPONSE: CompareResponse(self) &parse-from=self.applicationBytes; + ProtocolOpcode::ABANDON_REQUEST -> ABANDON_REQUEST: AbandonRequest(self) &parse-from=self.applicationBytes; + + # TODO: not yet implemented + # ProtocolOpcode::ADD_REQUEST -> ADD_REQUEST: AddRequest(self) &parse-from=self.applicationBytes; + # ProtocolOpcode::COMPARE_REQUEST -> COMPARE_REQUEST: CompareRequest(self) &parse-from=self.applicationBytes; + # ProtocolOpcode::EXTENDED_REQUEST -> EXTENDED_REQUEST: ExtendedRequest(self) &parse-from=self.applicationBytes; + # ProtocolOpcode::EXTENDED_RESPONSE -> EXTENDED_RESPONSE: ExtendedResponse(self) &parse-from=self.applicationBytes; + # ProtocolOpcode::INTERMEDIATE_RESPONSE -> INTERMEDIATE_RESPONSE: IntermediateResponse(self) &parse-from=self.applicationBytes; + # ProtocolOpcode::MOD_DN_REQUEST -> MOD_DN_REQUEST: ModDNRequest(self) &parse-from=self.applicationBytes; + # ProtocolOpcode::SEARCH_RESULT_REFERENCE -> SEARCH_RESULT_REFERENCE: SearchResultReference(self) &parse-from=self.applicationBytes; + }; + # TODO: add support for switch-level &parse-from/&parse-at + # https://github.com/zeek/spicy/issues/913 +} &requires=((self?.messageID) && (self?.opcode) && (self.opcode != ProtocolOpcode::NOT_SET)); + +#----------------------------------------------------------------------------- +# Bind Operation +# https://tools.ietf.org/html/rfc4511#section-4.2 + +public type BindAuthType = enum { + BIND_AUTH_SIMPLE = 0, + BIND_AUTH_SASL = 3, + # TODO: remove once zeek/spicy-plugin#35 is fixed + NOT_SET = 127 +}; + +type SaslCredentials = unit() { + mechanism: asn1::ASN1Message(True) &convert=$$.body.str_value; + # TODO: if we want to parse the (optional) credentials string +}; + +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::NOT_SET; + var authData: bytes = b""; + var simpleCreds: string = ""; + + : asn1::ASN1Message(True) { + if ($$?.application_id) { + self.authType = cast(cast($$.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::NOT_SET)); + +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, + # TODO: remove once zeek/spicy-plugin#35 is fixed + NOT_SET = 255 +}; + +public type SearchDerefAlias = enum { + DEREF_NEVER = 0, + DEREF_IN_SEARCHING = 1, + DEREF_FINDING_BASE = 2, + DEREF_ALWAYS = 3, + # TODO: remove once zeek/spicy-plugin#35 is fixed + NOT_SET = 255 +}; + +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, + # TODO: remove once zeek/spicy-plugin#35 is fixed + NOT_SET = 255 +}; + +type AttributeSelection = unit { + var attributes: vector; + + # 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; + } + } + } +}; + +type SubstringFilter = unit { + var ftype: string = ""; + var substrings: asn1::ASN1Message; + + : 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.ftype = $$.body.seq.submessages[0].body.str_value; + } + if ($$.body.seq.submessages[1].head.tag.type_ == asn1::ASN1Type::Sequence) { + self.substrings = $$.body.seq.submessages[1]; + } + } + # TODO: if we want to descend deeper into the substrings filter + # if (self?.substrings) { + # + #} + } +}; + +type SearchFilter = unit { + var filterType: FilterType = FilterType::NOT_SET; + var filterBytes: bytes = b""; + var filterLen: uint64 = 0; + + : asn1::ASN1Message(True) { + if ($$?.application_id) { + self.filterType = cast(cast($$.application_id)); + self.filterBytes = $$.application_data; + self.filterLen = $$.head.len.len; + } else { + self.filterType = FilterType::FILTER_INVALID; + } + } + + # TODO: parse search request filter as per + # https://tools.ietf.org/html/rfc4511#section-4.5.1.7 + # This descent gets pretty involved... I wonder what is + # the best way to represent this as a string in a log. + # I've just left some of them as asn1::ASN1Message for now. + + switch ( self.filterType ) { + FilterType::FILTER_AND -> FILTER_AND: asn1::ASN1Message(True) + &parse-from=self.filterBytes; + FilterType::FILTER_OR -> FILTER_OR: asn1::ASN1Message(True) + &parse-from=self.filterBytes; + FilterType::FILTER_NOT -> FILTER_NOT: SearchFilter() + &parse-from=self.filterBytes; + FilterType::FILTER_EQ -> FILTER_EQ: AttributeValueAssertion() + &parse-from=self.filterBytes; + FilterType::FILTER_SUBSTR -> FILTER_SUBSTR: SubstringFilter() + &parse-from=self.filterBytes; + FilterType::FILTER_GE -> FILTER_GE: AttributeValueAssertion() + &parse-from=self.filterBytes; + FilterType::FILTER_LE -> FILTER_LE: AttributeValueAssertion() + &parse-from=self.filterBytes; + FilterType::FILTER_PRESENT -> FILTER_PRESENT: asn1::ASN1OctetString(self.filterLen, False) + &convert=$$.value.decode(hilti::Charset::ASCII) + &parse-from=self.filterBytes; + FilterType::FILTER_APPROX -> FILTER_APPROX: AttributeValueAssertion() + &parse-from=self.filterBytes; + FilterType::FILTER_EXT -> FILTER_EXT: asn1::ASN1Message(True) + &parse-from=self.filterBytes; + }; +}; + +type SearchRequest = unit(inout message: Message) { + baseObject: asn1::ASN1Message(True) &convert=$$.body.str_value { + message.obj = self.baseObject; + } + scope: asn1::ASN1Message(True) &convert=cast(cast($$.body.num_value)) + &default=SearchScope::NOT_SET { + message.arg = "%s" % self.scope; + } + deref: asn1::ASN1Message(True) &convert=cast(cast($$.body.num_value)) + &default=SearchDerefAlias::NOT_SET; + 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; + attributes: AttributeSelection; +}; + +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) { +# +# }; diff --git a/analyzer/protocol/ldap/ldap_zeek.spicy b/analyzer/protocol/ldap/ldap_zeek.spicy new file mode 100644 index 00000000..c3411503 --- /dev/null +++ b/analyzer/protocol/ldap/ldap_zeek.spicy @@ -0,0 +1,14 @@ +# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. + +module ldap_zeek; + +import zeek; +import ldap; + +on ldap::Message::%done { + zeek::confirm_protocol(); +} + +on ldap::Message::%error { + zeek::reject_protocol("error while parsing LDAP message"); +} diff --git a/analyzer/protocol/ldap/main.zeek b/analyzer/protocol/ldap/main.zeek new file mode 100644 index 00000000..13320e76 --- /dev/null +++ b/analyzer/protocol/ldap/main.zeek @@ -0,0 +1,467 @@ +# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. + +module ldap; + +export { + redef enum Log::ID += { LDAP_LOG, + LDAP_SEARCH_LOG }; + + ############################################################################# + # 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; + + }; + + # 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"]); + Log::create_stream(ldap::LDAP_SEARCH_LOG, [$columns=Search, $ev=log_ldap_search, $path="ldap_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]; + } + +} + +############################################################################# +event protocol_confirmation(c: connection, atype: Analyzer::Tag, aid: count) &priority=5 { + + if ( atype == Analyzer::ANALYZER_SPICY_LDAP_TCP ) { + c$ldap_proto = "tcp"; + } + +} + +############################################################################# +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_NOT_SET ) { + 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_NOT_SET ) { + 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(); + 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 " 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) { + + set_session(c, message_id, ldap::ProtocolOpcode_SEARCH_REQUEST); + + if ( scope != ldap::SearchScope_NOT_SET ) { + 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_NOT_SET ) { + 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; + } + +} + +############################################################################# +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 " 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; + } + +} + diff --git a/tests/Baseline/protocol.ldap.basic/conn.log b/tests/Baseline/protocol.ldap.basic/conn.log new file mode 100644 index 00000000..5ab37878 --- /dev/null +++ b/tests/Baseline/protocol.ldap.basic/conn.log @@ -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 local_orig local_resp 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 bool bool 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 - diff --git a/tests/Baseline/protocol.ldap.basic/ldap.log b/tests/Baseline/protocol.ldap.basic/ldap.log new file mode 100644 index 00000000..021bf93c --- /dev/null +++ b/tests/Baseline/protocol.ldap.basic/ldap.log @@ -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 passwor8d1 +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 /dev/rdsk/c0t0d0s0 diff --git a/tests/Baseline/protocol.ldap.basic/ldap_search.log b/tests/Baseline/protocol.ldap.basic/ldap_search.log new file mode 100644 index 00000000..8e44022e --- /dev/null +++ b/tests/Baseline/protocol.ldap.basic/ldap_search.log @@ -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 +#types time string addr port addr port string int set[string] set[string] vector[string] count set[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 - diff --git a/tests/Traces/README b/tests/Traces/README index d0e41308..e2aaf5cc 100644 --- a/tests/Traces/README +++ b/tests/Traces/README @@ -19,6 +19,9 @@ IPSec - [ipsec-ikev1-isakmp-main-mode.pcap](https://www.cloudshark.org/captures/ff740838f1c2) - [ipsec-ikev1-isakmp-aggressive-mode.pcap](https://www.cloudshark.org/captures/e51f5c8a6b24) +LDAP +- [ldap-simpleauth.pcap](https://github.com/arkime/arkime/blob/main/tests/pcap/ldap-simpleauth.pcap) + OpenVPN - [openvpn-tcp-tls-auth.pcap](https://wiki.wireshark.org/SampleCaptures?action=AttachFile&do=get&target=OpenVPN_TCP_tls-auth.pcapng) - openvpn.pcap (self-made) diff --git a/tests/Traces/ldap-simpleauth.pcap b/tests/Traces/ldap-simpleauth.pcap new file mode 100644 index 00000000..1cf904a0 Binary files /dev/null and b/tests/Traces/ldap-simpleauth.pcap differ diff --git a/tests/protocol/ldap/availability.zeek b/tests/protocol/ldap/availability.zeek new file mode 100644 index 00000000..68cb285b --- /dev/null +++ b/tests/protocol/ldap/availability.zeek @@ -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. diff --git a/tests/protocol/ldap/basic.zeek b/tests/protocol/ldap/basic.zeek new file mode 100644 index 00000000..8e03fb0c --- /dev/null +++ b/tests/protocol/ldap/basic.zeek @@ -0,0 +1,10 @@ +# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. + +# @TEST-EXEC: ${ZEEK} -C -r ${TRACES}/ldap-simpleauth.pcap %INPUT +# @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 spicy-analyzers/protocol/ldap