mysql: Implement and test COM_CHANGE_USER

This reworks the parser such that COM_CHANGE_USER switches the
connection back into the CONNECTION_PHASE so that we can remove the
EXPECT_AUTH_SWITCH special case in the COMMAND_PHASE. Adds two pcaps
produced with Python that actually do COM_CHANGE_USER as it seems
not possible from the MySQL CLI.
This commit is contained in:
Arne Welzel 2024-08-13 17:29:36 +02:00
parent a4c79e7304
commit 02f4665e9b
12 changed files with 275 additions and 18 deletions

View file

@ -84,6 +84,11 @@ event mysql_command_request(c: connection, command: count, arg: string) &priorit
Conn::register_removal_hook(c, finalize_mysql); Conn::register_removal_hook(c, finalize_mysql);
} }
event mysql_change_user(c: connection, username: string) &priority=5
{
c$mysql$arg = username;
}
event mysql_command_request(c: connection, command: count, arg: string) &priority=-5 event mysql_command_request(c: connection, command: count, arg: string) &priority=-5
{ {
if ( c?$mysql && c$mysql?$cmd && c$mysql$cmd == "quit" ) if ( c?$mysql && c$mysql?$cmd && c$mysql$cmd == "quit" )

View file

@ -12,6 +12,18 @@
## .. zeek:see:: mysql_error mysql_ok mysql_server_version mysql_handshake ## .. zeek:see:: mysql_error mysql_ok mysql_server_version mysql_handshake
event mysql_command_request%(c: connection, command: count, arg: string%); event mysql_command_request%(c: connection, command: count, arg: string%);
## Generated for a change user command from a MySQL client.
##
## See the MySQL `documentation <http://dev.mysql.com/doc/internals/en/client-server-protocol.html>`__
## for more information about the MySQL protocol.
##
## c: The connection.
##
## username: The username supplied by the client
##
## .. zeek:see:: mysql_error mysql_ok mysql_server_version mysql_handshake
event mysql_change_user%(c: connection, username: string%);
## Generated for an unsuccessful MySQL response. ## Generated for an unsuccessful MySQL response.
## ##
## See the MySQL `documentation <http://dev.mysql.com/doc/internals/en/client-server-protocol.html>`__ ## See the MySQL `documentation <http://dev.mysql.com/doc/internals/en/client-server-protocol.html>`__

View file

@ -87,10 +87,44 @@ refine flow MySQL_Flow += {
function proc_mysql_command_request_packet(msg: Command_Request_Packet): bool function proc_mysql_command_request_packet(msg: Command_Request_Packet): bool
%{ %{
if ( mysql_command_request ) if ( mysql_command_request )
{
auto arg = to_stringval(${msg.arg});
// CHANGE_USER will have parsed away the arg,
// restore it for backwards compat.
if ( ${msg.command} == COM_CHANGE_USER )
arg = to_stringval(${msg.change_user.sourcedata});
zeek::BifEvent::enqueue_mysql_command_request(connection()->zeek_analyzer(), zeek::BifEvent::enqueue_mysql_command_request(connection()->zeek_analyzer(),
connection()->zeek_analyzer()->Conn(), connection()->zeek_analyzer()->Conn(),
${msg.command}, ${msg.command},
to_stringval(${msg.arg})); std::move(arg));
}
return true;
%}
function proc_mysql_change_user_packet(msg: Change_User_Packet): bool
%{
if ( mysql_change_user )
zeek::BifEvent::enqueue_mysql_change_user(connection()->zeek_analyzer(),
connection()->zeek_analyzer()->Conn(),
zeek::make_intrusive<zeek::StringVal>(c_str(${msg.username})));
if ( mysql_auth_plugin )
{
auto data = to_stringval(${msg.auth_plugin_data});
auto auth_plugin = zeek::val_mgr->EmptyString();
if ( ${msg.have_more_data} )
auth_plugin = zeek::make_intrusive<zeek::StringVal>(c_str(${msg.auth_plugin_name}));
zeek::BifEvent::enqueue_mysql_auth_plugin(connection()->zeek_analyzer(),
connection()->zeek_analyzer()->Conn(),
true /*is_orig*/,
std::move(auth_plugin),
std::move(data));
}
return true; return true;
%} %}
@ -153,7 +187,7 @@ refine flow MySQL_Flow += {
return true; return true;
%} %}
function proc_auth_switch_request_payload(msg: AuthSwitchRequestPayload): bool function proc_auth_switch_request(msg: AuthSwitchRequest): bool
%{ %{
zeek::BifEvent::enqueue_mysql_auth_switch_request(connection()->zeek_analyzer(), zeek::BifEvent::enqueue_mysql_auth_switch_request(connection()->zeek_analyzer(),
connection()->zeek_analyzer()->Conn(), connection()->zeek_analyzer()->Conn(),
@ -183,6 +217,8 @@ refine typeattr Handshake_Response_Packet += &let {
refine typeattr Command_Request_Packet += &let { refine typeattr Command_Request_Packet += &let {
proc = $context.flow.proc_mysql_command_request_packet(this); proc = $context.flow.proc_mysql_command_request_packet(this);
# Enqueue mysql_change_user() *after* mysql_command_request().
proc_change_user = $context.flow.proc_mysql_change_user_packet(change_user) &if(is_change_user);
}; };
refine typeattr ERR_Packet += &let { refine typeattr ERR_Packet += &let {
@ -201,8 +237,8 @@ refine typeattr Resultset += &let {
proc = $context.flow.proc_resultset(this); proc = $context.flow.proc_resultset(this);
}; };
refine typeattr AuthSwitchRequestPayload += &let { refine typeattr AuthSwitchRequest += &let {
proc = $context.flow.proc_auth_switch_request_payload(this); proc = $context.flow.proc_auth_switch_request(this);
}; };
refine typeattr AuthMoreData += &let { refine typeattr AuthMoreData += &let {

View file

@ -154,7 +154,6 @@ enum Expected {
EXPECT_EOF_THEN_RESULTSET, EXPECT_EOF_THEN_RESULTSET,
EXPECT_RESULTSET, EXPECT_RESULTSET,
EXPECT_REST_OF_PACKET, EXPECT_REST_OF_PACKET,
EXPECT_AUTH_SWITCH,
}; };
enum EOFType { enum EOFType {
@ -297,7 +296,7 @@ type MySQL_PDU(is_orig: bool) = record {
hdr : Header; hdr : Header;
msg : case is_orig of { msg : case is_orig of {
false -> server_msg: Server_Message(hdr.seq_id, hdr.len, state); false -> server_msg: Server_Message(hdr.seq_id, hdr.len, state);
true -> client_msg: Client_Message(state); true -> client_msg: Client_Message(hdr.len, state);
} &requires(state); } &requires(state);
} &let { } &let {
state: int = $context.connection.get_state(); state: int = $context.connection.get_state();
@ -377,7 +376,7 @@ type Server_Connection_Phase_Packets = record {
packet: case pkt_type of { packet: case pkt_type of {
0x00 -> data_ok: OK_Packet; 0x00 -> data_ok: OK_Packet;
0x01 -> auth_more_data: AuthMoreData(false); 0x01 -> auth_more_data: AuthMoreData(false);
0xfe -> auth_switch_request: AuthSwitchRequestPayload; 0xfe -> auth_switch_request: AuthSwitchRequest;
0xff -> data_err: ERR_Packet; 0xff -> data_err: ERR_Packet;
}; };
}; };
@ -443,6 +442,7 @@ type Handshake_Response_Packet_v10 = record {
} &let { } &let {
deprecate_eof: bool = $context.connection.set_deprecate_eof(cap_flags & CLIENT_DEPRECATE_EOF); deprecate_eof: bool = $context.connection.set_deprecate_eof(cap_flags & CLIENT_DEPRECATE_EOF);
client_query_attrs: bool = $context.connection.set_client_query_attrs(cap_flags & CLIENT_QUERY_ATTRIBUTES); client_query_attrs: bool = $context.connection.set_client_query_attrs(cap_flags & CLIENT_QUERY_ATTRIBUTES);
proc_cap_flags: bool = $context.connection.set_client_capabilities(cap_flags);
}; };
type Handshake_Response_Packet_v9 = record { type Handshake_Response_Packet_v9 = record {
@ -459,9 +459,9 @@ type Handshake_Response_Packet_v9 = record {
# Connection Phase # Connection Phase
type Client_Message(state: int) = case state of { type Client_Message(pkt_len: uint32, state: int) = case state of {
CONNECTION_PHASE -> connection_phase: Connection_Phase_Packets; CONNECTION_PHASE -> connection_phase: Connection_Phase_Packets;
COMMAND_PHASE -> command_phase : Command_Request_Packet; COMMAND_PHASE -> command_phase : Command_Request_Packet(pkt_len);
}; };
type Connection_Phase_Packets = case $context.connection.get_conn_expectation() of { type Connection_Phase_Packets = case $context.connection.get_conn_expectation() of {
@ -514,17 +514,49 @@ type Query_Attributes = record {
# Command Request # Command Request
type Command_Request_Packet = record { type Command_Request_Packet(pkt_len: uint32) = record {
command: uint8; command: uint8;
attrs : case ( command == COM_QUERY && $context.connection.get_client_query_attrs() && $context.connection.get_server_query_attrs() ) of { attrs : case ( command == COM_QUERY && $context.connection.get_client_query_attrs() && $context.connection.get_server_query_attrs() ) of {
true -> query_attrs: Query_Attributes; true -> query_attrs: Query_Attributes;
false -> none: empty; false -> none: empty;
}; };
have_change_user: case is_change_user of {
true -> change_user: Change_User_Packet(pkt_len);
false -> none_change_user: empty;
};
arg : bytestring &restofdata; arg : bytestring &restofdata;
} &let { } &let {
is_change_user = command == COM_CHANGE_USER;
update_expectation: bool = $context.connection.set_next_expected_from_command(command); update_expectation: bool = $context.connection.set_next_expected_from_command(command);
}; };
# Command from the client to switch the user mid-session.
#
# https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_change_user.html
type Change_User_Packet(pkt_len: uint32) = record {
username : NUL_String;
auth_plugin_data_len: uint8;
auth_plugin_data: bytestring &length=auth_plugin_data_len;
database: NUL_String;
charset: uint16;
auth_plugin_name_case: case have_auth_plugin_name of {
true -> auth_plugin_name: NUL_String;
false -> no_more_data1: empty;
};
conn_attrs_case: case have_conn_attrs of {
true -> conn_attrs: Handshake_Connection_Attributes;
false -> no_conn_attrs: empty;
};
} &let {
have_more_data = offsetof(auth_plugin_name_case) < pkt_len;
have_auth_plugin_name = have_more_data && ($context.connection.get_client_capabilities() & CLIENT_PLUGIN_AUTH) == CLIENT_PLUGIN_AUTH;
have_conn_attrs = have_more_data && ($context.connection.get_client_capabilities() & CLIENT_CONNECT_ATTRS) == CLIENT_CONNECT_ATTRS;
} &exportsourcedata;
# Command Response # Command Response
type Command_Response(pkt_len: uint32) = case $context.connection.get_expectation() of { type Command_Response(pkt_len: uint32) = case $context.connection.get_expectation() of {
@ -534,7 +566,6 @@ type Command_Response(pkt_len: uint32) = case $context.connection.get_expectatio
EXPECT_RESULTSET -> resultset : Resultset(pkt_len); EXPECT_RESULTSET -> resultset : Resultset(pkt_len);
EXPECT_REST_OF_PACKET -> rest : bytestring &restofdata; EXPECT_REST_OF_PACKET -> rest : bytestring &restofdata;
EXPECT_STATUS -> status : Command_Response_Status; EXPECT_STATUS -> status : Command_Response_Status;
EXPECT_AUTH_SWITCH -> auth_switch : AuthSwitchRequest;
EXPECT_EOF_THEN_RESULTSET -> eof : EOFIfLegacyThenResultset(pkt_len); EXPECT_EOF_THEN_RESULTSET -> eof : EOFIfLegacyThenResultset(pkt_len);
default -> unknown : empty; default -> unknown : empty;
}; };
@ -643,11 +674,6 @@ type AuthMoreData(is_orig: bool) = record {
}; };
type AuthSwitchRequest = record { type AuthSwitchRequest = record {
status: uint8 &enforce(status==254);
payload: AuthSwitchRequestPayload;
};
type AuthSwitchRequestPayload = record {
name : NUL_String; name : NUL_String;
data : bytestring &restofdata; data : bytestring &restofdata;
} &let { } &let {
@ -708,6 +734,7 @@ refine connection MySQL_Conn += {
bool deprecate_eof_; bool deprecate_eof_;
bool server_query_attrs_; bool server_query_attrs_;
bool client_query_attrs_; bool client_query_attrs_;
uint32 client_capabilities_;
std::string auth_plugin_; std::string auth_plugin_;
int query_attr_idx_; int query_attr_idx_;
%} %}
@ -797,6 +824,17 @@ refine connection MySQL_Conn += {
return true; return true;
%} %}
function set_client_capabilities(c: uint32): bool
%{
client_capabilities_ = c;
return true;
%}
function get_client_capabilities(): uint32
%{
return client_capabilities_;
%}
function get_expectation(): Expected function get_expectation(): Expected
%{ %{
return expected_; return expected_;
@ -874,8 +912,7 @@ refine connection MySQL_Conn += {
expected_ = EXPECT_STATUS; expected_ = EXPECT_STATUS;
break; break;
case COM_CHANGE_USER: case COM_CHANGE_USER:
// XXX: Could we switch into CONNECTION_PHASE instead? update_state(CONNECTION_PHASE);
expected_ = EXPECT_AUTH_SWITCH;
break; break;
case COM_BINLOG_DUMP: case COM_BINLOG_DUMP:
expected_ = NO_EXPECTATION; expected_ = NO_EXPECTATION;

View file

@ -0,0 +1,13 @@
### 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 127.0.0.1 37446 127.0.0.1 3306 login root T 0 -
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 37446 127.0.0.1 3306 ping (empty) T 0 -
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 37446 127.0.0.1 3306 change_user root2 F - Access denied for user 'root2'@'127.0.0.1' (using password: YES)
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,15 @@
### 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, ~~[R4_KL3\x1fi]M\x0fDRA\x12-\x0a\x00, 21
mysql handshake, root
mysql auth plugin, T, caching_sha2_password, \xda\xf6\xbf\x9e\xa2`\xe4\xba\xd8\xdd[\xdc\x84CE\xe0Ya\xdd\xb0\x9d;\x165Q\x89\xef\xacY\xef\x8dm, 32
mysql auth switch request, mysql_native_password, ~~[R4_KL3\x1fi]M\x0fDRA\x12-\x0a\x00, 21
mysql auth more data, T, W\xbf\xb89Z\x8d\xe4Z\xd4}\xaf\xeb\xd4\x1b\xf3\x0b\xb1OS\xd7, 20
mysql ok, 0
mysql request, 14,
mysql ok, 0
mysql request, 17, root2\x00 \xf5n\xad'\xb7)\xee\x08\xc2&\xac6 a\xe3\xf2\xcd{\xda)\x09\xf1j\xa8\x8a\xcb 7\xf1\xb6\x8cK\x00\xff\x00caching_sha2_password\x00\x96\x04_pid\x071581535\x09_platform\x06x86_64\x0c_source_host\x07tinkyx1\x0c_client_name\x16mysql-connector-python\x0f_client_license\x07GPL-2.0\x0f_client_version\x059.0.0\x03_os\x0cUbuntu-24.04
mysql change user, root2
mysql auth plugin, T, caching_sha2_password, \xf5n\xad'\xb7)\xee\x08\xc2&\xac6 a\xe3\xf2\xcd{\xda)\x09\xf1j\xa8\x8a\xcb 7\xf1\xb6\x8cK, 32
mysql auth switch request, mysql_native_password, ~~[R4_KL3\x1fi]M\x0fDRA\x12-\x0a\x00, 21
mysql auth more data, T, \xcc\xc0\xaf\x97=\xc2lG\xebG\xef=\x93\xd1\xf1\xe6\x98\xb5\x04\x19, 20
mysql error, 1045, Access denied for user 'root2'@'127.0.0.1' (using password: YES)

View file

@ -0,0 +1,17 @@
### 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 127.0.0.1 43330 127.0.0.1 3306 login root T 0 -
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 43330 127.0.0.1 3306 ping (empty) T 0 -
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 43330 127.0.0.1 3306 change_user root2 T 0 -
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 43330 127.0.0.1 3306 query SET NAMES 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci' T 0 -
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 43330 127.0.0.1 3306 query SET @@session.autocommit = OFF T 0 -
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 43330 127.0.0.1 3306 ping (empty) T 0 -
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 43330 127.0.0.1 3306 quit (empty) - - -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,22 @@
### 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, \x11<gm=NJ-\x12\x0fh\x0ddqQb\x1dZ24\x00, 21
mysql handshake, root
mysql auth plugin, T, caching_sha2_password, N#\x9d\x8a`\x0c.K\x81\xf7&\xbbE{\xd2*\x80\xb6b\xae>\x05\x8dA]\x9ay\xeeU\x1a\x8c%, 32
mysql auth switch request, mysql_native_password, \x11<gm=NJ-\x12\x0fh\x0ddqQb\x1dZ24\x00, 21
mysql auth more data, T, \x15\xe9Mj\x8d\x99/\xf8\x1d\xa7\x8am\xa4\xb9\x90\x1e!\xc5 \x05, 20
mysql ok, 0
mysql request, 14,
mysql ok, 0
mysql request, 17, root2\x00 N#\x9d\x8a`\x0c.K\x81\xf7&\xbbE{\xd2*\x80\xb6b\xae>\x05\x8dA]\x9ay\xeeU\x1a\x8c%\x00\xff\x00caching_sha2_password\x00\x96\x04_pid\x071581443\x09_platform\x06x86_64\x0c_source_host\x07tinkyx1\x0c_client_name\x16mysql-connector-python\x0f_client_license\x07GPL-2.0\x0f_client_version\x059.0.0\x03_os\x0cUbuntu-24.04
mysql change user, root2
mysql auth plugin, T, caching_sha2_password, N#\x9d\x8a`\x0c.K\x81\xf7&\xbbE{\xd2*\x80\xb6b\xae>\x05\x8dA]\x9ay\xeeU\x1a\x8c%, 32
mysql auth switch request, mysql_native_password, \x11<gm=NJ-\x12\x0fh\x0ddqQb\x1dZ24\x00, 21
mysql auth more data, T, \x15\xe9Mj\x8d\x99/\xf8\x1d\xa7\x8am\xa4\xb9\x90\x1e!\xc5 \x05, 20
mysql ok, 0
mysql request, 3, SET NAMES 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci'
mysql ok, 0
mysql request, 3, SET @@session.autocommit = OFF
mysql ok, 0
mysql request, 14,
mysql ok, 0
mysql request, 1,

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,50 @@
# @TEST-EXEC: zeek -b -C -r $TRACES/mysql/change-user-error.pcap %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_change_user(c: connection, username: string)
{
print "mysql change user", username;
}
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|;
}

View file

@ -0,0 +1,50 @@
# @TEST-EXEC: zeek -b -C -r $TRACES/mysql/change-user-success.pcap %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_change_user(c: connection, username: string)
{
print "mysql change user", username;
}
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|;
}