Merge remote-tracking branch 'origin/master' into topic/johanna/spicy-tls

* origin/master: (386 commits)
  Normalize version strings in test
  Update doc submodule [nomail] [skip ci]
  Update external testing baseline hashes
  fuzzers: Add DTLS fuzzer
  generic-analyzer-fuzzer: Support NextPacket() fuzzing
  Require `truncate` for a test using it
  Bump outdated baseline
  Fix tests so they work both with GNU and BSD tools
  Install libmaxminddb in macOS CI
  Bump auxil/spicy to latest release
  Supervisor: Handle EAGAIN error on stem pipe
  fuzzer-setup: Allow customization without recompiling
  ssl: Prevent unbounded ssl_history growth
  ssl: Cap number of alerts parsed from SSL record
  subdir-btest: Allow setting build_dir
  Update doc submodule [nomail] [skip ci]
  CI: Pass -A flag to btest for cluster-testing builds
  Update doc submodule [nomail] [skip ci]
  Update baselines
  ftp: Do not base seq on number of pending commands
  ...
This commit is contained in:
Johanna Amann 2023-10-30 12:28:40 +00:00
commit 0afe94154d
800 changed files with 109788 additions and 98811 deletions

View file

@ -88,8 +88,6 @@ function set_state(c: connection, state_x: BackingState)
c$dce_rpc$endpoint = uuid_endpoint_map[c$dce_rpc_state$uuid];
if ( c$dce_rpc_state?$named_pipe )
c$dce_rpc$named_pipe = c$dce_rpc_state$named_pipe;
Conn::register_removal_hook(c, finalize_dce_rpc);
}
function set_session(c: connection, fid: count)
@ -97,7 +95,9 @@ function set_session(c: connection, fid: count)
if ( ! c?$dce_rpc_backing )
{
c$dce_rpc_backing = table();
Conn::register_removal_hook(c, finalize_dce_rpc);
}
if ( fid !in c$dce_rpc_backing )
{
local info = Info($ts=network_time(),$id=c$id,$uid=c$uid);
@ -216,6 +216,23 @@ event dce_rpc_response(c: connection, fid: count, ctx_id: count, opnum: count, s
}
}
event smb_discarded_dce_rpc_analyzers(c: connection)
{
# This event is raised when the DCE-RPC analyzers table
# grew too large. Assume things are broken and wipe
# the backing table.
delete c$dce_rpc_backing;
Reporter::conn_weird("SMB_discarded_dce_rpc_analyzers", c, "", "SMB");
}
# If a fid representing a pipe was closed, remove it from dce_rpc_backing.
event smb2_close_request(c: connection, hdr: SMB2::Header, file_id: SMB2::GUID) &priority=-5
{
local fid = file_id$persistent + file_id$volatile;
if ( c?$dce_rpc_backing )
delete c$dce_rpc_backing[fid];
}
hook finalize_dce_rpc(c: connection)
{
if ( ! c?$dce_rpc )

View file

@ -204,11 +204,16 @@ event DHCP::aggregate_msgs(ts: time, id: conn_id, uid: string, is_orig: bool, ms
log_info$msg_types += DHCP::message_types[msg$m_type];
# The is_orig flag is T for "connections" initiated by servers
# to broadcast addresses, otherwise is_orig indicates that this
# is a DHCP client.
local is_client = is_orig && (id$orig_h == 0.0.0.0 || id$orig_p == 68/udp || id$resp_p == 67/udp);
# Let's watch for messages in any DHCP message type
# and split them out based on client and server.
if ( options?$message )
{
if ( is_orig )
if ( is_client )
log_info$client_message = options$message;
else
log_info$server_message = options$message;
@ -218,7 +223,7 @@ event DHCP::aggregate_msgs(ts: time, id: conn_id, uid: string, is_orig: bool, ms
# expiration handling.
log_info$last_message_ts = ts;
if ( is_orig ) # client requests
if ( is_client ) # client requests
{
# Assign the client addr in case this is a session
# of only INFORM messages (no lease handed out).
@ -246,12 +251,27 @@ event DHCP::aggregate_msgs(ts: time, id: conn_id, uid: string, is_orig: bool, ms
{
# Only log the address of the server if it handed out
# an IP address.
if ( msg$yiaddr != 0.0.0.0 &&
id$resp_h != 255.255.255.255 )
if ( msg$yiaddr != 0.0.0.0 )
{
log_info$server_addr = id$resp_h;
log_info$server_port = id$resp_p;
log_info$client_port = id$orig_p;
if ( is_orig )
{
# This is a server message and is_orig is T.
# This means it's a DHCP server broadcasting
# and the server is the originator.
log_info$server_addr = id$orig_h;
log_info$server_port = id$orig_p;
log_info$client_port = id$resp_p;
}
else
{
# When a server sends to a non-broadcast
# address, Zeek's connection flipping is
# in effect and the server is the responder
# instead.
log_info$server_addr = id$resp_h;
log_info$server_port = id$resp_p;
log_info$client_port = id$orig_p;
}
}
# Only use the client hardware address from the server

View file

@ -64,6 +64,9 @@ export {
## to are tracked here.
pending_commands: PendingCmds;
## Sequence number of previous command.
command_seq: count &default=0;
## Indicates if the session is in active or passive mode.
passive: bool &default=F;

View file

@ -165,7 +165,7 @@ function set_ftp_session(c: connection)
Conn::register_removal_hook(c, finalize_ftp);
# Add a shim command so the server can respond with some init response.
add_pending_cmd(c$ftp$pending_commands, "<init>", "");
add_pending_cmd(c$ftp$pending_commands, ++c$ftp$command_seq, "<init>", "");
}
}
@ -261,8 +261,8 @@ event ftp_request(c: connection, command: string, arg: string) &priority=5
# attackers.
if ( c?$ftp && c$ftp?$cmdarg && c$ftp?$reply_code )
{
remove_pending_cmd(c$ftp$pending_commands, c$ftp$cmdarg);
ftp_message(c);
if ( remove_pending_cmd(c$ftp$pending_commands, c$ftp$cmdarg) )
ftp_message(c);
}
local id = c$id;
@ -270,7 +270,7 @@ event ftp_request(c: connection, command: string, arg: string) &priority=5
# Queue up the new command and argument
if ( |c$ftp$pending_commands| < max_pending_commands )
add_pending_cmd(c$ftp$pending_commands, command, arg);
add_pending_cmd(c$ftp$pending_commands, ++c$ftp$command_seq, command, arg);
else
Reporter::conn_weird("FTP_too_many_pending_commands", c,
cat(|c$ftp$pending_commands|), "FTP");

View file

@ -78,9 +78,9 @@ export {
};
}
function add_pending_cmd(pc: PendingCmds, cmd: string, arg: string): CmdArg
function add_pending_cmd(pc: PendingCmds, seq: count, cmd: string, arg: string): CmdArg
{
local ca = [$cmd = cmd, $arg = arg, $seq=|pc|+1, $ts=network_time()];
local ca = [$cmd = cmd, $arg = arg, $seq=seq, $ts=network_time()];
pc[ca$seq] = ca;
return ca;

View file

@ -103,7 +103,7 @@ event http_header(c: connection, is_orig: bool, name: string, value: string) &pr
c$http$current_entity$filename = extract_filename_from_content_disposition(value);
}
else if ( name == "CONTENT-TYPE" &&
/[nN][aA][mM][eE][:blank:]*=/ in value )
/[nN][aA][mM][eE][[:blank:]]*=/ in value )
{
c$http$current_entity$filename = extract_filename_from_content_disposition(value);
}

View file

@ -133,6 +133,12 @@ export {
## HTTP finalization hook. Remaining HTTP info may get logged when it's called.
global finalize_http: Conn::RemovalHook;
## Only allow that many pending requests on a single connection.
## If this number is exceeded, all pending requests are flushed
## out and request/response tracking reset to prevent unbounded
## state growth.
option max_pending_requests = 100;
}
# Add the http state tracking fields to the connection record.
@ -205,6 +211,47 @@ event http_request(c: connection, method: string, original_URI: string,
Conn::register_removal_hook(c, finalize_http);
}
# Request/response tracking exists to account for HTTP pipelining.
# It fails if more responses have been seen than requests. If that
# happens, just fast-forward current_request such that the next
# response matches the in-flight request.
if ( c$http_state$current_request < c$http_state$current_response )
{
Reporter::conn_weird("HTTP_response_before_request", c);
c$http_state$current_request = c$http_state$current_response;
}
# Too many requests are pending for which we have not yet observed a
# reply. This might be due to excessive HTTP pipelining, one-sided
# traffic capture, or the responder side of the HTTP analyzer having
# been disabled. In any case, we simply log out all pending requests
# to make room for a new one. Any matching of pipelined requests and
# responses is most likely totally off anyhow.
if ( max_pending_requests > 0 && |c$http_state$pending| > max_pending_requests )
{
Reporter::conn_weird("HTTP_excessive_pipelining", c);
if ( c$http_state$current_response == 0 )
++c$http_state$current_response;
while ( c$http_state$current_response < c$http_state$current_request )
{
local cr = c$http_state$current_response;
if ( cr in c$http_state$pending )
{
Log::write(HTTP::LOG, c$http_state$pending[cr]);
delete c$http_state$pending[cr];
}
else
{
# The above should have been true...
# Reporter::error(fmt("Expected pending request at %d", cr));
}
++c$http_state$current_response;
}
}
++c$http_state$current_request;
set_state(c, T);
@ -290,7 +337,7 @@ event http_header(c: connection, is_orig: bool, name: string, value: string) &pr
{
if ( /^[bB][aA][sS][iI][cC] / in value )
{
local userpass = decode_base64_conn(c$id, sub(value, /[bB][aA][sS][iI][cC][[:blank:]]/, ""));
local userpass = decode_base64_conn(c$id, sub(value, /[bB][aA][sS][iI][cC][[:blank:]]+/, ""));
local up = split_string(userpass, /:/);
if ( |up| >= 2 )
{

View file

@ -0,0 +1,6 @@
@if ( have_spicy_analyzers() )
@load ./spicy-events.zeek
@load-sigs ./dpd.sig
@load ./consts
@load ./main.zeek
@endif

View file

@ -0,0 +1,123 @@
module LDAP;
export {
const PROTOCOL_OPCODES = { [ LDAP::ProtocolOpcode_BIND_REQUEST ] = "bind", [
LDAP::ProtocolOpcode_BIND_RESPONSE ] = "bind", [
LDAP::ProtocolOpcode_UNBIND_REQUEST ] = "unbind", [
LDAP::ProtocolOpcode_SEARCH_REQUEST ] = "search", [
LDAP::ProtocolOpcode_SEARCH_RESULT_ENTRY ] = "search", [
LDAP::ProtocolOpcode_SEARCH_RESULT_DONE ] = "search", [
LDAP::ProtocolOpcode_MODIFY_REQUEST ] = "modify", [
LDAP::ProtocolOpcode_MODIFY_RESPONSE ] = "modify", [
LDAP::ProtocolOpcode_ADD_REQUEST ] = "add", [
LDAP::ProtocolOpcode_ADD_RESPONSE ] = "add", [
LDAP::ProtocolOpcode_DEL_REQUEST ] = "delete", [
LDAP::ProtocolOpcode_DEL_RESPONSE ] = "delete", [
LDAP::ProtocolOpcode_MOD_DN_REQUEST ] = "modify", [
LDAP::ProtocolOpcode_MOD_DN_RESPONSE ] = "modify", [
LDAP::ProtocolOpcode_COMPARE_REQUEST ] = "compare", [
LDAP::ProtocolOpcode_COMPARE_RESPONSE ] = "compare", [
LDAP::ProtocolOpcode_ABANDON_REQUEST ] = "abandon", [
LDAP::ProtocolOpcode_SEARCH_RESULT_REFERENCE ] = "search", [
LDAP::ProtocolOpcode_EXTENDED_REQUEST ] = "extended", [
LDAP::ProtocolOpcode_EXTENDED_RESPONSE ] = "extended", [
LDAP::ProtocolOpcode_INTERMEDIATE_RESPONSE ] = "intermediate" }
&default="unknown";
const BIND_SIMPLE = "bind simple";
const BIND_SASL = "bind SASL";
const RESULT_CODES = { [ LDAP::ResultCode_SUCCESS ] = "success", [
LDAP::ResultCode_OPERATIONS_ERROR ] = "operations error", [
LDAP::ResultCode_PROTOCOL_ERROR ] = "protocol error", [
LDAP::ResultCode_TIME_LIMIT_EXCEEDED ] = "time limit exceeded", [
LDAP::ResultCode_SIZE_LIMIT_EXCEEDED ] = "size limit exceeded", [
LDAP::ResultCode_COMPARE_FALSE ] = "compare false", [
LDAP::ResultCode_COMPARE_TRUE ] = "compare true", [
LDAP::ResultCode_AUTH_METHOD_NOT_SUPPORTED ] =
"auth method not supported", [
LDAP::ResultCode_STRONGER_AUTH_REQUIRED ] =
"stronger auth required", [ LDAP::ResultCode_PARTIAL_RESULTS ] =
"partial results", [ LDAP::ResultCode_REFERRAL ] = "referral", [
LDAP::ResultCode_ADMIN_LIMIT_EXCEEDED ] = "admin limit exceeded", [
LDAP::ResultCode_UNAVAILABLE_CRITICAL_EXTENSION ] =
"unavailable critical extension", [
LDAP::ResultCode_CONFIDENTIALITY_REQUIRED ] =
"confidentiality required", [ LDAP::ResultCode_SASL_BIND_IN_PROGRESS ] =
"SASL bind in progress", [ LDAP::ResultCode_NO_SUCH_ATTRIBUTE ] =
"no such attribute", [ LDAP::ResultCode_UNDEFINED_ATTRIBUTE_TYPE ] =
"undefined attribute type", [
LDAP::ResultCode_INAPPROPRIATE_MATCHING ] =
"inappropriate matching", [ LDAP::ResultCode_CONSTRAINT_VIOLATION ] =
"constraint violation", [ LDAP::ResultCode_ATTRIBUTE_OR_VALUE_EXISTS ] =
"attribute or value exists", [
LDAP::ResultCode_INVALID_ATTRIBUTE_SYNTAX ] =
"invalid attribute syntax", [ LDAP::ResultCode_NO_SUCH_OBJECT ] =
"no such object", [ LDAP::ResultCode_ALIAS_PROBLEM ] =
"alias problem", [ LDAP::ResultCode_INVALID_DNSYNTAX ] =
"invalid DN syntax", [ LDAP::ResultCode_ALIAS_DEREFERENCING_PROBLEM ] =
"alias dereferencing problem", [
LDAP::ResultCode_INAPPROPRIATE_AUTHENTICATION ] =
"inappropriate authentication", [
LDAP::ResultCode_INVALID_CREDENTIALS ] = "invalid credentials", [
LDAP::ResultCode_INSUFFICIENT_ACCESS_RIGHTS ] =
"insufficient access rights", [ LDAP::ResultCode_BUSY ] = "busy", [
LDAP::ResultCode_UNAVAILABLE ] = "unavailable", [
LDAP::ResultCode_UNWILLING_TO_PERFORM ] = "unwilling to perform", [
LDAP::ResultCode_LOOP_DETECT ] = "loop detect", [
LDAP::ResultCode_SORT_CONTROL_MISSING ] = "sort control missing", [
LDAP::ResultCode_OFFSET_RANGE_ERROR ] = "offset range error", [
LDAP::ResultCode_NAMING_VIOLATION ] = "naming violation", [
LDAP::ResultCode_OBJECT_CLASS_VIOLATION ] =
"object class violation", [ LDAP::ResultCode_NOT_ALLOWED_ON_NON_LEAF ] =
"not allowed on non-leaf", [ LDAP::ResultCode_NOT_ALLOWED_ON_RDN ] =
"not allowed on RDN", [ LDAP::ResultCode_ENTRY_ALREADY_EXISTS ] =
"entry already exists", [
LDAP::ResultCode_OBJECT_CLASS_MODS_PROHIBITED ] =
"object class mods prohibited", [ LDAP::ResultCode_RESULTS_TOO_LARGE ] =
"results too large", [ LDAP::ResultCode_AFFECTS_MULTIPLE_DSAS ] =
"affects multiple DSAs", [ LDAP::ResultCode_CONTROL_ERROR ] =
"control error", [ LDAP::ResultCode_OTHER ] = "other", [
LDAP::ResultCode_SERVER_DOWN ] = "server down", [
LDAP::ResultCode_LOCAL_ERROR ] = "local error", [
LDAP::ResultCode_ENCODING_ERROR ] = "encoding error", [
LDAP::ResultCode_DECODING_ERROR ] = "decoding error", [
LDAP::ResultCode_TIMEOUT ] = "timeout", [
LDAP::ResultCode_AUTH_UNKNOWN ] = "auth unknown", [
LDAP::ResultCode_FILTER_ERROR ] = "filter error", [
LDAP::ResultCode_USER_CANCELED ] = "user canceled", [
LDAP::ResultCode_PARAM_ERROR ] = "param error", [
LDAP::ResultCode_NO_MEMORY ] = "no memory", [
LDAP::ResultCode_CONNECT_ERROR ] = "connect error", [
LDAP::ResultCode_NOT_SUPPORTED ] = "not supported", [
LDAP::ResultCode_CONTROL_NOT_FOUND ] = "control not found", [
LDAP::ResultCode_NO_RESULTS_RETURNED ] = "no results returned", [
LDAP::ResultCode_MORE_RESULTS_TO_RETURN ] =
"more results to return", [ LDAP::ResultCode_CLIENT_LOOP ] =
"client loop", [ LDAP::ResultCode_REFERRAL_LIMIT_EXCEEDED ] =
"referral limit exceeded", [ LDAP::ResultCode_INVALID_RESPONSE ] =
"invalid response", [ LDAP::ResultCode_AMBIGUOUS_RESPONSE ] =
"ambiguous response", [ LDAP::ResultCode_TLS_NOT_SUPPORTED ] =
"TLS not supported", [ LDAP::ResultCode_INTERMEDIATE_RESPONSE ] =
"intermediate response", [ LDAP::ResultCode_UNKNOWN_TYPE ] =
"unknown type", [ LDAP::ResultCode_LCUP_INVALID_DATA ] =
"LCUP invalid data", [ LDAP::ResultCode_LCUP_UNSUPPORTED_SCHEME ] =
"LCUP unsupported scheme", [ LDAP::ResultCode_LCUP_RELOAD_REQUIRED ] =
"LCUP reload required", [ LDAP::ResultCode_CANCELED ] =
"canceled", [ LDAP::ResultCode_NO_SUCH_OPERATION ] =
"no such operation", [ LDAP::ResultCode_TOO_LATE ] = "too late", [
LDAP::ResultCode_CANNOT_CANCEL ] = "cannot cancel", [
LDAP::ResultCode_ASSERTION_FAILED ] = "assertion failed", [
LDAP::ResultCode_AUTHORIZATION_DENIED ] = "authorization denied" }
&default="unknown";
const SEARCH_SCOPES = { [ LDAP::SearchScope_SEARCH_BASE ] = "base", [
LDAP::SearchScope_SEARCH_SINGLE ] = "single", [
LDAP::SearchScope_SEARCH_TREE ] = "tree", } &default="unknown";
const SEARCH_DEREF_ALIASES = { [ LDAP::SearchDerefAlias_DEREF_NEVER ] =
"never", [ LDAP::SearchDerefAlias_DEREF_IN_SEARCHING ] =
"searching", [ LDAP::SearchDerefAlias_DEREF_FINDING_BASE ] =
"finding", [ LDAP::SearchDerefAlias_DEREF_ALWAYS ] = "always", }
&default="unknown";
}

View file

@ -0,0 +1,23 @@
signature dpd_ldap_client_udp {
ip-proto == udp
payload /^\x30.\x02\x01.\x60/
}
signature dpd_ldap_server_udp {
ip-proto == udp
payload /^\x30/
requires-reverse-signature dpd_ldap_client_udp
enable "LDAP_UDP"
}
signature dpd_ldap_client_tcp {
ip-proto == tcp
payload /^\x30.\x02\x01.\x60/
}
signature dpd_ldap_server_tcp {
ip-proto == tcp
payload /^\x30/
requires-reverse-signature dpd_ldap_client_tcp
enable "LDAP_TCP"
}

View file

@ -0,0 +1,358 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
@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 operations (e.g., bind_request and bind_response to "bind")
opcodes: set[string] &log &optional;
# Result code(s)
results: set[string] &log &optional;
# result diagnostic message(s)
diagnostic_messages: vector of string &log &optional;
# object(s)
objects: vector of string &log &optional;
# argument(s)
arguments: 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 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
scopes: set[string] &log &optional;
derefs: set[string] &log &optional;
# base search objects
base_objects: vector of string &log &optional;
# number of results returned
result_count: count &log &optional;
# Result code (s)
results: set[string] &log &optional;
# result diagnostic message(s)
diagnostic_messages: vector of string &log &optional;
# a string representation of the search filter used in the query
filter: string &log &optional;
# a list of attributes that were returned in the search
attributes: vector of string &log &optional;
};
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 searches = c$ldap$searches[message_id];
if ( result != LDAP::ResultCode_Undef ) {
if ( ! searches?$results )
searches$results = set();
add searches$results[RESULT_CODES[result]];
}
if ( diagnostic_message != "" ) {
if ( ! searches?$diagnostic_messages )
searches$diagnostic_messages = vector();
searches$diagnostic_messages += diagnostic_message;
}
Log::write(LDAP::LDAP_SEARCH_LOG, searches);
delete c$ldap$searches[message_id];
} else if (opcode !in OPCODES_SEARCH) {
set_session(c, message_id, opcode);
local messages = c$ldap$messages[message_id];
if ( ! messages?$opcodes )
messages$opcodes = set();
add messages$opcodes[PROTOCOL_OPCODES[opcode]];
if ( result != LDAP::ResultCode_Undef ) {
if ( ! messages?$results )
messages$results = set();
add messages$results[RESULT_CODES[result]];
}
if ( diagnostic_message != "" ) {
if ( ! messages?$diagnostic_messages )
messages$diagnostic_messages = vector();
messages$diagnostic_messages += diagnostic_message;
}
if ( object != "" ) {
if ( ! messages?$objects )
messages$objects = vector();
messages$objects += object;
}
if ( argument != "" ) {
if ( ! messages?$arguments )
messages$arguments = vector();
if ("bind simple" in messages$opcodes && !default_capture_password)
messages$arguments += "REDACTED";
else
messages$arguments += argument;
}
if (opcode in OPCODES_FINISHED) {
if ((BIND_SIMPLE in messages$opcodes) ||
(BIND_SASL in messages$opcodes)) {
# don't have both "bind" and "bind <method>" in the operations list
delete messages$opcodes[PROTOCOL_OPCODES[LDAP::ProtocolOpcode_BIND_REQUEST]];
}
Log::write(LDAP::LDAP_LOG, messages);
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);
if ( scope != LDAP::SearchScope_Undef ) {
if ( ! c$ldap$searches[message_id]?$scopes )
c$ldap$searches[message_id]$scopes = set();
add c$ldap$searches[message_id]$scopes[SEARCH_SCOPES[scope]];
}
if ( deref != LDAP::SearchDerefAlias_Undef ) {
if ( ! c$ldap$searches[message_id]?$derefs )
c$ldap$searches[message_id]$derefs = set();
add c$ldap$searches[message_id]$derefs[SEARCH_DEREF_ALIASES[deref]];
}
if ( base_object != "" ) {
if ( ! c$ldap$searches[message_id]?$base_objects )
c$ldap$searches[message_id]$base_objects = vector();
c$ldap$searches[message_id]$base_objects += base_object;
}
c$ldap$searches[message_id]$filter = filter;
if ( default_log_search_attributes ) {
c$ldap$searches[message_id]$attributes = attributes;
}
}
#############################################################################
event LDAP::search_result(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);
if ( ! c$ldap$messages[message_id]?$version )
c$ldap$messages[message_id]$version = version;
if ( ! c$ldap$messages[message_id]?$opcodes )
c$ldap$messages[message_id]$opcodes = set();
if (authType == LDAP::BindAuthType_BIND_AUTH_SIMPLE) {
add c$ldap$messages[message_id]$opcodes[BIND_SIMPLE];
} else if (authType == LDAP::BindAuthType_BIND_AUTH_SASL) {
add c$ldap$messages[message_id]$opcodes[BIND_SASL];
}
}
#############################################################################
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) {
if ((BIND_SIMPLE in m$opcodes) || (BIND_SASL in m$opcodes)) {
# don't have both "bind" and "bind <method>" in the operations list
delete m$opcodes[PROTOCOL_OPCODES[LDAP::ProtocolOpcode_BIND_REQUEST]];
}
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;
}
}

View file

@ -0,0 +1,100 @@
##! Events generated by the LDAP analyzer.
##!
##! See See `RFC4511 <https://tools.ietf.org/html/rfc4511>`__.
## Event generated for each LDAPMessage (either direction).
##
## c: The connection.
##
## message_id: The messageID element.
##
## opcode: The protocolOp field in the message.
##
## result: The result code if the message contains a result.
##
## matched_dn: The DN if the message contains a result.
##
## diagnostic_message: Diagnostic message if the LDAP message contains a result.
##
## object: The object name this message refers to.
##
## argument: Additional arguments this message includes.
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
);
## Event generated for each LDAPMessage containing a BindRequest.
##
## c: The connection.
##
## message_id: The messageID element.
##
## version: The version field in the BindRequest.
##
## name: The name field in the BindRequest.
##
## auth_type: The auth type field in the BindRequest.
##
## auth_info: Additional information related to the used auth type.
global LDAP::bind_request: event(
c: connection,
message_id: int,
version: int,
name: string,
auth_type: LDAP::BindAuthType,
auth_info: string
);
## Event generated for each LDAPMessage containing a SearchRequest.
##
## c: The connection.
##
## message_id: The messageID element.
##
## base_object: The baseObject field in the SearchRequest.
##
## scope: The scope field in the SearchRequest.
##
## deref_alias: The derefAlias field in the SearchRequest
##
## size_limit: The sizeLimit field in the SearchRequest.
##
## time_limit: The timeLimit field in the SearchRequest.
##
## types_only: The typesOnly field in the SearchRequest.
##
## filter: The string representation of the filter field in the SearchRequest.
##
## attributes: Additional attributes of the SearchRequest.
global LDAP::search_request: event (
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
);
## Event generated for each SearchResultEntry in LDAP messages.
##
## c: The connection.
##
## message_id: The messageID element.
##
## object_name: The object name in the SearchResultEntry.
global LDAP::search_result: event (
c: connection,
message_id: int,
object_name: string
);

View file

@ -79,4 +79,3 @@ event modbus_exception(c: connection, headers: ModbusHeaders, code: count) &prio
Log::write(LOG, c$modbus);
delete c$modbus$exception;
}

