From a9f2610f956aad21d5282721a0d85d16adaa6ef4 Mon Sep 17 00:00:00 2001 From: nielsdos <7771979+nielsdos@users.noreply.github.com> Date: Mon, 12 Jun 2023 23:47:42 +0200 Subject: [PATCH 1/3] Fix GH-11438: mysqlnd fails to authenticate with sha256_password accounts using passwords longer than 19 characters https://dev.mysql.com/doc/dev/mysql-server/latest/page_caching_sha2_authentication_exchanges.html tells us that the nonce used in this authentication method is 20 bytes long. However, we might receive additional scramble data in php_mysqlnd_greet_read not used in this method. On my test setup, I received 21 bytes (20 bytes + '\0'). This resulted in the xor computation to incorrectly include the NUL byte. Every password of at least 20 characters therefore failed to authenticate using this method. Looking at mysql-server source code also seems to reveal that it always uses a fixed number of scramble bytes [1]. [1] https://github.com/mysql/mysql-server/blob/ea7087d885006918ad54458e7aad215b1650312c/sql/auth/sha2_password.cc#L1078-L1079 --- ext/mysqlnd/mysqlnd_auth.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ext/mysqlnd/mysqlnd_auth.c b/ext/mysqlnd/mysqlnd_auth.c index 421f4d082a3a9..8112433c35416 100644 --- a/ext/mysqlnd/mysqlnd_auth.c +++ b/ext/mysqlnd/mysqlnd_auth.c @@ -924,7 +924,10 @@ mysqlnd_sha256_auth_get_auth_data(struct st_mysqlnd_authentication_plugin * self char *xor_str = do_alloca(passwd_len + 1, use_heap); memcpy(xor_str, passwd, passwd_len); xor_str[passwd_len] = '\0'; - mysqlnd_xor_string(xor_str, passwd_len, (char *) auth_plugin_data, auth_plugin_data_len); + /* https://dev.mysql.com/doc/dev/mysql-server/latest/page_caching_sha2_authentication_exchanges.html + * This tells us that the nonce is 20 (==SCRAMBLE_LENGTH) bytes long. + * In a 5.5+ server we might get additional scramble data in php_mysqlnd_greet_read, not used by this authentication method. */ + mysqlnd_xor_string(xor_str, passwd_len, (char *) auth_plugin_data, SCRAMBLE_LENGTH); ret = mysqlnd_sha256_public_encrypt(conn, server_public_key, passwd_len, auth_data_len, xor_str); free_alloca(xor_str, use_heap); } From ddeee43459e492bacdbccb9fcb2025c57a87c0f0 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Wed, 2 Aug 2023 21:01:37 +0200 Subject: [PATCH 2/3] Add regression test --- ext/mysqli/tests/gh11438.phpt | 129 ++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 ext/mysqli/tests/gh11438.phpt diff --git a/ext/mysqli/tests/gh11438.phpt b/ext/mysqli/tests/gh11438.phpt new file mode 100644 index 0000000000000..468978cc55b58 --- /dev/null +++ b/ext/mysqli/tests/gh11438.phpt @@ -0,0 +1,129 @@ +--TEST-- +GH-11438 (mysqlnd fails to authenticate with sha256_password accounts using passwords longer than 19 characters) +--EXTENSIONS-- +mysqli +--SKIPIF-- +query("SHOW PLUGINS"))) { + die(sprintf("skip [%d] %s\n", $link->errno, $link->error)); +} + +$found = false; +while ($row = $res->fetch_assoc()) { + if (($row['Name'] == 'sha256_password') && ($row['Status'] == 'ACTIVE')) { + $found = true; + break; + } +} +if (!$found) + die("skip SHA-256 server plugin unavailable"); + +if (!($res = $link->query("SHOW STATUS LIKE 'Rsa_public_key'"))) { + die(sprintf("skip [%d] %s\n", $link->errno, $link->error)); +} + +if (!($row = $res->fetch_assoc())) { + die(sprintf("skip Failed to check RSA pub key, [%d] %s\n", $link->errno, $link->error)); +} + +$key = $row['Value']; +if (strlen($key) < 100) { + die(sprintf("skip Server misconfiguration? RSA pub key is suspicious, [%d] %s\n", $link->errno, $link->error)); +} + +/* date changes may give false positive */ +$file = "test_sha256_ini"; +if ((file_exists($file) && !unlink($file)) || !($fp = @fopen($file, "w"))) { + die(sprintf("skip Cannot create RSA pub key file '%s'", $file)); +} +$key = str_replace("A", "a", $key); +$key = str_replace("M", "m", $key); +if (strlen($key) != fwrite($fp, $key)) { + die(sprintf("skip Failed to create pub key file")); +} + +// Ignore errors because this variable exists only in MySQL 5.6 and 5.7 +$link->query("SET @@session.old_passwords=2"); + +$link->query('DROP USER shatest'); +$link->query("DROP USER shatest@localhost"); + + +if (!$link->query('CREATE USER shatest@"%" IDENTIFIED WITH sha256_password') || + !$link->query('CREATE USER shatest@"localhost" IDENTIFIED WITH sha256_password')) { + die(sprintf("skip CREATE USER failed [%d] %s", $link->errno, $link->error)); +} + +// Password of length 52, more than twice the length of the scramble data to ensure scramble is repeated correctly +if (!$link->query('SET PASSWORD FOR shatest@"%" = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"') || + !$link->query('SET PASSWORD FOR shatest@"localhost" = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"')) { + die(sprintf("skip SET PASSWORD failed [%d] %s", $link->errno, $link->error)); +} + +if (!$link->query("DROP TABLE IF EXISTS test") || + !$link->query("CREATE TABLE test (id INT)") || + !$link->query("INSERT INTO test(id) VALUES (1), (2), (3)")) + die(sprintf("SKIP [%d] %s\n", $link->errno, $link->error)); + + +if (!$link->query(sprintf("GRANT SELECT ON TABLE %s.test TO shatest@'%%'", $db)) || + !$link->query(sprintf("GRANT SELECT ON TABLE %s.test TO shatest@'localhost'", $db))) { + die(sprintf("skip Cannot grant SELECT to user [%d] %s", mysqli_errno($link), mysqli_error($link))); +} + +$link->close(); +echo "nocache"; +?> +--INI-- +mysqlnd.sha256_server_public_key="test_sha256_ini" +--FILE-- +connect_errno) { + printf("[001] [%d] %s\n", $link->connect_errno, $link->connect_error); + } else { + if (!$res = $link->query("SELECT id FROM test WHERE id = 1")) + printf("[002] [%d] %s\n", $link->errno, $link->error); + + if (!$row = mysqli_fetch_assoc($res)) { + printf("[003] [%d] %s\n", $link->errno, $link->error); + } + + if ($row['id'] != 1) { + printf("[004] Expecting 1 got %s/'%s'", gettype($row['id']), $row['id']); + } + } + print "done!"; +?> +--CLEAN-- +query('DROP USER shatest'); + $link->query('DROP USER shatest@localhost'); + $file = "test_sha256_ini"; + @unlink($file); +?> +--EXPECTF-- +Warning: mysqli::__construct(): (HY000/1045): %s in %s on line %d +[001] [1045] %s +done! From 3940402ff19a285b8158b5e63659e6957b6498be Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Wed, 2 Aug 2023 22:33:05 +0100 Subject: [PATCH 3/3] Fix the new regression test --- ext/mysqli/tests/gh11438.phpt | 85 +++++++++-------------------------- 1 file changed, 20 insertions(+), 65 deletions(-) diff --git a/ext/mysqli/tests/gh11438.phpt b/ext/mysqli/tests/gh11438.phpt index 468978cc55b58..49c715e72c03c 100644 --- a/ext/mysqli/tests/gh11438.phpt +++ b/ext/mysqli/tests/gh11438.phpt @@ -4,8 +4,7 @@ GH-11438 (mysqlnd fails to authenticate with sha256_password accounts using pass mysqli --SKIPIF-- fetch_assoc()) { if (!$found) die("skip SHA-256 server plugin unavailable"); -if (!($res = $link->query("SHOW STATUS LIKE 'Rsa_public_key'"))) { - die(sprintf("skip [%d] %s\n", $link->errno, $link->error)); -} - -if (!($row = $res->fetch_assoc())) { - die(sprintf("skip Failed to check RSA pub key, [%d] %s\n", $link->errno, $link->error)); -} - -$key = $row['Value']; -if (strlen($key) < 100) { - die(sprintf("skip Server misconfiguration? RSA pub key is suspicious, [%d] %s\n", $link->errno, $link->error)); -} - -/* date changes may give false positive */ -$file = "test_sha256_ini"; -if ((file_exists($file) && !unlink($file)) || !($fp = @fopen($file, "w"))) { - die(sprintf("skip Cannot create RSA pub key file '%s'", $file)); -} -$key = str_replace("A", "a", $key); -$key = str_replace("M", "m", $key); -if (strlen($key) != fwrite($fp, $key)) { - die(sprintf("skip Failed to create pub key file")); -} - // Ignore errors because this variable exists only in MySQL 5.6 and 5.7 $link->query("SET @@session.old_passwords=2"); $link->query('DROP USER shatest'); $link->query("DROP USER shatest@localhost"); - if (!$link->query('CREATE USER shatest@"%" IDENTIFIED WITH sha256_password') || !$link->query('CREATE USER shatest@"localhost" IDENTIFIED WITH sha256_password')) { die(sprintf("skip CREATE USER failed [%d] %s", $link->errno, $link->error)); @@ -77,53 +50,35 @@ if (!$link->query('SET PASSWORD FOR shatest@"%" = "abcdefghijklmnopqrstuvwxyzABC die(sprintf("skip SET PASSWORD failed [%d] %s", $link->errno, $link->error)); } -if (!$link->query("DROP TABLE IF EXISTS test") || - !$link->query("CREATE TABLE test (id INT)") || - !$link->query("INSERT INTO test(id) VALUES (1), (2), (3)")) - die(sprintf("SKIP [%d] %s\n", $link->errno, $link->error)); - - -if (!$link->query(sprintf("GRANT SELECT ON TABLE %s.test TO shatest@'%%'", $db)) || - !$link->query(sprintf("GRANT SELECT ON TABLE %s.test TO shatest@'localhost'", $db))) { - die(sprintf("skip Cannot grant SELECT to user [%d] %s", mysqli_errno($link), mysqli_error($link))); -} - -$link->close(); echo "nocache"; ?> ---INI-- -mysqlnd.sha256_server_public_key="test_sha256_ini" --FILE-- connect_errno) { + printf("[001] [%d] %s\n", $link->connect_errno, $link->connect_error); +} else { + if (!$res = $link->query("SELECT USER()")) + printf("[002] [%d] %s\n", $link->errno, $link->error); - $link = new mysqli($host, 'shatest', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', $db, $port, $socket); - if ($link->connect_errno) { - printf("[001] [%d] %s\n", $link->connect_errno, $link->connect_error); - } else { - if (!$res = $link->query("SELECT id FROM test WHERE id = 1")) - printf("[002] [%d] %s\n", $link->errno, $link->error); - - if (!$row = mysqli_fetch_assoc($res)) { - printf("[003] [%d] %s\n", $link->errno, $link->error); - } + if (!$row = mysqli_fetch_assoc($res)) { + printf("[003] [%d] %s\n", $link->errno, $link->error); + } - if ($row['id'] != 1) { - printf("[004] Expecting 1 got %s/'%s'", gettype($row['id']), $row['id']); - } + if (!is_string($row['USER()']) || !str_starts_with($row['USER()'], 'shatest')) { + printf("[004] Expecting 1 got %s/'%s'", gettype($row['USER()']), $row['USER()']); } - print "done!"; +} + +print "done!"; ?> --CLEAN-- query('DROP USER shatest'); - $link->query('DROP USER shatest@localhost'); - $file = "test_sha256_ini"; - @unlink($file); +require_once 'connect.inc'; +$link->query('DROP USER shatest'); +$link->query('DROP USER shatest@localhost'); ?> --EXPECTF-- -Warning: mysqli::__construct(): (HY000/1045): %s in %s on line %d -[001] [1045] %s done!