# Copyright (c) 2021 by the Zeek Project. See LICENSE for details. @load base/frameworks/reporter @load base/protocols/conn/removal-hooks @load ./consts module LDAP; export { redef enum Log::ID += { LDAP_LOG, LDAP_SEARCH_LOG }; ## TCP ports which should be considered for analysis. const ports_tcp = { 389/tcp, 3268/tcp } &redef; ## UDP ports which should be considered for analysis. const ports_udp = { 389/udp } &redef; ## Whether clear text passwords are captured or not. option default_capture_password = F; ## Whether to log LDAP search attributes or not. option default_log_search_attributes = F; ## Default logging policy hook for LDAP_LOG. global log_policy: Log::PolicyHook; ## Default logging policy hook for LDAP_SEARCH_LOG. global log_policy_search: Log::PolicyHook; ## LDAP finalization hook. global finalize_ldap: Conn::RemovalHook; ############################################################################# # This is the format of ldap.log (ldap operations minus search-related) # Each line represents a unique connection+message_id (requests/responses) type MessageInfo: record { # Timestamp for when the event happened. ts: time &log; # Unique ID for the connection. uid: string &log; # The connection's 4-tuple of endpoint addresses/ports. id: conn_id &log; # Message ID message_id: int &log &optional; # LDAP version version: int &log &optional; # Normalized operation (e.g., bind_request and bind_response to "bind") opcode: string &log &optional; # Result code result: string &log &optional; # Result diagnostic message diagnostic_message: string &log &optional; # Object object: string &log &optional; # Argument argument: string &log &optional; }; ############################################################################# # This is the format of ldap_search.log (search-related messages only) # Each line represents a unique connection+message_id (requests/responses) type SearchInfo: record { # Timestamp for when the event happened. ts: time &log; # Unique ID for the connection. uid: string &log; # The connection's 4-tuple of endpoint addresses/ports. id: conn_id &log; # Message ID message_id: int &log &optional; # sets of search scope and deref alias scope: string &log &optional; deref_aliases: string &log &optional; # Base search objects base_object: string &log &optional; # Number of results returned result_count: count &log &optional; # Result code of search operation result: string &log &optional; # Result diagnostic message diagnostic_message: string &log &optional; # A string representation of the search filter used in the query filter: string &log &optional; # A list of attributes that were returned in the search attributes: vector of string &log &optional; }; type State: record { messages: table[int] of MessageInfo &optional; searches: table[int] of SearchInfo &optional; }; # Event that can be handled to access the ldap record as it is sent on # to the logging framework. global log_ldap: event(rec: LDAP::MessageInfo); global log_ldap_search: event(rec: LDAP::SearchInfo); } redef record connection += { ldap: State &optional; }; redef likely_server_ports += { LDAP::ports_tcp, LDAP::ports_udp }; ############################################################################# global OPCODES_FINISHED: set[LDAP::ProtocolOpcode] = { LDAP::ProtocolOpcode_BIND_RESPONSE, LDAP::ProtocolOpcode_UNBIND_REQUEST, LDAP::ProtocolOpcode_SEARCH_RESULT_DONE, LDAP::ProtocolOpcode_MODIFY_RESPONSE, LDAP::ProtocolOpcode_ADD_RESPONSE, LDAP::ProtocolOpcode_DEL_RESPONSE, LDAP::ProtocolOpcode_MOD_DN_RESPONSE, LDAP::ProtocolOpcode_COMPARE_RESPONSE, LDAP::ProtocolOpcode_ABANDON_REQUEST, LDAP::ProtocolOpcode_EXTENDED_RESPONSE }; global OPCODES_SEARCH: set[LDAP::ProtocolOpcode] = { LDAP::ProtocolOpcode_SEARCH_REQUEST, LDAP::ProtocolOpcode_SEARCH_RESULT_ENTRY, LDAP::ProtocolOpcode_SEARCH_RESULT_DONE, LDAP::ProtocolOpcode_SEARCH_RESULT_REFERENCE }; ############################################################################# event zeek_init() &priority=5 { Analyzer::register_for_ports(Analyzer::ANALYZER_LDAP_TCP, LDAP::ports_tcp); Analyzer::register_for_ports(Analyzer::ANALYZER_LDAP_UDP, LDAP::ports_udp); Log::create_stream(LDAP::LDAP_LOG, [$columns=MessageInfo, $ev=log_ldap, $path="ldap", $policy=log_policy]); Log::create_stream(LDAP::LDAP_SEARCH_LOG, [$columns=SearchInfo, $ev=log_ldap_search, $path="ldap_search", $policy=log_policy_search]); } ############################################################################# function set_session(c: connection, message_id: int, opcode: LDAP::ProtocolOpcode) { if (! c?$ldap ) { c$ldap = State(); Conn::register_removal_hook(c, finalize_ldap); } if (! c$ldap?$messages ) c$ldap$messages = table(); if (! c$ldap?$searches ) c$ldap$searches = table(); if ((opcode in OPCODES_SEARCH) && (message_id !in c$ldap$searches)) { c$ldap$searches[message_id] = [$ts=network_time(), $uid=c$uid, $id=c$id, $message_id=message_id, $result_count=0]; } else if ((opcode !in OPCODES_SEARCH) && (message_id !in c$ldap$messages)) { c$ldap$messages[message_id] = [$ts=network_time(), $uid=c$uid, $id=c$id, $message_id=message_id]; } } ############################################################################# event LDAP::message(c: connection, message_id: int, opcode: LDAP::ProtocolOpcode, result: LDAP::ResultCode, matched_dn: string, diagnostic_message: string, object: string, argument: string) { if (opcode == LDAP::ProtocolOpcode_SEARCH_RESULT_DONE) { set_session(c, message_id, opcode); local sm = c$ldap$searches[message_id]; if ( result != LDAP::ResultCode_Undef ) { local sresult_str = RESULT_CODES[result]; if ( sm?$result && sm$result != sresult_str ) { Reporter::conn_weird("LDAP_search_result_change", c, fmt("%s: %s -> %s", message_id, sm$result, sresult_str), "LDAP"); } sm$result = sresult_str; } if ( diagnostic_message != "" ) { if ( sm?$diagnostic_message && sm$diagnostic_message != diagnostic_message ) { Reporter::conn_weird("LDAP_search_diagnostic_message_change", c, fmt("%s: %s -> %s", message_id, sm$diagnostic_message, diagnostic_message), "LDAP"); } sm$diagnostic_message = diagnostic_message; } Log::write(LDAP::LDAP_SEARCH_LOG, sm); delete c$ldap$searches[message_id]; } else if (opcode !in OPCODES_SEARCH) { # search is handled via LDAP::search_request() set_session(c, message_id, opcode); local m = c$ldap$messages[message_id]; local opcode_str = PROTOCOL_OPCODES[opcode]; # bind request is explicitly handled via LDAP::bind_request() and # can assume we have a more specific m$opcode set. if ( opcode_str != "bind" ) { if ( m?$opcode && opcode_str != m$opcode ) { Reporter::conn_weird("LDAP_message_opcode_change", c, fmt("%s: %s -> %s", message_id, m$opcode, opcode_str), "LDAP"); } m$opcode = opcode_str; } else if ( ! m?$opcode ) { # This can happen if we see a bind response before the bind request. Reporter::conn_weird("LDAP_bind_without_opcode", c, fmt("%s: %s", message_id, opcode_str), "LDAP"); m$opcode = opcode_str; } if ( result != LDAP::ResultCode_Undef ) { local result_str = RESULT_CODES[result]; if ( m?$result && m$result != result_str ) { Reporter::conn_weird("LDAP_message_result_change", c, fmt("%s: %s -> %s", message_id, m$result, result_str), "LDAP"); } m$result = result_str; } if ( diagnostic_message != "" ) { if ( m?$diagnostic_message && diagnostic_message != m$diagnostic_message ) { Reporter::conn_weird("LDAP_message_diagnostic_message_change", c, fmt("%s: %s -> %s", message_id, m$diagnostic_message, diagnostic_message), "LDAP"); } m$diagnostic_message = diagnostic_message; } if ( object != "" ) { if ( m?$object && m$object != object ) { Reporter::conn_weird("LDAP_message_object_change", c, fmt("%s: %s -> %s", message_id, m$object, object), "LDAP"); } m$object = object; if ( opcode == LDAP::ProtocolOpcode_EXTENDED_REQUEST ) m$object += fmt(" (%s)", EXTENDED_REQUESTS[object]); } if ( argument != "" ) { if ( m$opcode == BIND_SIMPLE && ! default_capture_password ) argument = "REDACTED"; if ( m?$argument && m$argument != argument ) { Reporter::conn_weird("LDAP_message_argument_change", c, fmt("%s: %s -> %s", message_id, m$argument, argument), "LDAP"); } m$argument = argument; } if (opcode in OPCODES_FINISHED) { Log::write(LDAP::LDAP_LOG, m); delete c$ldap$messages[message_id]; } } } ############################################################################# event LDAP::search_request(c: connection, message_id: int, base_object: string, scope: LDAP::SearchScope, deref: LDAP::SearchDerefAlias, size_limit: int, time_limit: int, types_only: bool, filter: string, attributes: vector of string) { set_session(c, message_id, LDAP::ProtocolOpcode_SEARCH_REQUEST); local sm = c$ldap$searches[message_id]; if ( scope != LDAP::SearchScope_Undef ) { local scope_str = SEARCH_SCOPES[scope]; if ( sm?$scope && sm$scope != scope_str ) { Reporter::conn_weird("LDAP_search_scope_change", c, fmt("%s: %s -> %s", message_id, sm$scope, scope_str), "LDAP"); } sm$scope = scope_str; } if ( deref != LDAP::SearchDerefAlias_Undef ) { local deref_aliases_str = SEARCH_DEREF_ALIASES[deref]; if ( sm?$deref_aliases && sm$deref_aliases != deref_aliases_str ) { Reporter::conn_weird("LDAP_search_deref_aliases_change", c, fmt("%s: %s -> %s", message_id, sm$deref_aliases, deref_aliases_str), "LDAP"); } sm$deref_aliases = deref_aliases_str; } if ( base_object != "" ) { if ( sm?$base_object && sm$base_object != base_object ) { Reporter::conn_weird("LDAP_search_base_object_change", c, fmt("%s: %s -> %s", message_id, sm$base_object, base_object), "LDAP"); } sm$base_object = base_object; } if ( sm?$filter && sm$filter != filter ) Reporter::conn_weird("LDAP_search_filter_change", c, fmt("%s: %s -> %s", message_id, sm$filter, filter), "LDAP"); sm$filter = filter; if ( default_log_search_attributes ) { if ( sm?$attributes && cat(sm$attributes) != cat(attributes) ) { Reporter::conn_weird("LDAP_search_attributes_change", c, fmt("%s: %s -> %s", message_id, sm$attributes, attributes), "LDAP"); } sm$attributes = attributes; } } ############################################################################# event LDAP::search_result_entry(c: connection, message_id: int, object_name: string) { set_session(c, message_id, LDAP::ProtocolOpcode_SEARCH_RESULT_ENTRY); c$ldap$searches[message_id]$result_count += 1; } ############################################################################# event LDAP::bind_request(c: connection, message_id: int, version: int, name: string, authType: LDAP::BindAuthType, authInfo: string) { set_session(c, message_id, LDAP::ProtocolOpcode_BIND_REQUEST); local m = c$ldap$messages[message_id]; if ( ! m?$version ) m$version = version; # Getting herre, we don't expect the LDAP opcode to be set at all # and it'll be overwritten below. if ( m?$opcode ) Reporter::conn_weird("LDAP_bind_opcode_already_set", c, m$opcode, "LDAP"); if (authType == LDAP::BindAuthType_BIND_AUTH_SIMPLE) { m$opcode = BIND_SIMPLE; } else if (authType == LDAP::BindAuthType_BIND_AUTH_SASL) { m$opcode = BIND_SASL; } else { Reporter::conn_weird("LDAP_unknown_auth_type", c, cat(authType), "LDAP"); m$opcode = cat(authType); } } ############################################################################# hook finalize_ldap(c: connection) { # log any "pending" unlogged LDAP messages/searches if ( c$ldap?$messages && (|c$ldap$messages| > 0) ) { for ( [mid], m in c$ldap$messages ) { if (mid > 0) 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; } }