View file

@ -0,0 +1,5 @@
@ifdef ( Analyzer::ANALYZER_QUIC )
@load ./spicy-events
@load ./consts
@load ./main
@endif

View file

@ -0,0 +1,7 @@
module QUIC;
export {
const version_strings: table[count] of string = {
[0x00000001] = "1",
} &default=function(version: count): string { return fmt("unknown-%x", version); };
}

View file

@ -0,0 +1,221 @@
##! Initial idea for a quic.log.
@load base/frameworks/notice/weird
@load base/protocols/conn/removal-hooks
@load ./consts
module QUIC;
export {
redef enum Log::ID += { LOG };
type Info: record {
## Timestamp of first QUIC packet for this entry.
ts: time &log;
## Unique ID for the connection.
uid: string &log;
## The connection's 4-tuple of endpoint addresses/ports.
id: conn_id &log;
## QUIC version as found in the first INITIAL packet from
## the client.
version: string &log;
## First Destination Connection ID used by client. This is
## random and unpredictable, but used for packet protection
## by client and server.
client_initial_dcid: string &log &optional;
## Server chosen Connection ID usually from server's first
## INITIAL packet. This is to be used by the client in
## subsequent packets.
server_scid: string &log &optional;
## Server name extracted from SNI extension in ClientHello
## packet if available.
server_name: string &log &optional;
## First protocol extracted from ALPN extension in ClientHello
## packet if available.
client_protocol: string &log &optional;
## Experimental QUIC history.
##
## Letters have the following meaning with client-sent
## letters being capitalized:
##
## ====== ====================================================
## Letter Meaning
## ====== ====================================================
## I INIT packet
## H HANDSHAKE packet
## Z 0RTT packet
## R RETRY packet
## C CONNECTION_CLOSE packet
## S SSL Client/Server Hello
## ====== ====================================================
history: string &log &default="";
# Internal state for the history field.
history_state: vector of string;
# Internal state if this record has already been logged.
logged: bool &default=F;
};
global log_quic: event(rec: Info);
global log_policy: Log::PolicyHook;
global finalize_quic: Conn::RemovalHook;
## The maximum length of the history field.
option max_history_length = 100;
}
redef record connection += {
# XXX: We may have multiple QUIC connections with different
# Connection ID over the same UDP connection.
quic: Info &optional;
};
# Faster to modify here than re-compiling .evt files.
const quic_ports = {
443/udp, # HTTP3-over-QUIC
853/udp, # DNS-over-QUIC
784/udp, # DNS-over-QUIC early
};
function add_to_history(c: connection, is_orig: bool, what: string)
{
if ( |c$quic$history_state| == max_history_length )
return;
c$quic$history_state += is_orig ? to_upper(what[0]) : to_lower(what[0]);
if ( |c$quic$history_state| == max_history_length )
Reporter::conn_weird("QUIC_max_history_length_reached", c);
}
function log_record(quic: Info)
{
quic$history = join_string_vec(quic$history_state, "");
Log::write(LOG, quic);
quic$logged = T;
}
function set_conn(c: connection, is_orig: bool, version: count, dcid: string, scid: string)
{
if ( ! c?$quic )
{
c$quic = Info(
$ts=network_time(),
$uid=c$uid,
$id=c$id,
$version=version_strings[version],
);
Conn::register_removal_hook(c, finalize_quic);
}
if ( is_orig && |dcid| > 0 && ! c$quic?$client_initial_dcid )
c$quic$client_initial_dcid = bytestring_to_hexstr(dcid);
if ( ! is_orig && |scid| > 0 )
c$quic$server_scid = bytestring_to_hexstr(scid);
}
event QUIC::initial_packet(c: connection, is_orig: bool, version: count, dcid: string, scid: string)
{
set_conn(c, is_orig, version, dcid, scid);
add_to_history(c, is_orig, "INIT");
}
event QUIC::handshake_packet(c: connection, is_orig: bool, version: count, dcid: string, scid: string)
{
set_conn(c, is_orig, version, dcid, scid);
add_to_history(c, is_orig, "HANDSHAKE");
}
event QUIC::zero_rtt_packet(c: connection, is_orig: bool, version: count, dcid: string, scid: string)
{
set_conn(c, is_orig, version, dcid, scid);
add_to_history(c, is_orig, "ZeroRTT");
}
# RETRY packets trigger a log entry and state reset.
event QUIC::retry_packet(c: connection, is_orig: bool, version: count, dcid: string, scid: string, retry_token: string, integrity_tag: string)
{
if ( ! c?$quic )
set_conn(c, is_orig, version, dcid, scid);
add_to_history(c, is_orig, "RETRY");
log_record(c$quic);
delete c$quic;
}
# Upon a connection_close_frame(), if any c$quic state is pending to be logged, do so
# now and prepare for a new entry.
event QUIC::connection_close_frame(c: connection, is_orig: bool, version: count, dcid: string, scid: string, error_code: count, reason_phrase: string)
{
if ( ! c?$quic )
return;
add_to_history(c, is_orig, "CONNECTION_CLOSE");
log_record(c$quic);
delete c$quic;
}
event ssl_extension_server_name(c: connection, is_client: bool, names: string_vec) &priority=5
{
if ( is_client && c?$quic && |names| > 0 )
c$quic$server_name = names[0];
}
event ssl_extension_application_layer_protocol_negotiation(c: connection, is_client: bool, protocols: string_vec)
{
if ( c?$quic && is_client )
{
c$quic$client_protocol = protocols[0];
if ( |protocols| > 1 )
# Probably not overly weird, but the quic.log only
# works with the first one in the hope to avoid
# vector or concatenation.
Reporter::conn_weird("QUIC_many_protocols", c, cat(protocols));
}
}
event ssl_client_hello(c: connection, version: count, record_version: count, possible_ts: time, client_random: string, session_id: string, ciphers: index_vec, comp_methods: index_vec)
{
if ( ! c?$quic )
return;
add_to_history(c, T, "SSL");
}
event ssl_server_hello(c: connection, version: count, record_version: count, possible_ts: time, server_random: string, session_id: string, cipher: count, comp_method: count) &priority=-5
{
if ( ! c?$quic )
return;
add_to_history(c, F, "SSL");
}
hook finalize_quic(c: connection)
{
if ( ! c?$quic || c$quic$logged )
return;
log_record(c$quic);
}
event zeek_init()
{
Log::create_stream(LOG, [$columns=Info, $ev=log_quic, $path="quic", $policy=log_policy]);
Analyzer::register_for_ports(Analyzer::ANALYZER_QUIC, quic_ports);
}

