From b1c63ae4e0736bd2ababcfaa89e99fcf1238c21b Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Tue, 13 Aug 2024 16:09:04 +0200 Subject: [PATCH] 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 --- .../protocol/mysql/mysql-analyzer.pac | 6 +-- .../protocol/mysql/mysql-protocol.pac | 43 ++++++++++++----- .../mysql.log | 11 +++++ .../out | 9 ++++ .../mysql/mysql8-navicat-login-failed.pcapng | Bin 0 -> 3227 bytes ...d-auth-switch-to-test-sha256_password.test | 45 ++++++++++++++++++ 6 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 testing/btest/Baseline/scripts.base.protocols.mysql.caching_sha2_password-auth-switch-to-test-sha256_password/mysql.log create mode 100644 testing/btest/Baseline/scripts.base.protocols.mysql.caching_sha2_password-auth-switch-to-test-sha256_password/out create mode 100644 testing/btest/Traces/mysql/mysql8-navicat-login-failed.pcapng create mode 100644 testing/btest/scripts/base/protocols/mysql/caching_sha2_password-auth-switch-to-test-sha256_password.test diff --git a/src/analyzer/protocol/mysql/mysql-analyzer.pac b/src/analyzer/protocol/mysql/mysql-analyzer.pac index afb75a44f3..eacacf773f 100644 --- a/src/analyzer/protocol/mysql/mysql-analyzer.pac +++ b/src/analyzer/protocol/mysql/mysql-analyzer.pac @@ -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 { diff --git a/src/analyzer/protocol/mysql/mysql-protocol.pac b/src/analyzer/protocol/mysql/mysql-protocol.pac index aeee595887..8366519984 100644 --- a/src/analyzer/protocol/mysql/mysql-protocol.pac +++ b/src/analyzer/protocol/mysql/mysql-protocol.pac @@ -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: diff --git a/testing/btest/Baseline/scripts.base.protocols.mysql.caching_sha2_password-auth-switch-to-test-sha256_password/mysql.log b/testing/btest/Baseline/scripts.base.protocols.mysql.caching_sha2_password-auth-switch-to-test-sha256_password/mysql.log new file mode 100644 index 0000000000..80d42c6d83 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.mysql.caching_sha2_password-auth-switch-to-test-sha256_password/mysql.log @@ -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 diff --git a/testing/btest/Baseline/scripts.base.protocols.mysql.caching_sha2_password-auth-switch-to-test-sha256_password/out b/testing/btest/Baseline/scripts.base.protocols.mysql.caching_sha2_password-auth-switch-to-test-sha256_password/out new file mode 100644 index 0000000000..bfab59e886 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.mysql.caching_sha2_password-auth-switch-to-test-sha256_password/out @@ -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) diff --git a/testing/btest/Traces/mysql/mysql8-navicat-login-failed.pcapng b/testing/btest/Traces/mysql/mysql8-navicat-login-failed.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..e8fa457c42ca20f3c237dd626b7ad5e28ec45fe8 GIT binary patch literal 3227 zcmeHJX>1cm7@c+E086+*;l@BA2?oc<+KC}Ztm6aQiH{`CRj66t8=vvo-Xx_ZqR>Jb zE>WOGP>u?UssJf1M?r#8pr{B`prD9CL?J;f2vj5pRDeilY*LC7sekgbBYpOGR(|`w zH*daguADlsg9B1Q_vq>Z0E0go?i_UC3;F{EoTHez>Fa&#?q9jvxS?WCf1m<@AU~N0 z8q(St@loGM@lhky?M(W;rR20Z2&sYd>>=0=04J6EEtZnfAHz~P2ysN8BX*kce>f)C zC732S0x%EEO|wlx{1DIdd+p3(%~dbtncpz{*~5J|UU7u{AkTh?ljXl|! z1Aq?x6VDR}1rjM31m{yya;K)u%HV*muF3G@<|w%KYAD2cgNZqHOE7c)0l-e#J#@fr zWjvJF8lo764Tc?YSN!}A4n{zWph(gOU|e!zT%W_e=yTLGngdoj0k{S&wtRP21AOni z$=z>H`gSmW5#Vk0Ogh%YUwb@FFuJ;Z?d}al>1R^ykDqlt)2UZv+>G*jXSq1V8f5yF z(*Z}2Wzwy7AMFf8tO3gJ#C^2QPlaj9VN11!Xa`r6N63hN)`%zUq#RsAD3lQctRWv2 zaRtMEE?K0M5ktBGV$Lu_2LoJ*KrE1?TN!59X8zeN8f z(SYjLr>uvwv%D+g&fbya?4wJ`>0?YovCiL@Bu*JZSp=Pfq6bQ`w=s?q>lI}*)_LI~ z0MlUsc!^9dmA$rk)@%_L(dV*dvlOHt!{f?U4$O*sf6s0lpaO>?+52D?xe0l{ZS|Yp zpFRfGh7RQYJG0}pH3dT++1GbaJ6mw@5}rmB$tqwEy&oeL9=9f`&;hla2&+WrjywQP zV;~iqg@!_<(HeLri=t3lz|*TLe?Pa)y(v`9n4F$swpQ&nXy_UvK@>$wtND7hUr1Ifs+^1xFEP<9l_Tbpl)oZYUv87> z>kEtPv;}tB;39?YqJj!tsMa)}sgZpY_dmNNXH+A;Ic(%6iJFben66Yvo3%x|B6b3pE9Gjyg+Gg*xKHg%VT1 zr_u2(RW+qyrY3k8e(MlV zbfq=(%au^9YuwGT zjY|)uYy9~q*!NFw&KPj+dmXmwcv`S6cuRbyuwmeD)_`|>bM^(!*;5UatG)HolIvi5 z^zz=p86(f?)3`0-ANapEkC>ZXshhL#_M_JA5@OAT6C;Ypx2?Xte%p||x1Mk1+!fpp z9pJF!YHvg&;MdSW2&J0qna23jI+_Waa zIrsj37hC>9N>2D+$v#rjdkF{2p|MaTS17=Go8ooa_-2J_ZC*-dPC*q)(hh(gfV8er zsZ+&yLZOhf+no%b}_xy(p#DeDuI7agy;^i=oN#hjm{?hB~y LRMg-*Bmn#k7RqB$ literal 0 HcmV?d00001 diff --git a/testing/btest/scripts/base/protocols/mysql/caching_sha2_password-auth-switch-to-test-sha256_password.test b/testing/btest/scripts/base/protocols/mysql/caching_sha2_password-auth-switch-to-test-sha256_password.test new file mode 100644 index 0000000000..a3eeb86e3b --- /dev/null +++ b/testing/btest/scripts/base/protocols/mysql/caching_sha2_password-auth-switch-to-test-sha256_password.test @@ -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|; + }