mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 22:58:20 +00:00

Skimming through the RFC, the previous approach of having containers for most fields seems unfounded for normal protocol operation. The new weirds could just as well be considered protocol violations. Outside of duplicated or missed data they just shouldn't happen for well-behaved client/server behavior. Additionally, with non-conformant traffic it would be trivial to cause unbounded state growth and immense log record sizes. Unfortunately, things have become a bit clunky now. Closes #3504
403 lines
14 KiB
Text
403 lines
14 KiB
Text
# 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;
|
|
}
|
|
|
|
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 ( 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;
|
|
}
|
|
|
|
}
|