mysql: Handle server connection phase separately from command phase

This avoids interpreting an AuthSwitchRequest (0xfe) during the command
phase as EOF_Packet.

Thanks @AmazingPP.

Closes #3880
This commit is contained in:
Arne Welzel 2024-08-13 16:09:04 +02:00
parent 44a3ed676b
commit b1c63ae4e0
6 changed files with 99 additions and 15 deletions

View file

@ -153,7 +153,7 @@ refine flow MySQL_Flow += {
return true;
%}
function proc_auth_switch_request(msg: AuthSwitchRequest): bool
function proc_auth_switch_request_payload(msg: AuthSwitchRequestPayload): bool
%{
zeek::BifEvent::enqueue_mysql_auth_switch_request(connection()->zeek_analyzer(),
connection()->zeek_analyzer()->Conn(),
@ -201,8 +201,8 @@ refine typeattr Resultset += &let {
proc = $context.flow.proc_resultset(this);
};
refine typeattr AuthSwitchRequest += &let {
proc = $context.flow.proc_auth_switch_request(this);
refine typeattr AuthSwitchRequestPayload += &let {
proc = $context.flow.proc_auth_switch_request_payload(this);
};
refine typeattr AuthMoreData += &let {

View file

@ -296,7 +296,7 @@ type EmptyOrNUL_String = RE/([^\0]*\0)?/;
type MySQL_PDU(is_orig: bool) = record {
hdr : Header;
msg : case is_orig of {
false -> server_msg: Server_Message(hdr.seq_id, hdr.len);
false -> server_msg: Server_Message(hdr.seq_id, hdr.len, state);
true -> client_msg: Client_Message(state);
} &requires(state);
} &let {
@ -310,17 +310,17 @@ type Header = record {
len : uint32 = to_int()(le_len) + 4;
} &length=4;
type Server_Message(seq_id: uint8, pkt_len: uint32) = case is_initial of {
true -> initial_handshake: Initial_Handshake_Packet;
false -> command_response : Command_Response(pkt_len);
} &let {
type Server_Message(seq_id: uint8, pkt_len: uint32, state: int) = case state of {
CONNECTION_PHASE -> connection_phase: Server_Connection_Phase(is_initial);
COMMAND_PHASE -> command_response: Command_Response(pkt_len);
} &requires(is_initial) &let {
is_initial : bool = (seq_id == 0) && ($context.connection.get_previous_seq_id() != 255);
update_seq_id : bool = $context.connection.set_previous_seq_id(seq_id);
};
type Client_Message(state: int) = case state of {
CONNECTION_PHASE -> connection_phase: Connection_Phase_Packets;
COMMAND_PHASE -> command_phase : Command_Request_Packet;
type Server_Connection_Phase(is_initial: bool) = case is_initial of {
true -> initial_handshake: Initial_Handshake_Packet;
false -> subsequent_handshake: Server_Connection_Phase_Packets;
};
# Handshake Request
@ -371,6 +371,19 @@ type Handshake_v9 = record {
scramble : NUL_String;
};
# While in the CONNECTION_PHASE, handle the following packets. Note that
# this is subtly different from Command_Response_Status which interprets
# 0xfe as EOF packet and also has does not support AuthMoreData.
type Server_Connection_Phase_Packets = record {
pkt_type: uint8;
packet: case pkt_type of {
0x00 -> data_ok: OK_Packet;
0x01 -> auth_more_data: AuthMoreData(false);
0xfe -> auth_switch_request: AuthSwitchRequestPayload;
0xff -> data_err: ERR_Packet;
};
};
# Handshake Response
type Handshake_Response_Packet = case $context.connection.get_version() of {
@ -451,6 +464,11 @@ type Handshake_Response_Packet_v9 = record {
# Connection Phase
type Client_Message(state: int) = case state of {
CONNECTION_PHASE -> connection_phase: Connection_Phase_Packets;
COMMAND_PHASE -> command_phase : Command_Request_Packet;
};
type Connection_Phase_Packets = case $context.connection.get_conn_expectation() of {
EXPECT_HANDSHAKE -> handshake_resp: Handshake_Response_Packet;
EXPECT_AUTH_DATA -> auth_data: AuthMoreData(true);
@ -530,10 +548,6 @@ type Command_Response_Status = record {
pkt_type: uint8;
response: case pkt_type of {
0x00 -> data_ok: OK_Packet;
# When still in the CONNECTION_PHASE, the server can reply
# with AuthMoreData which is 0x01 stuffed opaque payload.
# https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_more_data.html
0x01 -> auth_more_data: AuthMoreData(false);
0xfe -> data_eof: EOF_Packet(EOF_END);
0xff -> data_err: ERR_Packet;
default -> unknown: empty;
@ -635,6 +649,10 @@ type AuthMoreData(is_orig: bool) = record {
type AuthSwitchRequest = record {
status: uint8 &enforce(status==254);
payload: AuthSwitchRequestPayload;
};
type AuthSwitchRequestPayload = record {
name : NUL_String;
data : bytestring &restofdata;
} &let {
@ -875,6 +893,7 @@ refine connection MySQL_Conn += {
expected_ = EXPECT_STATUS;
break;
case COM_CHANGE_USER:
// XXX: Could we switch into CONNECTION_PHASE instead?
expected_ = EXPECT_AUTH_SWITCH;
break;
case COM_BINLOG_DUMP:

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path mysql
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p cmd arg success rows response
#types time string addr port addr port string string bool count string
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.21.179.53 58227 10.21.20.70 3306 login dsm1 F - Access denied for user 'dsm1'@'10.21.179.53' (using password: YES)
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,9 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
mysql auth plugin, F, caching_sha2_password, "\x037#\x03-\x17\x02B28\x0e4;y\x7f\x01Q\x08\x09\x00, 21
mysql handshake, dsm1
mysql auth plugin, T, caching_sha2_password, \x9dh\x1f\xbb\x8c+\x90\xbe\x06.\x18j\xe3\x90\xa6\x95M\x0c\xc5\x04c\xf4\xfa\xff\xfaJF\x88\x17\xdfu+, 32
mysql auth switch request, sha256_password, (:1\x01tK7wV-BA.\x17hf)D\x0b(\x00, 21
mysql auth more data, T, \x01, 1
mysql auth more data, F, -----BEGIN PUBLIC KEY-----\x0aMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwp+ZvMw7i8dEUQK43WIz\x0aB1F7rnvXmrw+awePuJu2Syn+aqHU/pL92WP/zpbFPbliaNUcrEf9dZXgYmW96zcV\x0aUaJTwf2RtV9PcZVurYLjG0LDjiigAl5p3PehRwxFgNGiqO44MtTj+KFm0AjBZesD\x0aSPiwa/2+AamXvzWb6KzLRyICciNfA0gMCXJpxPHsq3pJrob0FimviRiaTfTyYe3n\x0aDNOlvtmoUeD4nv3Jg22+mwlHd5qdc04wKJDzaeDgQqRx9O5FTwQ5LGCydyUMXFtl\x0a03PnlGJ+UZqTrsyo3oXwhVmrHOuzd64Oz6lO74uNWcpSpsEV9EOW9IVidca1uxeA\x0aXQIDAQAB\x0a-----END PUBLIC KEY-----\x0a, 451
mysql auth more data, T, }[\xdc\x7f\xca\xc1\x83\xb7\x14\x00%\xb6\x8fG\x89r\x935\xd54\x0c<9\xb2N\x10\x1d\x03\xcc\xb9u\x1f\xf0`\xad\x86\xcc*p\xefs\x8a\xac\x03\xbe\xe1\xb6 1\xd4\xd7'\xa7\x89\xf7\xed\xdbu\xad\x85W\x96\xd1\xfe\x89D+\x90\xc7/\x1b\xb0h\xadKM\x8c\xd9Z\x9c\x9c.\x0aJ\xcd)9\x1d&{5p\x1e\xd3\xa5\x1b\xdf\x1a\x8a\x82\xb6\x0cGm<\xbfw~\xc4\x9a\x17\x09\xca\xc0J\x01\x8d\xbe\x06o\xddo\xe12\xc5L\x80\x0b\xe0_nk\x1c\xac)\xcd\x02\xc7\xc3\x80af\xe3\xd7\xd1\x7f\xdb\x00\xa0u\xd2\xad\x0e\x17\x14\xc7K\x06\x05\xb02\xc2+\xda\xac\x13=)YJ;]\xe2\xf3\xd7\xa034\x8f\xcd\xbf\x13R\x1c\xdd\x8e\xe2\x93\x9f\x0f8$\xfb\x9d\x02\xe9.\xeco\xb2U\x80\xd0\x12\xf3\xec\xaa\xedx\x87\x0cq\x8aE\\x8c\x8f\x1d\x84\x07\x0e\xc1\xb0Y<\xf7\x01b5\x9fm\x8dSB\x8a\xaf\xa3\xc5\x0dD\x88fL6b\x05\x06\x8c.\xcag\xaf\xae\xd0\xb0\xf8, 256
mysql error, 1045, Access denied for user 'dsm1'@'10.21.179.53' (using password: YES)

View file

@ -0,0 +1,45 @@
# @TEST-EXEC: zeek -b -C -r $TRACES/mysql/mysql8-navicat-login-failed.pcapng %INPUT >out
# @TEST-EXEC: btest-diff out
# @TEST-EXEC: btest-diff mysql.log
@load base/protocols/mysql
event mysql_ok(c: connection, affected_rows: count)
{
print "mysql ok", affected_rows;
}
event mysql_eof(c: connection, is_intermediate: bool)
{
print "mysql eof", is_intermediate;
}
event mysql_error(c: connection, code: count, msg: string)
{
print "mysql error", code, msg;
}
event mysql_command_request(c: connection, command: count, arg: string)
{
print "mysql request", command, arg;
}
event mysql_handshake(c: connection, username: string)
{
print "mysql handshake", username;
}
event mysql_auth_plugin(c: connection, is_orig: bool, name: string, data: string)
{
print "mysql auth plugin", is_orig, name, data, |data|;
}
event mysql_auth_switch_request(c: connection, name: string, data: string)
{
print "mysql auth switch request", name, data, |data|;
}
event mysql_auth_more_data(c: connection, is_orig: bool, data: string)
{
print "mysql auth more data", is_orig, data, |data|;
}