View file

@ -0,0 +1,82 @@
##! Events generated by the QUIC analyzer.
##!
##! See See `RFC9000 <https://tools.ietf.org/html/rfc9000>`__.
## Generated for a QUIC Initial packet.
##
## c: The connection.
##
## is_orig: True if the packet is from the the connection's originator.
##
## version: The Version field.
##
## dcid: The Destination Connection ID field.
##
## scid: The Source Connection ID field.
##
global QUIC::initial_packet: event(c: connection, is_orig: bool, version: count, dcid: string, scid: string);
## Generated for a QUIC Retry packet.
##
## c: The connection.
##
## is_orig: True if the packet is from the the connection's originator.
##
## version: The Version field.
##
## dcid: The Destination Connection ID field.
##
## scid: The Source Connection ID field.
##
## retry_token: The Retry Token field.
##
## integrity_tag: The Retry Integrity Tag field.
global QUIC::retry_packet: event(c: connection, is_orig: bool, version: count, dcid: string, scid: string, retry_token: string, retry_integrity_tag: string);
## Generated for a QUIC Handshake packet.
##
## c: The connection.
##
## is_orig: True if the packet is from the the connection's originator.
##
## version: The Version field.
##
## dcid: The Destination Connection ID field.
##
## scid: The Source Connection ID field.
global QUIC::handshake_packet: event(c: connection, is_orig: bool, version: count, dcid: string, scid: string);
## Generated for a QUIC 0-RTT packet.
##
## c: The connection.
##
## is_orig: True if the packet is from the the connection's originator.
##
## version: The Version field.
##
## dcid: The Destination Connection ID field.
##
## scid: The Source Connection ID field.
global QUIC::zero_rtt_packet: event(c: connection, is_orig: bool, version: count, dcid: string, scid: string);
## Generated for a QUIC CONNECTION_CLOSE frame.
##
## c: The connection.
##
## is_orig: True if the packet is from the the connection's originator.
##
## version: The Version field.
##
## dcid: The Destination Connection ID field.
##
## scid: The Source Connection ID field.
##
## error_code: Count indicating the reason for closing this connection.
##
## reason_phrase: Additional diagnostic information for the closure.
##
## .. note:: Packets with CONNECTION_CLOSE frames are usually encrypted after connection establishment and not visible to Zeek.
global QUIC::connection_close_frame: event(c: connection, is_orig: bool, version: count, dcid: string, scid: string, error_code: count, reason_phrase: string);

View file

@ -67,7 +67,7 @@ event mime_one_header(c: connection, h: mime_header_rec) &priority=5
c$smtp$entity$filename = extract_filename_from_content_disposition(h$value);
if ( h$name == "CONTENT-TYPE" &&
/[nN][aA][mM][eE][:blank:]*=/ in h$value )
/[nN][aA][mM][eE][[:blank:]]*=/ in h$value )
c$smtp$entity$filename = extract_filename_from_content_disposition(h$value);
}

View file

@ -143,6 +143,10 @@ export {
## (especially with large file transfers).
option disable_analyzer_after_detection = T;
## Maximum length of the ssl_history field to prevent unbounded
## growth when the parser is running into unexpected situations.
option max_ssl_history_length = 100;
## Delays an SSL record for a specific token: the record will not be
## logged as long as the token exists or until 15 seconds elapses.
global delay_log: function(info: Info, token: string);
@ -208,10 +212,16 @@ function set_session(c: connection)
function add_to_history(c: connection, is_client: bool, char: string)
{
if ( |c$ssl$ssl_history| == max_ssl_history_length )
return;
if ( is_client )
c$ssl$ssl_history = c$ssl$ssl_history+to_upper(char);
else
c$ssl$ssl_history = c$ssl$ssl_history+to_lower(char);
if ( |c$ssl$ssl_history| == max_ssl_history_length )
Reporter::conn_weird("SSL_max_ssl_history_length_reached", c);
}
function delay_log(info: Info, token: string)