From de1c46a21ccbed8d2bb7a1bc472e7b3ab52cc259 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Mon, 10 May 2021 11:01:59 -0600 Subject: [PATCH 01/20] Topic/ldap (#1) * added LDAP stubbed out files * stubbing PDU types * work in progress (found asn1.spicy module) * more asn1 work in progress * more asn1 work in progress * more asn1 work in progress * more asn1 work in progress * more asn1 work in progress; compiling but some stuff has been commented out. need to examine one by one * more asn1 work in progress; compiling but some stuff has been commented out. need to examine one by one * asn1 work in progress * asn1 work in progress * stub out debug output * work in progress * added debug back in * more work on bind request * more work in progress on bind request * more work on ldap bindRequest * more work in progress, figururing out application ASN.1 BER class. see https://ldap.com/ldapv3-wire-protocol-reference-asn1-ber/ for a big help * more work in progress, figururing out application ASN.1 * more work in progress, figururing out application ASN.1 * working on bindrequest * more work on ldap * wip on ldap/spicy * comment out specifying vector length * more work in progress on ldap * LDAP work in progress * Fix indents and remove wrapper. * Spaces to tabs. * Switch to spaces. * Update source for trace file. * Fix various vector parsing issues. Also remove typing from the_type since we don't know all cases yet. * Added Cisco vendor IDs. * Update baselines. * Add another vendor id. * work in progress with zeek integration plumbing: * plumbing in place for logging * more logging work in progress * more logging work in progress * comment out some stuff * redue verbosity * print out numbers of unparsed bytes * debugging ldap * specify message length so we don't parse more than we should per-message * ldap work in progress * push 'catch-all' bytes &eod array to the sub-messages * debug print out the list of unparsed data * need to parse ldap messages in an array * Adding result * don't explicitly set a bool for hasResult * explicitly set a bool for hasResult * add column * use unset value instead of a separate boolean * progress on ldap.log * added more results * more work on ldap log * make op and result set of enum instead of vector of enum * add comments * need EOL * formatting and work on ldap processor * more work on ldap * working on putting search into its own separate log file * working on putting search into its own separate log file * more work on search filtering * work in progress on the ldap processor; asn1 can now be recursive, although I'm not using it yet because it's a whole mindshift from what i've been doing * Added more debug printing * Added more debug printing * for now store application types in a big 'bytes' array * Added more debug printing * recursive parsing for ldap via asn1 * great progress on ldap * great progress on ldap * great progress on ldap * Allow success with empty entries * formatting, and use &convert to decomplicate member access * use strings instead of enums for log output Co-authored-by: Keith Jones Co-authored-by: Robin Sommer --- analyzer/__load__.zeek | 1 + analyzer/protocol/CMakeLists.txt | 1 + analyzer/protocol/ldap/CMakeLists.txt | 1 + analyzer/protocol/ldap/__load__.zeek | 1 + analyzer/protocol/ldap/asn1.spicy | 339 ++++++++++++++ analyzer/protocol/ldap/ldap.evt | 34 ++ analyzer/protocol/ldap/ldap.spicy | 603 +++++++++++++++++++++++++ analyzer/protocol/ldap/ldap.zeek | 363 +++++++++++++++ analyzer/protocol/ldap/ldap_zeek.spicy | 12 + 9 files changed, 1355 insertions(+) create mode 100644 analyzer/protocol/ldap/CMakeLists.txt create mode 100644 analyzer/protocol/ldap/__load__.zeek create mode 100644 analyzer/protocol/ldap/asn1.spicy create mode 100644 analyzer/protocol/ldap/ldap.evt create mode 100644 analyzer/protocol/ldap/ldap.spicy create mode 100644 analyzer/protocol/ldap/ldap.zeek create mode 100644 analyzer/protocol/ldap/ldap_zeek.spicy 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..30526401 --- /dev/null +++ b/analyzer/protocol/ldap/CMakeLists.txt @@ -0,0 +1 @@ +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..8b137891 --- /dev/null +++ b/analyzer/protocol/ldap/__load__.zeek @@ -0,0 +1 @@ + diff --git a/analyzer/protocol/ldap/asn1.spicy b/analyzer/protocol/ldap/asn1.spicy new file mode 100644 index 00000000..8c5f671a --- /dev/null +++ b/analyzer/protocol/ldap/asn1.spicy @@ -0,0 +1,339 @@ +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; + +# set asn1::DEBUG_PRINT to True to enable debug printing of ASN.1 body values +const DEBUG_PRINT = False; +# DEBUG_SPACES is used with *.depth to print nested ASN.1 structures +const DEBUG_SPACES = " "; + +#- 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, + CONTEXT_SPECIFIC = 2, + PRIVATE = 3 +}; + +#- ASN.1 tag definition (including length) ------------------------------------ + +type LengthType = unit { + + data : bitfield(8) { + num: 0..6; + islong: 7; + }; + + var len: uint64; + + var tag_len: uint8; + + switch ( self.data.islong ) { + 0 -> : b"" { + self.len = self.data.num; self.tag_len = 1; + } + 1 -> length_parse: bytes &size=self.data.num + &convert=$$.to_uint(spicy::ByteOrder::Network) { + self.len = self.length_parse; + self.tag_len = self.data.num + 1; + } + }; +}; + +type ASN1Tag = unit { + var tpe: ASN1Type; + var len: uint8 = 1; + var class: ASN1Class; + var constructed: bool; + + data : bitfield(8) { + num: 0..4; + constructed: 5; + class: 6..7; + }; + + on %done { + self.tpe = ASN1Type(self.data.num); + self.class = ASN1Class(self.data.class); + self.constructed = cast(self.data.constructed); + } +}; + +#- ASN.1 bit string ----------------------------------------------------------- +# https://www.obj-sys.com/asn1tutorial/node10.html + +type ASN1BitString = unit(len: uint64, constructed: bool) { + unused_bits: uint8; + value_bits: bytes &size=(len - 1); + + # TODO - constructed form + # `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) { + var len: uint64; + value: bytes &size = len; + + # TODO - constructed form + + on %done { + self.len = len; + } +}; + +#- ASN.1 various string types ------------------------------------------------- +# https://www.obj-sys.com/asn1tutorial/node124.html + +type ASN1String = unit(tag: ASN1Tag, len: uint64) { + var value: string = ""; + + octetstring: ASN1OctetString(len, tag.constructed) { + + if ((tag.tpe == ASN1Type::PrintableString) || + (tag.tpe == ASN1Type::GeneralizedTime) || + (tag.tpe == ASN1Type::UTCTime)) { + self.value = self.octetstring.value.decode(hilti::Charset::ASCII); + + } else if (tag.tpe == ASN1Type::UTF8String) { + self.value = self.octetstring.value.decode(hilti::Charset::UTF8); + + } else { + # this would include ASN1Type::BMPString and ASN1Type::UniversalString because + # hilti::Charset::UTF16BE and hilti::Charset::UTF32BE don't exist any more + throw "Unsupported ASN1String type"; + } + } + +}; + +#- ASN.1 OID ------------------------------------------------------------------ +# https://www.obj-sys.com/asn1tutorial/node124.html + +type oidnibble = unit { + data : bitfield(8) { + num: 0..6; + more: 7; + }; +}; + +type ASN1ObjectIdentifier = unit(len: uint64) { + var oid: vector; + var temp: uint64; + var oidstring: string; + + first: uint8 if ( len >= 1 ) { + self.temp = self.first / 40; + self.oid.push_back( self.temp ); + self.oidstring = "%d" % (self.temp); + self.temp = self.first % 40; + self.oid.push_back( self.temp ); + self.oidstring = self.oidstring + ".%d" % (self.temp); + self.temp = 0; + } + + sublist: oidnibble[len - 1] foreach { + self.temp = ( self.temp<<7 ) | $$.data.num; + if ( $$.data.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; + + var total_length: uint64; + + len: LengthType { + self.total_length = self.len.len + self.tag.len + self.len.tag_len; + } +}; + +#- ASN.1 message body --------------------------------------------------------- + +public type ASN1Body = unit(head: ASN1Header, recursive: bool, depth: uint32) { + switch ( head.tag.tpe ) { + + ASN1Type::BOOLEAN -> bool_value: uint8 &convert=cast($$) { + if (head.len.len != 1) { + throw "ASN1Type::BOOLEAN detected with length != 1"; + } + } + + ASN1Type::INTEGER, + ASN1Type::ENUMERATED -> num_value: bytes &size=head.len.len + &convert=$$.to_int(spicy::ByteOrder::Big); + + ASN1Type::NULLVAL -> : void { + if (head.len.len != 0) { + throw "ASN1Type::NULLVAL detected with non-zero length"; + } + } + + ASN1Type::BITSTRING -> bitstring: ASN1BitString(head.len.len, head.tag.constructed); + + ASN1Type::OCTETSTRING -> octetstring: ASN1OctetString(head.len.len, head.tag.constructed) + &convert=$$.value.decode(hilti::Charset::ASCII); + + ASN1Type::OBJECTIDENTIFIER -> objectidentifier: ASN1ObjectIdentifier(head.len.len) + &convert=$$.oidstring; + + ASN1Type::PrintableString, + ASN1Type::GeneralizedTime, + ASN1Type::UTCTime, + ASN1Type::UTF8String, + ASN1Type::BMPString, + ASN1Type::UniversalString -> asn1string: ASN1String(head.tag, head.len.len) + &convert=$$.value; + + ASN1Type::SEQUENCE, ASN1Type::SET -> seq: ASN1SubMessages(head.len.len, depth+1) if (recursive); + + * -> : bytes &size=head.len.len { + throw "Unsupported ASN1Body type"; + } + }; + + on %done { + if (DEBUG_PRINT == True) { + if (self?.bool_value) { + print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "BOOLEAN"), self.bool_value; + } else if (self?.num_value) { + print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "NUMBER"), self.num_value; + } else if (self?.bitstring) { + print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "BITSTRING"), self.bitstring, head.tag.constructed; + } else if (self?.octetstring) { + print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "OCTETSTRING"), self.octetstring, head.tag.constructed; + } else if (self?.objectidentifier) { + print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "OBJECTIDENTIFIER"), self.objectidentifier; + } else if (self?.asn1string) { + print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "STRING"), self.asn1string; + } else if (self?.seq) { + print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "SEQUENCE|SET"), |self.seq.submessages|, head.len.len; + } else { + print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe); + } + } + } +}; + +#- ASN.1 array of ASN.1 sequence/set sub-messages (up to msgLen bytes) -------- + +public type ASN1SubMessages = unit(msgLen: uint64, depth: uint32) { + submessages: ASN1Message(True, depth)[] &eod; +} &size=msgLen; + +#- ASN.1 message with header and body ----------------------------------------- +# UNIVERSAL or APPLICATION/CONTEXT_SPECIFIC/PRIVATE +# - if UNIVERSAL, body:ASN1Body is parsed +# - else, application_data:bytes stores data array + +public type ASN1Message = unit(recursive: bool, depth: uint32) { + var depth: uint32 = depth; + + head: ASN1Header; + body: ASN1Body(self.head, recursive, depth) if ( self.head.tag.class == ASN1Class::UNIVERSAL ); + + var application_id: int32; + application_data: bytes &size=self.head.len.len if ( self.head.tag.class != ASN1Class::UNIVERSAL ) { + self.application_id = self.head.tag.data.num; + } + + on %done { + if (DEBUG_PRINT == True) { + if ( self.head.tag.class != ASN1Class::UNIVERSAL ) { + print "%.*s%s %d %s %d" % ((depth*2)+1, DEBUG_SPACES, self.head.tag.class, self.application_id, self.head.tag.constructed, self.head.len.len), self.head; + } + } + } +}; + +#- ASN.1 message (headless) --------------------------------------------------- +# same rules as ASN1Message for body/application_data + +public type ASN1MessageHeadless = unit(head: ASN1Header, recursive: bool, depth: uint32) { + var depth: uint32 = depth; + + body: ASN1Body(head, recursive, depth) if ( head.tag.class == ASN1Class::UNIVERSAL ); + + var application_id: int32; + application_data: bytes &size=head.len.len if ( head.tag.class != ASN1Class::UNIVERSAL ) { + self.application_id = head.tag.data.num; + } + + on %done { + if (DEBUG_PRINT == True) { + if ( head.tag.class != ASN1Class::UNIVERSAL ) { + print "%.*s%s %d %s %d" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, self.application_id, head.tag.constructed, head.len.len), head; + } + } + } +}; + +#- ASN.1 message helper units for recursive (set/sequence) vs. not ------------ + +public type ASN1MessageBaseRecursive = unit { + msg : ASN1Message(True, 0); +}; + +public type ASN1MessageBase = unit { + msg : ASN1Message(False, 0); +}; diff --git a/analyzer/protocol/ldap/ldap.evt b/analyzer/protocol/ldap/ldap.evt new file mode 100644 index 00000000..6734c1e1 --- /dev/null +++ b/analyzer/protocol/ldap/ldap.evt @@ -0,0 +1,34 @@ +protocol analyzer spicy::ldap_tcp over TCP: + parse with ldap::Messages, + ports {389/tcp, 3268/tcp}; + +# TODO: UDP hasn't been tested/implemented yet +# TODO: test this, will it be Message or Messages? +# protocol analyzer spicy::ldap_udp over UDP: +# parse with ldap::Messages, +# ports {389/udp, 3268/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::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..842fc200 --- /dev/null +++ b/analyzer/protocol/ldap/ldap.spicy @@ -0,0 +1,603 @@ +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, + 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, + 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, + 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, + AFFECTS_MULTIPLE_DSAS = 71, + OTHER = 80, + NOT_SET = 255 +}; + +#----------------------------------------------------------------------------- +public type Result = unit(depth: uint32) { + code: asn1::ASN1Message(True, depth) &convert=cast(cast($$.body.num_value)) + &default=ResultCode::NOT_SET; + matchedDN: asn1::ASN1Message(True, depth) &convert=$$.body.octetstring + &default=""; + diagnosticMessage: asn1::ASN1Message(True, depth) &convert=$$.body.octetstring + &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 = ""; + + seq: asn1::ASN1Message(True, 0) { + if ((self.seq.head.tag.tpe == asn1::ASN1Type::SEQUENCE) && + (self.seq.body?.seq) && + (|self.seq.body.seq.submessages| >= 2)) { + if (self.seq.body.seq.submessages[0].body?.num_value) { + self.messageID = self.seq.body.seq.submessages[0].body.num_value; + } + if (self.seq.body.seq.submessages[1]?.application_id) { + self.opcode = cast(cast(self.seq.body.seq.submessages[1].application_id)); + self.applicationBytes = self.seq.body.seq.submessages[1].application_data; + } + } + if ((! self?.messageID) || + (! self?.opcode) || + (self.opcode == ProtocolOpcode::NOT_SET)) { + throw "LDAP message is not ASN.1 sequence as described in RFC 4511"; + } + } + + 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_REQUEST -> ADD_REQUEST: AddRequest(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_REQUEST -> MOD_DN_REQUEST: ModDNRequest(self) &parse-from=self.applicationBytes; + ProtocolOpcode::MOD_DN_RESPONSE -> MOD_DN_RESPONSE: ModDNResponse(self) &parse-from=self.applicationBytes; + ProtocolOpcode::COMPARE_REQUEST -> COMPARE_REQUEST: CompareRequest(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; + ProtocolOpcode::SEARCH_RESULT_REFERENCE -> SEARCH_RESULT_REFERENCE: SearchResultReference(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; + }; + +}; + +#----------------------------------------------------------------------------- +# Bind Operation +# https://tools.ietf.org/html/rfc4511#section-4.2 + +type BindAuthType = enum { + BIND_AUTH_SIMPLE = 0, + BIND_AUTH_SASL = 3, + NOT_SET = 127 +}; + +type SaslCredentials = unit(depth: uint32) { + mechanism: asn1::ASN1Message(True, depth) &convert=$$.body.octetstring; + # TODO: if we want to parse the (optional) credentials string +}; + +type BindRequest = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + version: asn1::ASN1Message(True, self.depth) &convert=$$.body.num_value; + name: asn1::ASN1Message(True, self.depth) &convert=$$.body.octetstring; + var authType: BindAuthType = BindAuthType::NOT_SET; + var simpleCreds: string = ""; + + authentication: asn1::ASN1Message(True, self.depth) { + if (self.authentication?.application_id) { + self.authType = cast(cast(self.authentication.application_id)); + } + if ((! self?.authType) || (self.authType == BindAuthType::NOT_SET)) { + throw "BindRequest is not ASN.1 sequence as described in RFC 4511"; + } + if ((self.authType == BindAuthType::BIND_AUTH_SIMPLE) && + (|self.authentication.application_data| > 0)) { + self.simpleCreds = self.authentication.application_data.decode(); + } + } + saslCreds: SaslCredentials(self.depth+1) &parse-from=self.authentication.application_data if (self.authType == BindAuthType::BIND_AUTH_SASL); + + on %done { + message.obj = self.name; + if (|self.simpleCreds| > 0) { + message.arg = self.simpleCreds; + } else if (self.authType == BindAuthType::BIND_AUTH_SASL) { + message.arg = self.saslCreds.mechanism; + } + print "BindRequest", message.messageID, self.version, self.name, + self.authType, message.obj, message.arg; + } +}; + +type BindResponse = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + result: Result(self.depth+1); + + # TODO: if we want to parse SASL credentials returned + + on %done { + message.result = self.result; + print "BindResponse", message.messageID, self; + } +}; + +#----------------------------------------------------------------------------- +# Unbind Operation +# https://tools.ietf.org/html/rfc4511#section-4.3 + +type UnbindRequest = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + on %done { + print "UnbindRequest", message.messageID, self; + } +}; + +#----------------------------------------------------------------------------- +# Search Operation +# https://tools.ietf.org/html/rfc4511#section-4.5 + +public type SearchScope = enum { + SEARCH_BASE = 0, + SEARCH_SINGLE = 1, + SEARCH_TREE = 2, + NOT_SET = 255 +}; + +public type SearchDerefAlias = enum { + DEREF_NEVER = 0, + DEREF_IN_SEARCHING = 1, + DEREF_FINDING_BASE = 2, + DEREF_ALWAYS = 3, + 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, + NOT_SET = 255 +}; + +type AttributeSelection = unit(depth: uint32) { + 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. + seq: asn1::ASN1Message(True, depth) { + if ((self.seq.head.tag.tpe == asn1::ASN1Type::SEQUENCE) && + (self.seq.body?.seq) && + (|self.seq.body.seq.submessages| > 0)) { + for (i in self.seq.body.seq.submessages) { + if (i.body?.octetstring) { + self.attributes.push_back(i.body.octetstring); + } + } + } + } +}; + +type AttributeValueAssertion = unit(depth: uint32) { + var desc: string = ""; + var val: string = ""; + + seq: asn1::ASN1Message(True, depth) { + if ((self.seq.head.tag.tpe == asn1::ASN1Type::SEQUENCE) && + (self.seq.body?.seq) && + (|self.seq.body.seq.submessages| >= 2)) { + if (self.seq.body.seq.submessages[0].body?.octetstring) { + self.desc = self.seq.body.seq.submessages[0].body.octetstring; + } + if (self.seq.body.seq.submessages[1].body?.octetstring) { + self.val = self.seq.body.seq.submessages[1].body.octetstring; + } + } + } + +}; + +type SubstringFilter = unit(depth: uint32) { + var ftype: string = ""; + var substrings: asn1::ASN1Message; + + seq: asn1::ASN1Message(True, depth) { + if ((self.seq.head.tag.tpe == asn1::ASN1Type::SEQUENCE) && + (self.seq.body?.seq) && + (|self.seq.body.seq.submessages| >= 2)) { + if (self.seq.body.seq.submessages[0].body?.octetstring) { + self.ftype = self.seq.body.seq.submessages[0].body.octetstring; + } + if (self.seq.body.seq.submessages[1].head.tag.tpe == asn1::ASN1Type::SEQUENCE) { + self.substrings = self.seq.body.seq.submessages[1]; + } + } + # TODO: if we want to descend deeper into the substrings filter + # if (self?.substrings) { + # + #} + } + +}; + +type SearchFilter = unit(depth: uint32) { + var filterType: FilterType = FilterType::NOT_SET; + var filterBytes: bytes = b""; + + filter : asn1::ASN1Message(True, depth) { + if (self.filter?.application_id) { + self.filterType = cast(cast(self.filter.application_id)); + self.filterBytes = self.filter.application_data; + } 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, depth+1) + &parse-from=self.filterBytes; + FilterType::FILTER_OR -> FILTER_OR: asn1::ASN1Message(True, depth+1) + &parse-from=self.filterBytes; + FilterType::FILTER_NOT -> FILTER_NOT: SearchFilter(depth+1) + &parse-from=self.filterBytes; + FilterType::FILTER_EQ -> FILTER_EQ: AttributeValueAssertion(depth+1) + &parse-from=self.filterBytes; + FilterType::FILTER_SUBSTR -> FILTER_SUBSTR: SubstringFilter(depth+1) + &parse-from=self.filterBytes; + FilterType::FILTER_GE -> FILTER_GE: AttributeValueAssertion(depth+1) + &parse-from=self.filterBytes; + FilterType::FILTER_LE -> FILTER_LE: AttributeValueAssertion(depth+1) + &parse-from=self.filterBytes; + FilterType::FILTER_PRESENT -> FILTER_PRESENT: asn1::ASN1OctetString(self.filter.head.len.len, False) + &convert=$$.value.decode(hilti::Charset::ASCII) + &parse-from=self.filterBytes; + FilterType::FILTER_APPROX -> FILTER_APPROX: AttributeValueAssertion(depth+1) + &parse-from=self.filterBytes; + FilterType::FILTER_EXT -> FILTER_EXT: asn1::ASN1Message(True, depth+1) + &parse-from=self.filterBytes; + * -> unknownFilter: void; + }; +}; + +type SearchRequest = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + baseObject: asn1::ASN1Message(True, self.depth) &convert=$$.body.octetstring; + scope: asn1::ASN1Message(True, self.depth) &convert=cast(cast($$.body.num_value)) + &default=SearchScope::NOT_SET; + deref: asn1::ASN1Message(True, self.depth) &convert=cast(cast($$.body.num_value)) + &default=SearchDerefAlias::NOT_SET; + sizeLimit: asn1::ASN1Message(True, self.depth) &convert=$$.body.num_value &default=0; + timeLimit: asn1::ASN1Message(True, self.depth) &convert=$$.body.num_value &default=0; + typesOnly: asn1::ASN1Message(True, self.depth) &convert=$$.body.bool_value &default=False; + filter: SearchFilter(self.depth); + attributes: AttributeSelection(self.depth); + + on %done { + message.obj = self.baseObject; + message.arg = "%s" % self.scope; + print "SearchRequest", message.messageID, self; + } +}; + +type SearchResultEntry = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + objectName: asn1::ASN1Message(True, self.depth) &convert=$$.body.octetstring; + # TODO: if we want to descend down into PartialAttributeList + attributes: asn1::ASN1Message(True, self.depth); + + on %done { + message.obj = self.objectName; + print "SearchResultEntry", message.messageID, self; + } +}; + +type SearchResultDone = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + result: Result(self.depth+1); + + on %done { + message.result = self.result; + print "SearchResultDone", message.messageID, self; + } +}; + +type SearchResultReference = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + # TODO: SearchResultReference + # The SearchResultReference is of the same data type as the Referral. + + on %done { + print "SearchResultReference", message.messageID, self; + } +}; + +#----------------------------------------------------------------------------- +# Modify Operation +# https://tools.ietf.org/html/rfc4511#section-4.6 + +type ModifyRequest = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + objectName: asn1::ASN1Message(True, self.depth) &convert=$$.body.octetstring; + + # TODO: parse changes + + on %done { + message.obj = self.objectName; + print "ModifyRequest", message.messageID, self; + } +}; + +type ModifyResponse = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + result: Result(self.depth+1); + + on %done { + message.result = self.result; + print "ModifyResponse", message.messageID, self; + } +}; + +#----------------------------------------------------------------------------- +# Add Operation +# https://tools.ietf.org/html/rfc4511#section-4.7 + +type AddRequest = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + # TODO: add request sequence + + on %done { + print "AddRequest", message.messageID, self; + } +}; + +type AddResponse = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + result: Result(self.depth+1); + + on %done { + message.result = self.result; + print "AddResponse", message.messageID, self; + } +}; + +#----------------------------------------------------------------------------- +# Delete Operation +# https://tools.ietf.org/html/rfc4511#section-4.8 + +type DelRequest = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + objectName: asn1::ASN1Message(True, self.depth) &convert=$$.body.octetstring; + + on %done { + message.obj = self.objectName; + print "DelRequest", message.messageID, self; + } +}; + +type DelResponse = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + result: Result(self.depth+1); + + on %done { + message.result = self.result; + print "DelResponse", message.messageID, self; + } +}; + +#----------------------------------------------------------------------------- +# Modify DN Operation +# https://tools.ietf.org/html/rfc4511#section-4.8 + +type ModDNRequest = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + # TODO: parse mod DN request sequence + + on %done { + print "ModDNRequest", message.messageID, self; + } +}; + +type ModDNResponse = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + result: Result(self.depth+1); + + on %done { + message.result = self.result; + print "ModDNResponse", message.messageID, self; + } +}; + +#----------------------------------------------------------------------------- +# Compare Operation +# https://tools.ietf.org/html/rfc4511#section-4.10 + +type CompareRequest = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + # TODO: parse compare request entry/ava sequence + + on %done { + print "CompareRequest", message.messageID, self; + } +}; + +type CompareResponse = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + result: Result(self.depth+1); + + on %done { + message.result = self.result; + print "CompareResponse", message.messageID, self; + } +}; + +#----------------------------------------------------------------------------- +# Abandon Operation +# https://tools.ietf.org/html/rfc4511#section-4.11 + +type AbandonRequest = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + # messageID: asn1::ASN1Message(True, self.depth) &convert=$$.body.num_value; + + on %done { + # TODO: can't do string -> bytes + # but see https://github.com/zeek/spicy/issues/911 for next release + # message.obj = "%d" % (self.messageID); + print "AbandonRequest", message.messageID, self; + } +}; + +#----------------------------------------------------------------------------- +# Extended Operation +# https://tools.ietf.org/html/rfc4511#section-4.12 + +type ExtendedRequest = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + # TODO: parse ExtendedRequest sequence + + on %done { + print "ExtendedRequest", message.messageID, self; + } +}; + +type ExtendedResponse = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + # TODO: parse ExtendedRequest sequence + + on %done { + # message.result = self.result; + print "ExtendedResponse", message.messageID, self; + } +}; + +#----------------------------------------------------------------------------- +# IntermediateResponse Message +# https://tools.ietf.org/html/rfc4511#section-4.13 + +type IntermediateResponse = unit(inout message: Message) { + var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; + + # TODO: parse IntermediateResponse sequence + + on %done { + print "IntermediateResponse", message.messageID, self; + } +}; diff --git a/analyzer/protocol/ldap/ldap.zeek b/analyzer/protocol/ldap/ldap.zeek new file mode 100644 index 00000000..8a2ad90b --- /dev/null +++ b/analyzer/protocol/ldap/ldap.zeek @@ -0,0 +1,363 @@ +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; + + # Message ID + message_id: 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; + + # 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 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_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_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_AFFECTS_MULTIPLE_DSAS] = "affects multiple DSAs", + [ldap::ResultCode_OTHER] = "other" + } &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_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 { + + # todo: do we really need to do anything here? + +} + +############################################################################# +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; + } + + 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) { + 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 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) { + Log::write(ldap::LDAP_LOG, m); + } + } + delete c$ldap_messages; + } + + if ( c?$ldap_searches && (|c$ldap_searches| > 0) ) { + for ( [mid], s in c$ldap_searches ) { + if (mid > 0) { + Log::write(ldap::LDAP_SEARCH_LOG, s); + } + } + delete c$ldap_searches; + } + +} diff --git a/analyzer/protocol/ldap/ldap_zeek.spicy b/analyzer/protocol/ldap/ldap_zeek.spicy new file mode 100644 index 00000000..53cff452 --- /dev/null +++ b/analyzer/protocol/ldap/ldap_zeek.spicy @@ -0,0 +1,12 @@ +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"); +} From 3949c90e849f15386be854223acde885bd376fc1 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Mon, 10 May 2021 12:18:58 -0600 Subject: [PATCH 02/20] Added ldap version --- analyzer/protocol/ldap/ldap.evt | 7 +++++++ analyzer/protocol/ldap/ldap.spicy | 2 +- analyzer/protocol/ldap/ldap.zeek | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/analyzer/protocol/ldap/ldap.evt b/analyzer/protocol/ldap/ldap.evt index 6734c1e1..8c6e1f95 100644 --- a/analyzer/protocol/ldap/ldap.evt +++ b/analyzer/protocol/ldap/ldap.evt @@ -20,6 +20,13 @@ on ldap::Message -> event ldap::message($conn, 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, diff --git a/analyzer/protocol/ldap/ldap.spicy b/analyzer/protocol/ldap/ldap.spicy index 842fc200..0d5a6fc8 100644 --- a/analyzer/protocol/ldap/ldap.spicy +++ b/analyzer/protocol/ldap/ldap.spicy @@ -152,7 +152,7 @@ public type Message = unit { # Bind Operation # https://tools.ietf.org/html/rfc4511#section-4.2 -type BindAuthType = enum { +public type BindAuthType = enum { BIND_AUTH_SIMPLE = 0, BIND_AUTH_SASL = 3, NOT_SET = 127 diff --git a/analyzer/protocol/ldap/ldap.zeek b/analyzer/protocol/ldap/ldap.zeek index 8a2ad90b..7e66ee40 100644 --- a/analyzer/protocol/ldap/ldap.zeek +++ b/analyzer/protocol/ldap/ldap.zeek @@ -21,6 +21,9 @@ export { # 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; @@ -337,6 +340,20 @@ event ldap::searchres(c: connection, 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; +} + ############################################################################# event connection_state_remove(c: connection) { @@ -361,3 +378,4 @@ event connection_state_remove(c: connection) { } } + From 599c7a9286b16db85728d6f5a9d0683627c78786 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Mon, 10 May 2021 13:07:16 -0600 Subject: [PATCH 03/20] added more ldap codes --- analyzer/protocol/ldap/ldap.spicy | 36 +++++++++++++++++++++++++++++ analyzer/protocol/ldap/ldap.zeek | 38 ++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/analyzer/protocol/ldap/ldap.spicy b/analyzer/protocol/ldap/ldap.spicy index 0d5a6fc8..e257b6cc 100644 --- a/analyzer/protocol/ldap/ldap.spicy +++ b/analyzer/protocol/ldap/ldap.spicy @@ -43,6 +43,7 @@ public type ResultCode = enum { 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, @@ -65,14 +66,49 @@ public type ResultCode = enum { 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, NOT_SET = 255 }; diff --git a/analyzer/protocol/ldap/ldap.zeek b/analyzer/protocol/ldap/ldap.zeek index 7e66ee40..23b21464 100644 --- a/analyzer/protocol/ldap/ldap.zeek +++ b/analyzer/protocol/ldap/ldap.zeek @@ -124,6 +124,7 @@ export { [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", @@ -146,14 +147,49 @@ export { [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_OTHER] = "other" + [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 = { From b5f36c7fd315ba8924445c9dcad6111008b782ec Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Mon, 10 May 2021 14:14:39 -0600 Subject: [PATCH 04/20] don't save both 'bind' and 'bind simple'/'bind SASL' in the operations list --- analyzer/protocol/ldap/ldap.zeek | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/analyzer/protocol/ldap/ldap.zeek b/analyzer/protocol/ldap/ldap.zeek index 23b21464..e137dff1 100644 --- a/analyzer/protocol/ldap/ldap.zeek +++ b/analyzer/protocol/ldap/ldap.zeek @@ -114,6 +114,9 @@ export { [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", @@ -327,6 +330,11 @@ event ldap::message(c: connection, } 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]]; + } Log::write(ldap::LDAP_LOG, c$ldap_messages[message_id]); delete c$ldap_messages[message_id]; } @@ -388,6 +396,16 @@ event ldap::bindreq(c: connection, 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]; + } + } ############################################################################# @@ -398,6 +416,10 @@ event connection_state_remove(c: connection) { 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]]; + } Log::write(ldap::LDAP_LOG, m); } } From 9594f6d4ccdac1c72e859c987f9d70d4e583a76c Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Mon, 10 May 2021 15:46:37 -0600 Subject: [PATCH 05/20] rename ldap.zeek to main.zeek --- README.md | 1 + analyzer/protocol/ldap/__load__.zeek | 2 +- analyzer/protocol/ldap/{ldap.zeek => main.zeek} | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename analyzer/protocol/ldap/{ldap.zeek => main.zeek} (100%) 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/protocol/ldap/__load__.zeek b/analyzer/protocol/ldap/__load__.zeek index 8b137891..a10fe855 100644 --- a/analyzer/protocol/ldap/__load__.zeek +++ b/analyzer/protocol/ldap/__load__.zeek @@ -1 +1 @@ - +@load ./main diff --git a/analyzer/protocol/ldap/ldap.zeek b/analyzer/protocol/ldap/main.zeek similarity index 100% rename from analyzer/protocol/ldap/ldap.zeek rename to analyzer/protocol/ldap/main.zeek From cd5c670d4f0d582bcf35f8f9eca4789dc5b83d10 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Tue, 11 May 2021 08:47:34 -0600 Subject: [PATCH 06/20] added ldap test --- tests/Baseline/protocol.ldap.basic/conn.log | 12 ++++++++++++ tests/Baseline/protocol.ldap.basic/ldap.log | 13 +++++++++++++ .../Baseline/protocol.ldap.basic/ldap_search.log | 12 ++++++++++++ tests/Traces/README | 3 +++ tests/Traces/ldap-simpleauth.pcap | Bin 0 -> 1390 bytes tests/protocol/ldap/availability.zeek | 3 +++ tests/protocol/ldap/basic.zeek | 8 ++++++++ 7 files changed, 51 insertions(+) create mode 100644 tests/Baseline/protocol.ldap.basic/conn.log create mode 100644 tests/Baseline/protocol.ldap.basic/ldap.log create mode 100644 tests/Baseline/protocol.ldap.basic/ldap_search.log create mode 100644 tests/Traces/ldap-simpleauth.pcap create mode 100644 tests/protocol/ldap/availability.zeek create mode 100644 tests/protocol/ldap/basic.zeek 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..a73b8923 --- /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 message_id version opcode result diagnostic_message object argument +#types time string addr port addr port 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 1 3 bind simple success - xxxxxxxxxxx@xx.xxx.xxxxx.net passwor8d1 +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 3 3 bind simple success - CN=xxxxxxxx\x2cOU=Users\x2cOU=Accounts\x2cDC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net /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..31738e32 --- /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 message_id scope deref base_object result_count result diagnostic_message +#types time string addr port addr port 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 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 0000000000000000000000000000000000000000..1cf904a0061a0a2b143f4c2de209dddb12e78da2 GIT binary patch literal 1390 zcmbtUL1+_E5S_o-#*m~X#?poouu2gNX|gTRdN4J$wKwY?tUc62V|GCr&1yESf#wj= zsy%rT#DhT*6cjv2ky^nLn;s0g2-b^G5B5?icoGW5)R~{OiR(%ZelnB)H~DYgdo#(W zrB{yy5Mg(P0T>?c%=C`BvH(N4r|`voer_%DynG#?#|EbWf@xK(7?5T$g1pu?WS2`O$^%*D~@{#Q>^nj zR~UlU^7#r^rm__uaswyL{5lw=Qsp(9xRCf;@-g9vT**ctX%jmy9Or3g=b91NOnPQ<+tJp9}6POU45WRx~p;XGp^UhoOj5gVs zU}qMA0lpZ6N1eAF+Jy- zq)lOJ^SpnO+Z;mbwKh+C7S=SfS@TwGP9lAUY>tVcfnk(TUcNIUp%Gn8=(MRY)644q zp^-Q35YO#K&fQeNUZbkgU_muDIDnGw( literal 0 HcmV?d00001 diff --git a/tests/protocol/ldap/availability.zeek b/tests/protocol/ldap/availability.zeek new file mode 100644 index 00000000..a33fa753 --- /dev/null +++ b/tests/protocol/ldap/availability.zeek @@ -0,0 +1,3 @@ +# @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..f074fd11 --- /dev/null +++ b/tests/protocol/ldap/basic.zeek @@ -0,0 +1,8 @@ +# @TEST-EXEC: ${ZEEK} -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 From 78ea9824c679939a0822b1a1cb00856d127a304e Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Tue, 11 May 2021 08:52:32 -0600 Subject: [PATCH 07/20] update changes --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index b67e204c..79a91961 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ +0.2.11 | 2021-05-11 10:00:00 -0600 + + * Add LDAP analyzer. (Seth Grover, INL) + 0.2.10-4 | 2021-05-05 11:49:06 +0200 * Add Aruba Networks vendor ID info. (Keith Jones, Corelight) From a666fb5817d2d607763c0f553dd6691437000f7e Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Tue, 11 May 2021 09:02:22 -0600 Subject: [PATCH 08/20] fix LDAP test (specify -C to zeek) --- tests/protocol/ldap/basic.zeek | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/protocol/ldap/basic.zeek b/tests/protocol/ldap/basic.zeek index f074fd11..d9280591 100644 --- a/tests/protocol/ldap/basic.zeek +++ b/tests/protocol/ldap/basic.zeek @@ -1,4 +1,4 @@ -# @TEST-EXEC: ${ZEEK} -r ${TRACES}/ldap-simpleauth.pcap %INPUT +# @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 From 7cce9bbea6ec5804105e2e53651b03296a9f7a39 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Tue, 11 May 2021 14:43:22 -0600 Subject: [PATCH 09/20] void fields never store a value and cannot be named --- analyzer/protocol/ldap/ldap.spicy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzer/protocol/ldap/ldap.spicy b/analyzer/protocol/ldap/ldap.spicy index e257b6cc..982b3f87 100644 --- a/analyzer/protocol/ldap/ldap.spicy +++ b/analyzer/protocol/ldap/ldap.spicy @@ -394,7 +394,7 @@ type SearchFilter = unit(depth: uint32) { &parse-from=self.filterBytes; FilterType::FILTER_EXT -> FILTER_EXT: asn1::ASN1Message(True, depth+1) &parse-from=self.filterBytes; - * -> unknownFilter: void; + * -> : void; }; }; From 40fbc831a25b6d96fa1b58af3ca3534ee1b6fdc1 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Tue, 11 May 2021 14:51:06 -0600 Subject: [PATCH 10/20] void fields never store a value and cannot be named --- analyzer/protocol/ldap/ldap.spicy | 1 - 1 file changed, 1 deletion(-) diff --git a/analyzer/protocol/ldap/ldap.spicy b/analyzer/protocol/ldap/ldap.spicy index 982b3f87..87e39fc5 100644 --- a/analyzer/protocol/ldap/ldap.spicy +++ b/analyzer/protocol/ldap/ldap.spicy @@ -394,7 +394,6 @@ type SearchFilter = unit(depth: uint32) { &parse-from=self.filterBytes; FilterType::FILTER_EXT -> FILTER_EXT: asn1::ASN1Message(True, depth+1) &parse-from=self.filterBytes; - * -> : void; }; }; From 7737a0c0baefc209371bd658ea6c348b4f0879f4 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Wed, 12 May 2021 06:49:04 -0600 Subject: [PATCH 11/20] Moving computation for |self.seq.submessages| to temporary local variable to fix CI integration error. Thanks to @bbannier in https://github.com/zeek/spicy-analyzers/pull/56#discussion_r630996954: "This combination of stringification, tuples, and|...| triggers the CI error you are seeing." This could be removed once https://github.com/zeek/spicy/pull/919 is in. --- analyzer/protocol/ldap/asn1.spicy | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/analyzer/protocol/ldap/asn1.spicy b/analyzer/protocol/ldap/asn1.spicy index 8c5f671a..09665ac6 100644 --- a/analyzer/protocol/ldap/asn1.spicy +++ b/analyzer/protocol/ldap/asn1.spicy @@ -267,7 +267,11 @@ public type ASN1Body = unit(head: ASN1Header, recursive: bool, depth: uint32) { } else if (self?.asn1string) { print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "STRING"), self.asn1string; } else if (self?.seq) { - print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "SEQUENCE|SET"), |self.seq.submessages|, head.len.len; + # TODO: this combination of stringification, tuples, and |...| was triggering a CI error. + # Using a local variable for the |...| as a workaround. + # This could be reverted once PR https://github.com/zeek/spicy/pull/919 is in. + local num_submessages = |self.seq.submessages|; + print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "SEQUENCE|SET"), num_submessages, head.len.len; } else { print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe); } From d403227d277b3529f104d25931459655e40f82e7 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Wed, 12 May 2021 11:11:24 -0600 Subject: [PATCH 12/20] changes made after @bbanier's review of PR zeek/spicy-analyzers#56. See the comments in that review for the details. --- CHANGES | 4 - analyzer/protocol/ldap/CMakeLists.txt | 2 + analyzer/protocol/ldap/__load__.zeek | 2 + analyzer/protocol/ldap/asn1.spicy | 253 +++++++--------- analyzer/protocol/ldap/ldap.evt | 8 +- analyzer/protocol/ldap/ldap.spicy | 393 ++++++++++--------------- analyzer/protocol/ldap/ldap_zeek.spicy | 2 + analyzer/protocol/ldap/main.zeek | 2 + tests/protocol/ldap/availability.zeek | 2 + tests/protocol/ldap/basic.zeek | 2 + 10 files changed, 271 insertions(+), 399 deletions(-) diff --git a/CHANGES b/CHANGES index 79a91961..b67e204c 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,3 @@ -0.2.11 | 2021-05-11 10:00:00 -0600 - - * Add LDAP analyzer. (Seth Grover, INL) - 0.2.10-4 | 2021-05-05 11:49:06 +0200 * Add Aruba Networks vendor ID info. (Keith Jones, Corelight) diff --git a/analyzer/protocol/ldap/CMakeLists.txt b/analyzer/protocol/ldap/CMakeLists.txt index 30526401..a0ea1632 100644 --- a/analyzer/protocol/ldap/CMakeLists.txt +++ b/analyzer/protocol/ldap/CMakeLists.txt @@ -1 +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 index a10fe855..6a29e295 100644 --- a/analyzer/protocol/ldap/__load__.zeek +++ b/analyzer/protocol/ldap/__load__.zeek @@ -1 +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 index 09665ac6..0853f668 100644 --- a/analyzer/protocol/ldap/asn1.spicy +++ b/analyzer/protocol/ldap/asn1.spicy @@ -1,3 +1,5 @@ +# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. + module asn1; ############################################################################### @@ -16,31 +18,26 @@ module asn1; import spicy; -# set asn1::DEBUG_PRINT to True to enable debug printing of ASN.1 body values -const DEBUG_PRINT = False; -# DEBUG_SPACES is used with *.depth to print nested ASN.1 structures -const DEBUG_SPACES = " "; - #- 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, + Boolean = 1, + Integer = 2, + BitString = 3, + OctetString = 4, + NullVal = 5, + ObjectIdentifier = 6, ObjectDescriptor = 7, - INSTANCEOF = 8, - REAL = 9, - ENUMERATED = 10, - EMBEDDEDPDV = 11, + InstanceOf = 8, + Real = 9, + Enumerated = 10, + EmbeddedPDV = 11, UTF8String = 12, - RELATIVEOID = 13, - SEQUENCE = 16, - SET = 17, + RelativeOID = 13, + Sequence = 16, + Set = 17, NumericString = 18, PrintableString = 19, TeletextString = 20, @@ -52,17 +49,17 @@ public type ASN1Type = enum { VisibleString = 26, GeneralString = 27, UniversalString = 28, - CHARACTERSTRING = 29, + CharacterString = 29, BMPString = 30 }; #- ASN.1 data classes -------------------------------------------------------- public type ASN1Class = enum { - UNIVERSAL = 0, - APPLICATION = 1, - CONTEXT_SPECIFIC = 2, - PRIVATE = 3 + Universal = 0, + Application = 1, + ContextSpecific = 2, + Private = 3 }; #- ASN.1 tag definition (including length) ------------------------------------ @@ -79,8 +76,9 @@ type LengthType = unit { var tag_len: uint8; switch ( self.data.islong ) { - 0 -> : b"" { - self.len = self.data.num; self.tag_len = 1; + 0 -> : void { + self.len = self.data.num; + self.tag_len = 1; } 1 -> length_parse: bytes &size=self.data.num &convert=$$.to_uint(spicy::ByteOrder::Network) { @@ -91,7 +89,7 @@ type LengthType = unit { }; type ASN1Tag = unit { - var tpe: ASN1Type; + var type_: ASN1Type; var len: uint8 = 1; var class: ASN1Class; var constructed: bool; @@ -100,10 +98,8 @@ type ASN1Tag = unit { num: 0..4; constructed: 5; class: 6..7; - }; - - on %done { - self.tpe = ASN1Type(self.data.num); + } { + self.type_ = ASN1Type(self.data.num); self.class = ASN1Class(self.data.class); self.constructed = cast(self.data.constructed); } @@ -117,6 +113,7 @@ type ASN1BitString = unit(len: uint64, constructed: bool) { 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 }; @@ -140,29 +137,43 @@ type ASN1OctetString = unit(len: uint64, constructed: bool) { type ASN1String = unit(tag: ASN1Tag, len: uint64) { var value: string = ""; - octetstring: ASN1OctetString(len, tag.constructed) { + octetstring: 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) - if ((tag.tpe == ASN1Type::PrintableString) || - (tag.tpe == ASN1Type::GeneralizedTime) || - (tag.tpe == ASN1Type::UTCTime)) { + ASN1Type::PrintableString, + ASN1Type::GeneralizedTime, + ASN1Type::UTCTime -> : void { self.value = self.octetstring.value.decode(hilti::Charset::ASCII); + } - } else if (tag.tpe == ASN1Type::UTF8String) { + 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 -> : void { self.value = self.octetstring.value.decode(hilti::Charset::UTF8); - - } else { - # this would include ASN1Type::BMPString and ASN1Type::UniversalString because - # hilti::Charset::UTF16BE and hilti::Charset::UTF32BE don't exist any more - throw "Unsupported ASN1String type"; } - } + }; }; #- ASN.1 OID ------------------------------------------------------------------ # https://www.obj-sys.com/asn1tutorial/node124.html -type oidnibble = unit { +type ASN1ObjectIdentifierNibble = unit { data : bitfield(8) { num: 0..6; more: 7; @@ -184,7 +195,7 @@ type ASN1ObjectIdentifier = unit(len: uint64) { self.temp = 0; } - sublist: oidnibble[len - 1] foreach { + sublist: ASN1ObjectIdentifierNibble[len - 1] foreach { self.temp = ( self.temp<<7 ) | $$.data.num; if ( $$.data.more != 1 ) { self.oid.push_back(self.temp); @@ -200,144 +211,86 @@ type ASN1ObjectIdentifier = unit(len: uint64) { public type ASN1Header = unit { tag: ASN1Tag; - - var total_length: uint64; - - len: LengthType { - self.total_length = self.len.len + self.tag.len + self.len.tag_len; - } + len: LengthType; }; #- ASN.1 message body --------------------------------------------------------- -public type ASN1Body = unit(head: ASN1Header, recursive: bool, depth: uint32) { - switch ( head.tag.tpe ) { +public type ASN1Body = unit(head: ASN1Header, recursive: bool) { + switch ( head.tag.type_ ) { - ASN1Type::BOOLEAN -> bool_value: uint8 &convert=cast($$) { - if (head.len.len != 1) { - throw "ASN1Type::BOOLEAN detected with length != 1"; - } - } + ASN1Type::Boolean -> bool_value: uint8 &convert=cast($$) &requires=head.len.len==1; - ASN1Type::INTEGER, - ASN1Type::ENUMERATED -> num_value: bytes &size=head.len.len + ASN1Type::Integer, + ASN1Type::Enumerated -> num_value: bytes &size=head.len.len &convert=$$.to_int(spicy::ByteOrder::Big); - ASN1Type::NULLVAL -> : void { - if (head.len.len != 0) { - throw "ASN1Type::NULLVAL detected with non-zero length"; - } - } + ASN1Type::NullVal -> null_value: bytes &size=0 &requires=head.len.len==0; - ASN1Type::BITSTRING -> bitstring: ASN1BitString(head.len.len, head.tag.constructed); + ASN1Type::BitString -> bitstr_value: ASN1BitString(head.len.len, head.tag.constructed); - ASN1Type::OCTETSTRING -> octetstring: ASN1OctetString(head.len.len, head.tag.constructed) - &convert=$$.value.decode(hilti::Charset::ASCII); + ASN1Type::OctetString -> str_value: ASN1OctetString(head.len.len, head.tag.constructed) + &convert=$$.value.decode(hilti::Charset::ASCII); - ASN1Type::OBJECTIDENTIFIER -> objectidentifier: ASN1ObjectIdentifier(head.len.len) - &convert=$$.oidstring; + ASN1Type::ObjectIdentifier -> str_value: ASN1ObjectIdentifier(head.len.len) + &convert=$$.oidstring; - ASN1Type::PrintableString, + ASN1Type::BMPString, + ASN1Type::CharacterString, ASN1Type::GeneralizedTime, + ASN1Type::GeneralString, + ASN1Type::GraphicString, + ASN1Type::IA5String, + ASN1Type::NumericString, + ASN1Type::PrintableString, + ASN1Type::TeletextString, ASN1Type::UTCTime, ASN1Type::UTF8String, - ASN1Type::BMPString, - ASN1Type::UniversalString -> asn1string: ASN1String(head.tag, head.len.len) - &convert=$$.value; - - ASN1Type::SEQUENCE, ASN1Type::SET -> seq: ASN1SubMessages(head.len.len, depth+1) if (recursive); - - * -> : bytes &size=head.len.len { - throw "Unsupported ASN1Body type"; - } + 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; }; - on %done { - if (DEBUG_PRINT == True) { - if (self?.bool_value) { - print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "BOOLEAN"), self.bool_value; - } else if (self?.num_value) { - print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "NUMBER"), self.num_value; - } else if (self?.bitstring) { - print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "BITSTRING"), self.bitstring, head.tag.constructed; - } else if (self?.octetstring) { - print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "OCTETSTRING"), self.octetstring, head.tag.constructed; - } else if (self?.objectidentifier) { - print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "OBJECTIDENTIFIER"), self.objectidentifier; - } else if (self?.asn1string) { - print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "STRING"), self.asn1string; - } else if (self?.seq) { - # TODO: this combination of stringification, tuples, and |...| was triggering a CI error. - # Using a local variable for the |...| as a workaround. - # This could be reverted once PR https://github.com/zeek/spicy/pull/919 is in. - local num_submessages = |self.seq.submessages|; - print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe, "SEQUENCE|SET"), num_submessages, head.len.len; - } else { - print "%.*s%s %s %s" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, head.tag.tpe); - } - } - } }; #- ASN.1 array of ASN.1 sequence/set sub-messages (up to msgLen bytes) -------- -public type ASN1SubMessages = unit(msgLen: uint64, depth: uint32) { - submessages: ASN1Message(True, depth)[] &eod; +public type ASN1SubMessages = unit(msgLen: uint64) { + submessages: ASN1Message(True)[] &eod; } &size=msgLen; #- ASN.1 message with header and body ----------------------------------------- -# UNIVERSAL or APPLICATION/CONTEXT_SPECIFIC/PRIVATE -# - if UNIVERSAL, body:ASN1Body is parsed +# Universal or Application/ContextSpecific/Private +# - if Universal, body:ASN1Body is parsed # - else, application_data:bytes stores data array -public type ASN1Message = unit(recursive: bool, depth: uint32) { - var depth: uint32 = depth; - - head: ASN1Header; - body: ASN1Body(self.head, recursive, depth) if ( self.head.tag.class == ASN1Class::UNIVERSAL ); - +public type ASN1Message = unit(recursive: bool) { var application_id: int32; - application_data: bytes &size=self.head.len.len if ( self.head.tag.class != ASN1Class::UNIVERSAL ) { - self.application_id = self.head.tag.data.num; - } - on %done { - if (DEBUG_PRINT == True) { - if ( self.head.tag.class != ASN1Class::UNIVERSAL ) { - print "%.*s%s %d %s %d" % ((depth*2)+1, DEBUG_SPACES, self.head.tag.class, self.application_id, self.head.tag.constructed, self.head.len.len), self.head; - } - } - } -}; - -#- ASN.1 message (headless) --------------------------------------------------- -# same rules as ASN1Message for body/application_data - -public type ASN1MessageHeadless = unit(head: ASN1Header, recursive: bool, depth: uint32) { - var depth: uint32 = depth; - - body: ASN1Body(head, recursive, depth) if ( head.tag.class == ASN1Class::UNIVERSAL ); + head: ASN1Header; + switch ( self.head.tag.class ) { - var application_id: int32; - application_data: bytes &size=head.len.len if ( head.tag.class != ASN1Class::UNIVERSAL ) { - self.application_id = head.tag.data.num; - } + ASN1Class::Universal -> body: ASN1Body(self.head, recursive); - on %done { - if (DEBUG_PRINT == True) { - if ( head.tag.class != ASN1Class::UNIVERSAL ) { - print "%.*s%s %d %s %d" % ((depth*2)+1, DEBUG_SPACES, head.tag.class, self.application_id, head.tag.constructed, head.len.len), head; - } + ASN1Class::Application, + ASN1Class::ContextSpecific, + ASN1Class::Private -> application_data: bytes &size=self.head.len.len { + self.application_id = self.head.tag.data.num; } - } -}; -#- ASN.1 message helper units for recursive (set/sequence) vs. not ------------ - -public type ASN1MessageBaseRecursive = unit { - msg : ASN1Message(True, 0); -}; + }; -public type ASN1MessageBase = unit { - msg : ASN1Message(False, 0); }; diff --git a/analyzer/protocol/ldap/ldap.evt b/analyzer/protocol/ldap/ldap.evt index 8c6e1f95..2a97f22d 100644 --- a/analyzer/protocol/ldap/ldap.evt +++ b/analyzer/protocol/ldap/ldap.evt @@ -1,12 +1,10 @@ +# 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: UDP hasn't been tested/implemented yet -# TODO: test this, will it be Message or Messages? -# protocol analyzer spicy::ldap_udp over UDP: -# parse with ldap::Messages, -# ports {389/udp, 3268/udp}; +# TODO: LDAP can also use UDP transport import ldap; import ldap_zeek; diff --git a/analyzer/protocol/ldap/ldap.spicy b/analyzer/protocol/ldap/ldap.spicy index 87e39fc5..d4aa48e5 100644 --- a/analyzer/protocol/ldap/ldap.spicy +++ b/analyzer/protocol/ldap/ldap.spicy @@ -1,3 +1,5 @@ +# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. + module ldap; import asn1; @@ -29,6 +31,7 @@ public type ProtocolOpcode = enum { EXTENDED_REQUEST = 23, EXTENDED_RESPONSE = 24, INTERMEDIATE_RESPONSE = 25, + # TODO: remove once zeek/spicy-plugin#35 is fixed NOT_SET = 255 }; @@ -109,17 +112,18 @@ public type ResultCode = enum { 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(depth: uint32) { - code: asn1::ASN1Message(True, depth) &convert=cast(cast($$.body.num_value)) - &default=ResultCode::NOT_SET; - matchedDN: asn1::ASN1Message(True, depth) &convert=$$.body.octetstring - &default=""; - diagnosticMessage: asn1::ASN1Message(True, depth) &convert=$$.body.octetstring - &default=""; +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 @@ -139,23 +143,18 @@ public type Message = unit { var obj: string = ""; var arg: string = ""; - seq: asn1::ASN1Message(True, 0) { - if ((self.seq.head.tag.tpe == asn1::ASN1Type::SEQUENCE) && - (self.seq.body?.seq) && - (|self.seq.body.seq.submessages| >= 2)) { - if (self.seq.body.seq.submessages[0].body?.num_value) { - self.messageID = self.seq.body.seq.submessages[0].body.num_value; + : 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 (self.seq.body.seq.submessages[1]?.application_id) { - self.opcode = cast(cast(self.seq.body.seq.submessages[1].application_id)); - self.applicationBytes = self.seq.body.seq.submessages[1].application_data; + 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; } } - if ((! self?.messageID) || - (! self?.opcode) || - (self.opcode == ProtocolOpcode::NOT_SET)) { - throw "LDAP message is not ASN.1 sequence as described in RFC 4511"; - } } switch ( self.opcode ) { @@ -167,22 +166,26 @@ public type Message = unit { 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_REQUEST -> ADD_REQUEST: AddRequest(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_REQUEST -> MOD_DN_REQUEST: ModDNRequest(self) &parse-from=self.applicationBytes; ProtocolOpcode::MOD_DN_RESPONSE -> MOD_DN_RESPONSE: ModDNResponse(self) &parse-from=self.applicationBytes; - ProtocolOpcode::COMPARE_REQUEST -> COMPARE_REQUEST: CompareRequest(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; - ProtocolOpcode::SEARCH_RESULT_REFERENCE -> SEARCH_RESULT_REFERENCE: SearchResultReference(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; + + # 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 @@ -191,59 +194,49 @@ public type Message = unit { 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(depth: uint32) { - mechanism: asn1::ASN1Message(True, depth) &convert=$$.body.octetstring; +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) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - version: asn1::ASN1Message(True, self.depth) &convert=$$.body.num_value; - name: asn1::ASN1Message(True, self.depth) &convert=$$.body.octetstring; + 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 simpleCreds: string = ""; - authentication: asn1::ASN1Message(True, self.depth) { + authentication: asn1::ASN1Message(True) { if (self.authentication?.application_id) { self.authType = cast(cast(self.authentication.application_id)); } - if ((! self?.authType) || (self.authType == BindAuthType::NOT_SET)) { - throw "BindRequest is not ASN.1 sequence as described in RFC 4511"; - } if ((self.authType == BindAuthType::BIND_AUTH_SIMPLE) && (|self.authentication.application_data| > 0)) { self.simpleCreds = self.authentication.application_data.decode(); + if (|self.simpleCreds| > 0) { + message.arg = self.simpleCreds; + } } } - saslCreds: SaslCredentials(self.depth+1) &parse-from=self.authentication.application_data if (self.authType == BindAuthType::BIND_AUTH_SASL); - - on %done { - message.obj = self.name; - if (|self.simpleCreds| > 0) { - message.arg = self.simpleCreds; - } else if (self.authType == BindAuthType::BIND_AUTH_SASL) { - message.arg = self.saslCreds.mechanism; - } - print "BindRequest", message.messageID, self.version, self.name, - self.authType, message.obj, message.arg; + saslCreds: SaslCredentials() &parse-from=self.authentication.application_data if (self.authType == BindAuthType::BIND_AUTH_SASL) { + message.arg = self.saslCreds.mechanism; } -}; - -type BindResponse = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - result: Result(self.depth+1); +} &requires=((self?.authType) && (self.authType != BindAuthType::NOT_SET)); - # TODO: if we want to parse SASL credentials returned +type BindResponse = unit(inout message: Message) { - on %done { + result: Result { message.result = self.result; - print "BindResponse", message.messageID, self; } + + # TODO: if we want to parse SASL credentials returned }; #----------------------------------------------------------------------------- @@ -251,11 +244,7 @@ type BindResponse = unit(inout message: Message) { # https://tools.ietf.org/html/rfc4511#section-4.3 type UnbindRequest = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - on %done { - print "UnbindRequest", message.messageID, self; - } + # this page intentionally left blank }; #----------------------------------------------------------------------------- @@ -266,6 +255,7 @@ 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 }; @@ -274,6 +264,7 @@ public type SearchDerefAlias = enum { DEREF_IN_SEARCHING = 1, DEREF_FINDING_BASE = 2, DEREF_ALWAYS = 3, + # TODO: remove once zeek/spicy-plugin#35 is fixed NOT_SET = 255 }; @@ -289,60 +280,60 @@ type FilterType = enum { FILTER_APPROX = 8, FILTER_EXT = 9, FILTER_INVALID = 254, + # TODO: remove once zeek/spicy-plugin#35 is fixed NOT_SET = 255 }; -type AttributeSelection = unit(depth: uint32) { +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. - seq: asn1::ASN1Message(True, depth) { - if ((self.seq.head.tag.tpe == asn1::ASN1Type::SEQUENCE) && - (self.seq.body?.seq) && - (|self.seq.body.seq.submessages| > 0)) { - for (i in self.seq.body.seq.submessages) { - if (i.body?.octetstring) { - self.attributes.push_back(i.body.octetstring); + : 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(depth: uint32) { +type AttributeValueAssertion = unit() { var desc: string = ""; var val: string = ""; - seq: asn1::ASN1Message(True, depth) { - if ((self.seq.head.tag.tpe == asn1::ASN1Type::SEQUENCE) && - (self.seq.body?.seq) && - (|self.seq.body.seq.submessages| >= 2)) { - if (self.seq.body.seq.submessages[0].body?.octetstring) { - self.desc = self.seq.body.seq.submessages[0].body.octetstring; + : 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 (self.seq.body.seq.submessages[1].body?.octetstring) { - self.val = self.seq.body.seq.submessages[1].body.octetstring; + if ($$.body.seq.submessages[1].body?.str_value) { + self.val = $$.body.seq.submessages[1].body.str_value; } } } }; -type SubstringFilter = unit(depth: uint32) { +type SubstringFilter = unit() { var ftype: string = ""; var substrings: asn1::ASN1Message; - seq: asn1::ASN1Message(True, depth) { - if ((self.seq.head.tag.tpe == asn1::ASN1Type::SEQUENCE) && - (self.seq.body?.seq) && - (|self.seq.body.seq.submessages| >= 2)) { - if (self.seq.body.seq.submessages[0].body?.octetstring) { - self.ftype = self.seq.body.seq.submessages[0].body.octetstring; + : 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 (self.seq.body.seq.submessages[1].head.tag.tpe == asn1::ASN1Type::SEQUENCE) { - self.substrings = self.seq.body.seq.submessages[1]; + 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 @@ -353,14 +344,16 @@ type SubstringFilter = unit(depth: uint32) { }; -type SearchFilter = unit(depth: uint32) { +type SearchFilter = unit() { var filterType: FilterType = FilterType::NOT_SET; var filterBytes: bytes = b""; + var filterLen: uint64 = 0; - filter : asn1::ASN1Message(True, depth) { - if (self.filter?.application_id) { - self.filterType = cast(cast(self.filter.application_id)); - self.filterBytes = self.filter.application_data; + : 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; } @@ -373,111 +366,87 @@ type SearchFilter = unit(depth: uint32) { # I've just left some of them as asn1::ASN1Message for now. switch ( self.filterType ) { - FilterType::FILTER_AND -> FILTER_AND: asn1::ASN1Message(True, depth+1) + FilterType::FILTER_AND -> FILTER_AND: asn1::ASN1Message(True) &parse-from=self.filterBytes; - FilterType::FILTER_OR -> FILTER_OR: asn1::ASN1Message(True, depth+1) + FilterType::FILTER_OR -> FILTER_OR: asn1::ASN1Message(True) &parse-from=self.filterBytes; - FilterType::FILTER_NOT -> FILTER_NOT: SearchFilter(depth+1) + FilterType::FILTER_NOT -> FILTER_NOT: SearchFilter() &parse-from=self.filterBytes; - FilterType::FILTER_EQ -> FILTER_EQ: AttributeValueAssertion(depth+1) + FilterType::FILTER_EQ -> FILTER_EQ: AttributeValueAssertion() &parse-from=self.filterBytes; - FilterType::FILTER_SUBSTR -> FILTER_SUBSTR: SubstringFilter(depth+1) + FilterType::FILTER_SUBSTR -> FILTER_SUBSTR: SubstringFilter() &parse-from=self.filterBytes; - FilterType::FILTER_GE -> FILTER_GE: AttributeValueAssertion(depth+1) + FilterType::FILTER_GE -> FILTER_GE: AttributeValueAssertion() &parse-from=self.filterBytes; - FilterType::FILTER_LE -> FILTER_LE: AttributeValueAssertion(depth+1) + FilterType::FILTER_LE -> FILTER_LE: AttributeValueAssertion() &parse-from=self.filterBytes; - FilterType::FILTER_PRESENT -> FILTER_PRESENT: asn1::ASN1OctetString(self.filter.head.len.len, False) + 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(depth+1) + FilterType::FILTER_APPROX -> FILTER_APPROX: AttributeValueAssertion() &parse-from=self.filterBytes; - FilterType::FILTER_EXT -> FILTER_EXT: asn1::ASN1Message(True, depth+1) + FilterType::FILTER_EXT -> FILTER_EXT: asn1::ASN1Message(True) &parse-from=self.filterBytes; }; }; type SearchRequest = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - baseObject: asn1::ASN1Message(True, self.depth) &convert=$$.body.octetstring; - scope: asn1::ASN1Message(True, self.depth) &convert=cast(cast($$.body.num_value)) - &default=SearchScope::NOT_SET; - deref: asn1::ASN1Message(True, self.depth) &convert=cast(cast($$.body.num_value)) - &default=SearchDerefAlias::NOT_SET; - sizeLimit: asn1::ASN1Message(True, self.depth) &convert=$$.body.num_value &default=0; - timeLimit: asn1::ASN1Message(True, self.depth) &convert=$$.body.num_value &default=0; - typesOnly: asn1::ASN1Message(True, self.depth) &convert=$$.body.bool_value &default=False; - filter: SearchFilter(self.depth); - attributes: AttributeSelection(self.depth); - - on %done { + + 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; - print "SearchRequest", message.messageID, self; } + 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) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - objectName: asn1::ASN1Message(True, self.depth) &convert=$$.body.octetstring; - # TODO: if we want to descend down into PartialAttributeList - attributes: asn1::ASN1Message(True, self.depth); - on %done { + objectName: asn1::ASN1Message(True) &convert=$$.body.str_value { message.obj = self.objectName; - print "SearchResultEntry", message.messageID, self; } + # TODO: if we want to descend down into PartialAttributeList + attributes: asn1::ASN1Message(True); + }; type SearchResultDone = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - result: Result(self.depth+1); - on %done { + result: Result { message.result = self.result; - print "SearchResultDone", message.messageID, self; } }; -type SearchResultReference = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - # TODO: SearchResultReference - # The SearchResultReference is of the same data type as the Referral. - - on %done { - print "SearchResultReference", message.messageID, self; - } -}; +# 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) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - objectName: asn1::ASN1Message(True, self.depth) &convert=$$.body.octetstring; - - # TODO: parse changes - on %done { + objectName: asn1::ASN1Message(True) &convert=$$.body.str_value { message.obj = self.objectName; - print "ModifyRequest", message.messageID, self; } + + # TODO: parse changes }; type ModifyResponse = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - result: Result(self.depth+1); - - on %done { + result: Result { message.result = self.result; - print "ModifyResponse", message.messageID, self; } }; @@ -485,24 +454,16 @@ type ModifyResponse = unit(inout message: Message) { # Add Operation # https://tools.ietf.org/html/rfc4511#section-4.7 -type AddRequest = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - # TODO: add request sequence - - on %done { - print "AddRequest", message.messageID, self; - } -}; +# TODO: implement AddRequest +# type AddRequest = unit(inout message: Message) { +# +# +# }; type AddResponse = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - result: Result(self.depth+1); - on %done { + result: Result { message.result = self.result; - print "AddResponse", message.messageID, self; } }; @@ -511,24 +472,16 @@ type AddResponse = unit(inout message: Message) { # https://tools.ietf.org/html/rfc4511#section-4.8 type DelRequest = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - objectName: asn1::ASN1Message(True, self.depth) &convert=$$.body.octetstring; - - on %done { + objectName: asn1::ASN1Message(True) &convert=$$.body.str_value { message.obj = self.objectName; - print "DelRequest", message.messageID, self; } }; type DelResponse = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - result: Result(self.depth+1); - on %done { + result: Result { message.result = self.result; - print "DelResponse", message.messageID, self; } }; @@ -536,24 +489,15 @@ type DelResponse = unit(inout message: Message) { # Modify DN Operation # https://tools.ietf.org/html/rfc4511#section-4.8 -type ModDNRequest = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - # TODO: parse mod DN request sequence - - on %done { - print "ModDNRequest", message.messageID, self; - } -}; +# TODO: implement ModDNRequest +# type ModDNRequest = unit(inout message: Message) { +# +# }; type ModDNResponse = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - result: Result(self.depth+1); - - on %done { + result: Result { message.result = self.result; - print "ModDNResponse", message.messageID, self; } }; @@ -561,24 +505,15 @@ type ModDNResponse = unit(inout message: Message) { # Compare Operation # https://tools.ietf.org/html/rfc4511#section-4.10 -type CompareRequest = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - # TODO: parse compare request entry/ava sequence - - on %done { - print "CompareRequest", message.messageID, self; - } -}; +# TODO: implement CompareRequest +# type CompareRequest = unit(inout message: Message) { +# +# }; type CompareResponse = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - result: Result(self.depth+1); - on %done { + result: Result { message.result = self.result; - print "CompareResponse", message.messageID, self; } }; @@ -587,14 +522,8 @@ type CompareResponse = unit(inout message: Message) { # https://tools.ietf.org/html/rfc4511#section-4.11 type AbandonRequest = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - # messageID: asn1::ASN1Message(True, self.depth) &convert=$$.body.num_value; - - on %done { - # TODO: can't do string -> bytes - # but see https://github.com/zeek/spicy/issues/911 for next release - # message.obj = "%d" % (self.messageID); - print "AbandonRequest", message.messageID, self; + messageID: asn1::ASN1Message(True) &convert=$$.body.num_value { + message.obj = "%d" % (self.messageID); } }; @@ -602,37 +531,21 @@ type AbandonRequest = unit(inout message: Message) { # Extended Operation # https://tools.ietf.org/html/rfc4511#section-4.12 -type ExtendedRequest = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - # TODO: parse ExtendedRequest sequence - - on %done { - print "ExtendedRequest", message.messageID, self; - } -}; - -type ExtendedResponse = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - # TODO: parse ExtendedRequest sequence +# TODO: implement ExtendedRequest +# type ExtendedRequest = unit(inout message: Message) { +# +# }; - on %done { - # message.result = self.result; - print "ExtendedResponse", message.messageID, self; - } -}; +# TODO: implement ExtendedResponse +# type ExtendedResponse = unit(inout message: Message) { +# +# }; #----------------------------------------------------------------------------- # IntermediateResponse Message # https://tools.ietf.org/html/rfc4511#section-4.13 -type IntermediateResponse = unit(inout message: Message) { - var depth: uint32 = message.seq.body.seq.submessages[1].depth+1; - - # TODO: parse IntermediateResponse sequence - - on %done { - print "IntermediateResponse", message.messageID, self; - } -}; +# 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 index 53cff452..c3411503 100644 --- a/analyzer/protocol/ldap/ldap_zeek.spicy +++ b/analyzer/protocol/ldap/ldap_zeek.spicy @@ -1,3 +1,5 @@ +# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. + module ldap_zeek; import zeek; diff --git a/analyzer/protocol/ldap/main.zeek b/analyzer/protocol/ldap/main.zeek index e137dff1..cac53d0b 100644 --- a/analyzer/protocol/ldap/main.zeek +++ b/analyzer/protocol/ldap/main.zeek @@ -1,3 +1,5 @@ +# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. + module ldap; export { diff --git a/tests/protocol/ldap/availability.zeek b/tests/protocol/ldap/availability.zeek index a33fa753..68cb285b 100644 --- a/tests/protocol/ldap/availability.zeek +++ b/tests/protocol/ldap/availability.zeek @@ -1,3 +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 index d9280591..8e03fb0c 100644 --- a/tests/protocol/ldap/basic.zeek +++ b/tests/protocol/ldap/basic.zeek @@ -1,3 +1,5 @@ +# 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 From 6edc9ce7469279bf91a6be49be4ce72f6434c32f Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Wed, 12 May 2021 11:47:43 -0600 Subject: [PATCH 13/20] changes made after @bbanier's review of PR zeek/spicy-analyzers#56. See the comments in that review for the details. --- analyzer/protocol/ldap/asn1.spicy | 2 -- 1 file changed, 2 deletions(-) diff --git a/analyzer/protocol/ldap/asn1.spicy b/analyzer/protocol/ldap/asn1.spicy index 0853f668..1d466009 100644 --- a/analyzer/protocol/ldap/asn1.spicy +++ b/analyzer/protocol/ldap/asn1.spicy @@ -65,7 +65,6 @@ public type ASN1Class = enum { #- ASN.1 tag definition (including length) ------------------------------------ type LengthType = unit { - data : bitfield(8) { num: 0..6; islong: 7; @@ -90,7 +89,6 @@ type LengthType = unit { type ASN1Tag = unit { var type_: ASN1Type; - var len: uint8 = 1; var class: ASN1Class; var constructed: bool; From 9d470d123538b8e0ee653be68a57e118950f8dd6 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Wed, 12 May 2021 13:06:41 -0600 Subject: [PATCH 14/20] Added proto (TCP for now, may include UDP in the future ) to the ldap logs --- analyzer/protocol/ldap/main.zeek | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/analyzer/protocol/ldap/main.zeek b/analyzer/protocol/ldap/main.zeek index cac53d0b..13320e76 100644 --- a/analyzer/protocol/ldap/main.zeek +++ b/analyzer/protocol/ldap/main.zeek @@ -20,6 +20,9 @@ export { # 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; @@ -56,6 +59,9 @@ export { # 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; @@ -230,6 +236,7 @@ global OPCODES_SEARCH: set[ldap::ProtocolOpcode] = { ldap::ProtocolOpcode_SEARCH ############################################################################# redef record connection += { + ldap_proto: string &optional; ldap_messages: table[int] of Message &optional; ldap_searches: table[int] of Search &optional; }; @@ -268,7 +275,9 @@ function set_session(c: connection, message_id: int, opcode: ldap::ProtocolOpcod ############################################################################# event protocol_confirmation(c: connection, atype: Analyzer::Tag, aid: count) &priority=5 { - # todo: do we really need to do anything here? + if ( atype == Analyzer::ANALYZER_SPICY_LDAP_TCP ) { + c$ldap_proto = "tcp"; + } } @@ -297,6 +306,9 @@ event ldap::message(c: connection, 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]; @@ -332,11 +344,16 @@ event ldap::message(c: connection, } 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]; } @@ -418,10 +435,15 @@ event connection_state_remove(c: connection) { 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); } } @@ -431,6 +453,10 @@ event connection_state_remove(c: connection) { 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); } } From 0962e6ae09817b77034e8fcd73dad652d8cb46d3 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Wed, 12 May 2021 13:20:05 -0600 Subject: [PATCH 15/20] update .log files from baseline test to reflect new logs fields --- tests/Baseline/protocol.ldap.basic/ldap.log | 8 ++++---- tests/Baseline/protocol.ldap.basic/ldap_search.log | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Baseline/protocol.ldap.basic/ldap.log b/tests/Baseline/protocol.ldap.basic/ldap.log index a73b8923..021bf93c 100644 --- a/tests/Baseline/protocol.ldap.basic/ldap.log +++ b/tests/Baseline/protocol.ldap.basic/ldap.log @@ -6,8 +6,8 @@ #unset_field - #path ldap #open XXXX-XX-XX-XX-XX-XX -#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p message_id version opcode result diagnostic_message object argument -#types time string addr port addr port int int set[string] set[string] vector[string] vector[string] vector[string] +#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 1 3 bind simple success - xxxxxxxxxxx@xx.xxx.xxxxx.net passwor8d1 -XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 3 3 bind simple success - CN=xxxxxxxx\x2cOU=Users\x2cOU=Accounts\x2cDC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net /dev/rdsk/c0t0d0s0 +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 index 31738e32..8e44022e 100644 --- a/tests/Baseline/protocol.ldap.basic/ldap_search.log +++ b/tests/Baseline/protocol.ldap.basic/ldap_search.log @@ -6,7 +6,7 @@ #unset_field - #path ldap_search #open XXXX-XX-XX-XX-XX-XX -#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p message_id scope deref base_object result_count result diagnostic_message -#types time string addr port addr port int set[string] set[string] vector[string] count set[string] vector[string] +#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 2 tree always DC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net 1 success - +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 - From e5e5ffda8de9b5726a08a0688967d2964b791423 Mon Sep 17 00:00:00 2001 From: Keith Jones Date: Thu, 13 May 2021 09:03:38 -0400 Subject: [PATCH 16/20] Remove analyzer_id from scripts for ipsec. --- analyzer/protocol/ipsec/main.zeek | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/analyzer/protocol/ipsec/main.zeek b/analyzer/protocol/ipsec/main.zeek index 41e2f6b9..3f53a23c 100644 --- a/analyzer/protocol/ipsec/main.zeek +++ b/analyzer/protocol/ipsec/main.zeek @@ -58,10 +58,6 @@ export { ## Cipher hash of this IPSec transaction info: ## vendor_ids, notify_messages, transforms, ke_dh_groups, and proposals hash: string &log &optional; - # The analyzer ID used for the analyzer instance attached - # to each connection. It is not used for logging since it's a - # meaningless arbitrary number. - analyzer_id: count &optional; }; # Event that can be handled to access the IPSec record as it is sent on @@ -669,17 +665,6 @@ function set_session(c: connection) } } -event protocol_confirmation(c: connection, atype: Analyzer::Tag, aid: count) &priority=5 - { - if ( atype == Analyzer::ANALYZER_SPICY_IPSEC_IKE_UDP || - atype == Analyzer::ANALYZER_SPICY_IPSEC_TCP || - atype == Analyzer::ANALYZER_SPICY_IPSEC_UDP ) - { - set_session(c); - c$ipsec$analyzer_id = aid; - } - } - event ipsec::ike_message(c: connection, is_orig: bool, msg: ipsec::IKEMsg) { set_session(c); From bf8b3d755622fe3d931e450c90c83f2c7a589ab0 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Tue, 11 May 2021 08:52:32 -0600 Subject: [PATCH 17/20] update changes --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index b67e204c..50be6a09 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ +0.2.11 | 2021-05-17 09:39:00 +0200 + + * Remove `analyzer_id` from scripts for IPSec. (Keith Jones) + 0.2.10-4 | 2021-05-05 11:49:06 +0200 * Add Aruba Networks vendor ID info. (Keith Jones, Corelight) From 51ec8fdd2a8f145395cd07c080cc78fd064185f1 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Tue, 18 May 2021 08:47:15 -0600 Subject: [PATCH 18/20] try to expose less useless stuff in each unit, for zeek/spicy-analyzers#56 As per the suggestion here https://github.com/zeek/spicy-analyzers/pull/56#pullrequestreview-662110064 --- analyzer/protocol/ldap/asn1.spicy | 104 ++++++++++++++---------------- analyzer/protocol/ldap/ldap.spicy | 60 +++++++---------- 2 files changed, 71 insertions(+), 93 deletions(-) diff --git a/analyzer/protocol/ldap/asn1.spicy b/analyzer/protocol/ldap/asn1.spicy index 1d466009..cb06f86f 100644 --- a/analyzer/protocol/ldap/asn1.spicy +++ b/analyzer/protocol/ldap/asn1.spicy @@ -65,23 +65,23 @@ public type ASN1Class = enum { #- ASN.1 tag definition (including length) ------------------------------------ type LengthType = unit { + var len: uint64; + var tag_len: uint8; + data : bitfield(8) { num: 0..6; islong: 7; }; - var len: uint64; - - var tag_len: uint8; switch ( self.data.islong ) { 0 -> : void { self.len = self.data.num; self.tag_len = 1; } - 1 -> length_parse: bytes &size=self.data.num - &convert=$$.to_uint(spicy::ByteOrder::Network) { - self.len = self.length_parse; + 1 -> : bytes &size=self.data.num + &convert=$$.to_uint(spicy::ByteOrder::Network) { + self.len = $$; self.tag_len = self.data.num + 1; } }; @@ -92,14 +92,14 @@ type ASN1Tag = unit { var class: ASN1Class; var constructed: bool; - data : bitfield(8) { + : bitfield(8) { num: 0..4; constructed: 5; class: 6..7; } { - self.type_ = ASN1Type(self.data.num); - self.class = ASN1Class(self.data.class); - self.constructed = cast(self.data.constructed); + self.type_ = ASN1Type($$.num); + self.class = ASN1Class($$.class); + self.constructed = cast($$.constructed); } }; @@ -107,26 +107,22 @@ type ASN1Tag = unit { # https://www.obj-sys.com/asn1tutorial/node10.html type ASN1BitString = unit(len: uint64, constructed: bool) { - unused_bits: uint8; + : 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) { - var len: uint64; value: bytes &size = len; # TODO - constructed form - - on %done { - self.len = len; - } }; #- ASN.1 various string types ------------------------------------------------- @@ -135,37 +131,36 @@ type ASN1OctetString = unit(len: uint64, constructed: bool) { type ASN1String = unit(tag: ASN1Tag, len: uint64) { var value: string = ""; - octetstring: 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) - - ASN1Type::PrintableString, - ASN1Type::GeneralizedTime, - ASN1Type::UTCTime -> : void { - self.value = self.octetstring.value.decode(hilti::Charset::ASCII); - } - - 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 -> : void { - self.value = self.octetstring.value.decode(hilti::Charset::UTF8); + : 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 ------------------------------------------------------------------ @@ -176,32 +171,31 @@ type ASN1ObjectIdentifierNibble = unit { num: 0..6; more: 7; }; -}; +} &convert=self.data; type ASN1ObjectIdentifier = unit(len: uint64) { var oid: vector; var temp: uint64; var oidstring: string; - first: uint8 if ( len >= 1 ) { - self.temp = self.first / 40; + : uint8 if ( len >= 1 ) { + self.temp = $$ / 40; self.oid.push_back( self.temp ); self.oidstring = "%d" % (self.temp); - self.temp = self.first % 40; + 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 ) | $$.data.num; - if ( $$.data.more != 1 ) { + 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; } } - }; @@ -261,7 +255,6 @@ public type ASN1Body = unit(head: ASN1Header, recursive: bool) { # 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) -------- @@ -286,9 +279,8 @@ public type ASN1Message = unit(recursive: bool) { ASN1Class::Application, ASN1Class::ContextSpecific, ASN1Class::Private -> application_data: bytes &size=self.head.len.len { - self.application_id = self.head.tag.data.num; + self.application_id = cast(self.head.tag.type_); } }; - }; diff --git a/analyzer/protocol/ldap/ldap.spicy b/analyzer/protocol/ldap/ldap.spicy index d4aa48e5..ac6f88c5 100644 --- a/analyzer/protocol/ldap/ldap.spicy +++ b/analyzer/protocol/ldap/ldap.spicy @@ -184,7 +184,6 @@ public type Message = unit { }; # 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)); #----------------------------------------------------------------------------- @@ -204,36 +203,36 @@ type SaslCredentials = unit() { }; 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 simpleCreds: string = ""; + var saslData: bytes; - authentication: asn1::ASN1Message(True) { - if (self.authentication?.application_id) { - self.authType = cast(cast(self.authentication.application_id)); + : asn1::ASN1Message(True) { + if ($$?.application_id) { + self.authType = cast(cast($$.application_id)); } if ((self.authType == BindAuthType::BIND_AUTH_SIMPLE) && - (|self.authentication.application_data| > 0)) { - self.simpleCreds = self.authentication.application_data.decode(); + (|$$.application_data| > 0)) { + self.simpleCreds = $$.application_data.decode(); if (|self.simpleCreds| > 0) { message.arg = self.simpleCreds; } + } else { + self.saslData == $$.application_data; } } - saslCreds: SaslCredentials() &parse-from=self.authentication.application_data if (self.authType == BindAuthType::BIND_AUTH_SASL) { + saslCreds: SaslCredentials() &parse-from=self.saslData if (self?.saslData) { message.arg = self.saslCreds.mechanism; } - } &requires=((self?.authType) && (self.authType != BindAuthType::NOT_SET)); type BindResponse = unit(inout message: Message) { - - result: Result { - message.result = self.result; + : Result { + message.result = $$; } # TODO: if we want to parse SASL credentials returned @@ -318,7 +317,6 @@ type AttributeValueAssertion = unit() { } } } - }; type SubstringFilter = unit() { @@ -341,7 +339,6 @@ type SubstringFilter = unit() { # #} } - }; type SearchFilter = unit() { @@ -391,7 +388,6 @@ type SearchFilter = unit() { }; type SearchRequest = unit(inout message: Message) { - baseObject: asn1::ASN1Message(True) &convert=$$.body.str_value { message.obj = self.baseObject; } @@ -409,19 +405,16 @@ type SearchRequest = unit(inout message: Message) { }; 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: Result { - message.result = self.result; + : Result { + message.result = $$; } }; @@ -435,7 +428,6 @@ type SearchResultDone = unit(inout message: Message) { # 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; } @@ -444,9 +436,8 @@ type ModifyRequest = unit(inout message: Message) { }; type ModifyResponse = unit(inout message: Message) { - - result: Result { - message.result = self.result; + : Result { + message.result = $$; } }; @@ -461,9 +452,8 @@ type ModifyResponse = unit(inout message: Message) { # }; type AddResponse = unit(inout message: Message) { - - result: Result { - message.result = self.result; + : Result { + message.result = $$; } }; @@ -472,16 +462,14 @@ type AddResponse = unit(inout message: Message) { # 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: Result { - message.result = self.result; + : Result { + message.result = $$; } }; @@ -495,9 +483,8 @@ type DelResponse = unit(inout message: Message) { # }; type ModDNResponse = unit(inout message: Message) { - - result: Result { - message.result = self.result; + : Result { + message.result = $$; } }; @@ -511,9 +498,8 @@ type ModDNResponse = unit(inout message: Message) { # }; type CompareResponse = unit(inout message: Message) { - - result: Result { - message.result = self.result; + : Result { + message.result = $$; } }; From 66964283eea8204511701a053f7b997f9148a3de Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Tue, 18 May 2021 12:14:21 -0600 Subject: [PATCH 19/20] something in 51ec8fd broke something, this will (should) fix it --- analyzer/protocol/ldap/ldap.spicy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzer/protocol/ldap/ldap.spicy b/analyzer/protocol/ldap/ldap.spicy index ac6f88c5..f256121a 100644 --- a/analyzer/protocol/ldap/ldap.spicy +++ b/analyzer/protocol/ldap/ldap.spicy @@ -222,7 +222,7 @@ type BindRequest = unit(inout message: Message) { message.arg = self.simpleCreds; } } else { - self.saslData == $$.application_data; + self.saslData = $$.application_data; } } saslCreds: SaslCredentials() &parse-from=self.saslData if (self?.saslData) { From 3c5ccb4207f5b9e412709b01226d0e1b5ede6e70 Mon Sep 17 00:00:00 2001 From: SG <13872653+mmguero@users.noreply.github.com> Date: Tue, 18 May 2021 12:34:36 -0600 Subject: [PATCH 20/20] something in 51ec8fd broke something, this will (should) fix it Squashed commit of the following: commit 4cd5e7996a22fefb6998c3efb3929ea15f7fd4c0 Author: SG <13872653+mmguero@users.noreply.github.com> Date: Tue May 18 12:32:26 2021 -0600 something in 51ec8fd broke something, this branch is debugging it commit bbf65a741a48f8a08887a8d644a1280bd2f26ccb Author: SG <13872653+mmguero@users.noreply.github.com> Date: Tue May 18 12:29:39 2021 -0600 something in 51ec8fd broke something, this branch is debugging it commit 3636f308d370dfbb77e1870e591e7af46dd835c4 Author: SG <13872653+mmguero@users.noreply.github.com> Date: Tue May 18 12:27:18 2021 -0600 Formatting commit 7957242e0c9b2ecb1ea0d79c0ddc6d22d9cd4117 Author: SG <13872653+mmguero@users.noreply.github.com> Date: Tue May 18 12:24:38 2021 -0600 something in 51ec8fd broke something, this branch is debugging it commit 8bed45b756b8bf915dc3997ad333c4c2d66a1819 Merge: d86336c 6696428 Author: SG <13872653+mmguero@users.noreply.github.com> Date: Tue May 18 12:21:58 2021 -0600 Merge remote-tracking branch 'mmguero-dev/main' into topic/ldapdebug commit d86336c8674ec086143235bd674ed4f19294336b Author: SG <13872653+mmguero@users.noreply.github.com> Date: Tue May 18 12:06:38 2021 -0600 something in 51ec8fd broke something, this branch is debugging it commit 292186c0b017caef5bb63d7456c4f0b5bb3ce542 Author: SG <13872653+mmguero@users.noreply.github.com> Date: Tue May 18 11:59:45 2021 -0600 something in 51ec8fd broke something, this branch is debugging it --- analyzer/protocol/ldap/ldap.spicy | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/analyzer/protocol/ldap/ldap.spicy b/analyzer/protocol/ldap/ldap.spicy index f256121a..9e0a51fd 100644 --- a/analyzer/protocol/ldap/ldap.spicy +++ b/analyzer/protocol/ldap/ldap.spicy @@ -117,7 +117,7 @@ public type ResultCode = enum { }; #----------------------------------------------------------------------------- -public type Result = unit() { +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 @@ -208,24 +208,23 @@ type BindRequest = unit(inout message: Message) { message.obj = self.name; } var authType: BindAuthType = BindAuthType::NOT_SET; + var authData: bytes = b""; var simpleCreds: string = ""; - var saslData: bytes; : asn1::ASN1Message(True) { if ($$?.application_id) { self.authType = cast(cast($$.application_id)); + self.authData = $$.application_data; } - if ((self.authType == BindAuthType::BIND_AUTH_SIMPLE) && - (|$$.application_data| > 0)) { - self.simpleCreds = $$.application_data.decode(); + if ((self.authType == BindAuthType::BIND_AUTH_SIMPLE) && (|self.authData| > 0)) { + self.simpleCreds = self.authData.decode(); if (|self.simpleCreds| > 0) { message.arg = self.simpleCreds; } - } else { - self.saslData = $$.application_data; } } - saslCreds: SaslCredentials() &parse-from=self.saslData if (self?.saslData) { + 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)); @@ -283,7 +282,7 @@ type FilterType = enum { NOT_SET = 255 }; -type AttributeSelection = unit() { +type AttributeSelection = unit { var attributes: vector; # TODO: parse AttributeSelection as per @@ -301,7 +300,7 @@ type AttributeSelection = unit() { } }; -type AttributeValueAssertion = unit() { +type AttributeValueAssertion = unit { var desc: string = ""; var val: string = ""; @@ -319,7 +318,7 @@ type AttributeValueAssertion = unit() { } }; -type SubstringFilter = unit() { +type SubstringFilter = unit { var ftype: string = ""; var substrings: asn1::ASN1Message; @@ -341,7 +340,7 @@ type SubstringFilter = unit() { } }; -type SearchFilter = unit() { +type SearchFilter = unit { var filterType: FilterType = FilterType::NOT_SET; var filterBytes: bytes = b""; var filterLen: uint64 = 0;