Commit 09ec55b9 authored by Michael Paquier's avatar Michael Paquier

Fix buffer overflow when parsing SCRAM verifiers in backend

Any authenticated user can overflow a stack-based buffer by changing the
user's own password to a purpose-crafted value.  This often suffices to
execute arbitrary code as the PostgreSQL operating system account.

This fix is contributed by multiple folks, based on an initial analysis
from Tom Lane.  This issue has been introduced by 68e61ee7, so it was
possible to make use of it at authentication time.  It became more
easily to trigger after ccae190b which has made the SCRAM parsing more
strict when changing a password, in the case where the client passes
down a verifier already hashed using SCRAM.  Back-patch to v10 where
SCRAM has been introduced.

Reported-by: Alexander Lakhin
Author: Jonathan Katz, Heikki Linnakangas, Michael Paquier
Security: CVE-2019-10164
Backpatch-through: 10
parent 34120302
...@@ -542,6 +542,12 @@ scram_verify_plain_password(const char *username, const char *password, ...@@ -542,6 +542,12 @@ scram_verify_plain_password(const char *username, const char *password,
/* /*
* Parse and validate format of given SCRAM verifier. * Parse and validate format of given SCRAM verifier.
* *
* On success, the iteration count, salt, stored key, and server key are
* extracted from the verifier, and returned to the caller. For 'stored_key'
* and 'server_key', the caller must pass pre-allocated buffers of size
* SCRAM_KEY_LEN. Salt is returned as a base64-encoded, null-terminated
* string. The buffer for the salt is palloc'd by this function.
*
* Returns true if the SCRAM verifier has been parsed, and false otherwise. * Returns true if the SCRAM verifier has been parsed, and false otherwise.
*/ */
bool bool
...@@ -557,6 +563,8 @@ parse_scram_verifier(const char *verifier, int *iterations, char **salt, ...@@ -557,6 +563,8 @@ parse_scram_verifier(const char *verifier, int *iterations, char **salt,
char *serverkey_str; char *serverkey_str;
int decoded_len; int decoded_len;
char *decoded_salt_buf; char *decoded_salt_buf;
char *decoded_stored_buf;
char *decoded_server_buf;
/* /*
* The verifier is of form: * The verifier is of form:
...@@ -589,7 +597,8 @@ parse_scram_verifier(const char *verifier, int *iterations, char **salt, ...@@ -589,7 +597,8 @@ parse_scram_verifier(const char *verifier, int *iterations, char **salt,
* although we return the encoded version to the caller. * although we return the encoded version to the caller.
*/ */
decoded_salt_buf = palloc(pg_b64_dec_len(strlen(salt_str))); decoded_salt_buf = palloc(pg_b64_dec_len(strlen(salt_str)));
decoded_len = pg_b64_decode(salt_str, strlen(salt_str), decoded_salt_buf); decoded_len = pg_b64_decode(salt_str, strlen(salt_str),
decoded_salt_buf);
if (decoded_len < 0) if (decoded_len < 0)
goto invalid_verifier; goto invalid_verifier;
*salt = pstrdup(salt_str); *salt = pstrdup(salt_str);
...@@ -597,28 +606,38 @@ parse_scram_verifier(const char *verifier, int *iterations, char **salt, ...@@ -597,28 +606,38 @@ parse_scram_verifier(const char *verifier, int *iterations, char **salt,
/* /*
* Decode StoredKey and ServerKey. * Decode StoredKey and ServerKey.
*/ */
if (pg_b64_dec_len(strlen(storedkey_str) != SCRAM_KEY_LEN)) decoded_stored_buf = palloc(pg_b64_dec_len(strlen(storedkey_str)));
goto invalid_verifier;
decoded_len = pg_b64_decode(storedkey_str, strlen(storedkey_str), decoded_len = pg_b64_decode(storedkey_str, strlen(storedkey_str),
(char *) stored_key); decoded_stored_buf);
if (decoded_len != SCRAM_KEY_LEN) if (decoded_len != SCRAM_KEY_LEN)
goto invalid_verifier; goto invalid_verifier;
memcpy(stored_key, decoded_stored_buf, SCRAM_KEY_LEN);
if (pg_b64_dec_len(strlen(serverkey_str) != SCRAM_KEY_LEN)) decoded_server_buf = palloc(pg_b64_dec_len(strlen(serverkey_str)));
goto invalid_verifier;
decoded_len = pg_b64_decode(serverkey_str, strlen(serverkey_str), decoded_len = pg_b64_decode(serverkey_str, strlen(serverkey_str),
(char *) server_key); decoded_server_buf);
if (decoded_len != SCRAM_KEY_LEN) if (decoded_len != SCRAM_KEY_LEN)
goto invalid_verifier; goto invalid_verifier;
memcpy(server_key, decoded_server_buf, SCRAM_KEY_LEN);
return true; return true;
invalid_verifier: invalid_verifier:
pfree(v);
*salt = NULL; *salt = NULL;
return false; return false;
} }
/*
* Generate plausible SCRAM verifier parameters for mock authentication.
*
* In a normal authentication, these are extracted from the verifier
* stored in the server. This function generates values that look
* realistic, for when there is no stored verifier.
*
* Like in parse_scram_verifier(), for 'stored_key' and 'server_key', the
* caller must pass pre-allocated buffers of size SCRAM_KEY_LEN, and
* the buffer for the salt is palloc'd by this function.
*/
static void static void
mock_scram_verifier(const char *username, int *iterations, char **salt, mock_scram_verifier(const char *username, int *iterations, char **salt,
uint8 *stored_key, uint8 *server_key) uint8 *stored_key, uint8 *server_key)
......
...@@ -100,6 +100,26 @@ SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty'; ...@@ -100,6 +100,26 @@ SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
(1 row) (1 row)
-- Test with invalid stored and server keys.
--
-- The first is valid, to act as a control. The others have too long
-- stored/server keys. They will be re-hashed.
CREATE ROLE regress_passwd_sha_len0 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZI=';
CREATE ROLE regress_passwd_sha_len1 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96RqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZI=';
CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
-- Check that the invalid verifiers were re-hashed. A re-hashed verifier
-- should not contain the original salt.
SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
FROM pg_authid
WHERE rolname LIKE 'regress_passwd_sha_len%'
ORDER BY rolname;
rolname | is_rolpassword_rehashed
-------------------------+-------------------------
regress_passwd_sha_len0 | f
regress_passwd_sha_len1 | t
regress_passwd_sha_len2 | t
(3 rows)
DROP ROLE regress_passwd1; DROP ROLE regress_passwd1;
DROP ROLE regress_passwd2; DROP ROLE regress_passwd2;
DROP ROLE regress_passwd3; DROP ROLE regress_passwd3;
...@@ -109,6 +129,9 @@ DROP ROLE regress_passwd6; ...@@ -109,6 +129,9 @@ DROP ROLE regress_passwd6;
DROP ROLE regress_passwd7; DROP ROLE regress_passwd7;
DROP ROLE regress_passwd8; DROP ROLE regress_passwd8;
DROP ROLE regress_passwd_empty; DROP ROLE regress_passwd_empty;
DROP ROLE regress_passwd_sha_len0;
DROP ROLE regress_passwd_sha_len1;
DROP ROLE regress_passwd_sha_len2;
-- all entries should have been removed -- all entries should have been removed
SELECT rolname, rolpassword SELECT rolname, rolpassword
FROM pg_authid FROM pg_authid
......
...@@ -75,6 +75,21 @@ ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a'; ...@@ -75,6 +75,21 @@ ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4='; ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty'; SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
-- Test with invalid stored and server keys.
--
-- The first is valid, to act as a control. The others have too long
-- stored/server keys. They will be re-hashed.
CREATE ROLE regress_passwd_sha_len0 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZI=';
CREATE ROLE regress_passwd_sha_len1 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96RqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZI=';
CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
-- Check that the invalid verifiers were re-hashed. A re-hashed verifier
-- should not contain the original salt.
SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
FROM pg_authid
WHERE rolname LIKE 'regress_passwd_sha_len%'
ORDER BY rolname;
DROP ROLE regress_passwd1; DROP ROLE regress_passwd1;
DROP ROLE regress_passwd2; DROP ROLE regress_passwd2;
DROP ROLE regress_passwd3; DROP ROLE regress_passwd3;
...@@ -84,6 +99,9 @@ DROP ROLE regress_passwd6; ...@@ -84,6 +99,9 @@ DROP ROLE regress_passwd6;
DROP ROLE regress_passwd7; DROP ROLE regress_passwd7;
DROP ROLE regress_passwd8; DROP ROLE regress_passwd8;
DROP ROLE regress_passwd_empty; DROP ROLE regress_passwd_empty;
DROP ROLE regress_passwd_sha_len0;
DROP ROLE regress_passwd_sha_len1;
DROP ROLE regress_passwd_sha_len2;
-- all entries should have been removed -- all entries should have been removed
SELECT rolname, rolpassword SELECT rolname, rolpassword
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment