Blob Blame History Raw
From 9286875a73b2de7736b5e50692739d314cd8d9dc Mon Sep 17 00:00:00 2001
From: Darren Tucker <dtucker@zip.com.au>
Date: Fri, 15 Jul 2016 13:32:45 +1000
Subject: [PATCH] Determine appropriate salt for invalid users.

When sshd is processing a non-PAM login for a non-existent user it uses
the string from the fakepw structure as the salt for crypt(3)ing the
password supplied by the client.  That string has a Blowfish prefix, so on
systems that don't understand that crypt will fail fast due to an invalid
salt, and even on those that do it may have significantly different timing
from the hash methods used for real accounts (eg sha512).  This allows
user enumeration by, eg, sending large password strings.  This was noted
by EddieEzra.Harari at verint.com (CVE-2016-6210).

To mitigate, use the same hash algorithm that root uses for hashing
passwords for users that do not exist on the system.  ok djm@
---
 auth-passwd.c           | 12 ++++++++----
 openbsd-compat/xcrypt.c | 34 ++++++++++++++++++++++++++++++++++
 2 files changed, 42 insertions(+), 4 deletions(-)

diff --git a/auth-passwd.c b/auth-passwd.c
index 63ccf3c..530b5d4 100644
--- a/auth-passwd.c
+++ b/auth-passwd.c
@@ -193,7 +193,7 @@ int
 sys_auth_passwd(Authctxt *authctxt, const char *password)
 {
 	struct passwd *pw = authctxt->pw;
-	char *encrypted_password;
+	char *encrypted_password, *salt = NULL;
 
 	/* Just use the supplied fake password if authctxt is invalid */
 	char *pw_password = authctxt->valid ? shadow_pw(pw) : pw->pw_passwd;
@@ -202,9 +202,13 @@ sys_auth_passwd(Authctxt *authctxt, const char *password)
 	if (strcmp(pw_password, "") == 0 && strcmp(password, "") == 0)
 		return (1);
 
-	/* Encrypt the candidate password using the proper salt. */
-	encrypted_password = xcrypt(password,
-	    (pw_password[0] && pw_password[1]) ? pw_password : "xx");
+	/*
+	 * Encrypt the candidate password using the proper salt, or pass a
+	 * NULL and let xcrypt pick one.
+	 */
+	if (authctxt->valid && pw_password[0] && pw_password[1])
+		salt = pw_password;
+	encrypted_password = xcrypt(password, salt);
 
 	/*
 	 * Authentication is accepted if the encrypted passwords
diff --git a/openbsd-compat/xcrypt.c b/openbsd-compat/xcrypt.c
index 8577cbd..8913bb8 100644
--- a/openbsd-compat/xcrypt.c
+++ b/openbsd-compat/xcrypt.c
@@ -25,6 +25,7 @@
 #include "includes.h"
 
 #include <sys/types.h>
+#include <string.h>
 #include <unistd.h>
 #include <pwd.h>
 
@@ -62,11 +63,44 @@
 #  define crypt DES_crypt
 # endif
 
+/*
+ * Pick an appropriate password encryption type and salt for the running
+ * system.
+ */
+static const char *
+pick_salt(void)
+{
+	struct passwd *pw;
+	char *passwd, *p;
+	size_t typelen;
+	static char salt[32];
+
+	if (salt[0] != '\0')
+		return salt;
+	strlcpy(salt, "xx", sizeof(salt));
+	if ((pw = getpwuid(0)) == NULL)
+		return salt;
+	passwd = shadow_pw(pw);
+	if (passwd[0] != '$' || (p = strrchr(passwd + 1, '$')) == NULL)
+		return salt;  /* no $, DES */
+	typelen = p - passwd + 1;
+	strlcpy(salt, passwd, MIN(typelen, sizeof(salt)));
+	explicit_bzero(passwd, strlen(passwd));
+	return salt;
+}
+
 char *
 xcrypt(const char *password, const char *salt)
 {
 	char *crypted;
 
+	/*
+	 * If we don't have a salt we are encrypting a fake password for
+	 * for timing purposes.  Pick an appropriate salt.
+	 */
+	if (salt == NULL)
+		salt = pick_salt();
+
 # ifdef HAVE_MD5_PASSWORDS
         if (is_md5_salt(salt))
                 crypted = md5_crypt(password, salt);

From 283b97ff33ea2c641161950849931bd578de6946 Mon Sep 17 00:00:00 2001
From: Darren Tucker <dtucker@zip.com.au>
Date: Fri, 15 Jul 2016 13:49:44 +1000
Subject: [PATCH] Mitigate timing of disallowed users PAM logins.

When sshd decides to not allow a login (eg PermitRootLogin=no) and
it's using PAM, it sends a fake password to PAM so that the timing for
the failure is not noticeably different whether or not the password
is correct.  This behaviour can be detected by sending a very long
password string which is slower to hash than the fake password.

Mitigate by constructing an invalid password that is the same length
as the one from the client and thus takes the same time to hash.
Diff from djm@
---
 auth-pam.c | 35 +++++++++++++++++++++++++++++++----
 1 file changed, 31 insertions(+), 4 deletions(-)

diff --git a/auth-pam.c b/auth-pam.c
index 451de78..465b5a7 100644
--- a/auth-pam.c
+++ b/auth-pam.c
@@ -232,7 +232,6 @@ static int sshpam_account_status = -1;
 static char **sshpam_env = NULL;
 static Authctxt *sshpam_authctxt = NULL;
 static const char *sshpam_password = NULL;
-static char badpw[] = "\b\n\r\177INCORRECT";
 
 /* Some PAM implementations don't implement this */
 #ifndef HAVE_PAM_GETENVLIST
@@ -795,12 +794,35 @@ sshpam_query(void *ctx, char **name, char **info,
 	return (-1);
 }
 
+/*
+ * Returns a junk password of identical length to that the user supplied.
+ * Used to mitigate timing attacks against crypt(3)/PAM stacks that
+ * vary processing time in proportion to password length.
+ */
+static char *
+fake_password(const char *wire_password)
+{
+	const char junk[] = "\b\n\r\177INCORRECT";
+	char *ret = NULL;
+	size_t i, l = wire_password != NULL ? strlen(wire_password) : 0;
+
+	if (l >= INT_MAX)
+		fatal("%s: password length too long: %zu", __func__, l);
+
+	ret = malloc(l + 1);
+	for (i = 0; i < l; i++)
+		ret[i] = junk[i % (sizeof(junk) - 1)];
+	ret[i] = '\0';
+	return ret;
+}
+
 /* XXX - see also comment in auth-chall.c:verify_response */
 static int
 sshpam_respond(void *ctx, u_int num, char **resp)
 {
 	Buffer buffer;
 	struct pam_ctxt *ctxt = ctx;
+	char *fake;
 
 	debug2("PAM: %s entering, %u responses", __func__, num);
 	switch (ctxt->pam_done) {
@@ -821,8 +843,11 @@ sshpam_respond(void *ctx, u_int num, char **resp)
 	    (sshpam_authctxt->pw->pw_uid != 0 ||
 	    options.permit_root_login == PERMIT_YES))
 		buffer_put_cstring(&buffer, *resp);
-	else
-		buffer_put_cstring(&buffer, badpw);
+	else {
+		fake = fake_password(*resp);
+		buffer_put_cstring(&buffer, fake);
+		free(fake);
+	}
 	if (ssh_msg_send(ctxt->pam_psock, PAM_AUTHTOK, &buffer) == -1) {
 		buffer_free(&buffer);
 		return (-1);
@@ -1166,6 +1191,7 @@ sshpam_auth_passwd(Authctxt *authctxt, const char *password)
 {
 	int flags = (options.permit_empty_passwd == 0 ?
 	    PAM_DISALLOW_NULL_AUTHTOK : 0);
+	char *fake = NULL;
 
 	if (!options.use_pam || sshpam_handle == NULL)
 		fatal("PAM: %s called when PAM disabled or failed to "
@@ -1181,7 +1207,7 @@ sshpam_auth_passwd(Authctxt *authctxt, const char *password)
 	 */
 	if (!authctxt->valid || (authctxt->pw->pw_uid == 0 &&
 	    options.permit_root_login != PERMIT_YES))
-		sshpam_password = badpw;
+		sshpam_password = fake = fake_password(password);
 
 	sshpam_err = pam_set_item(sshpam_handle, PAM_CONV,
 	    (const void *)&passwd_conv);
@@ -1191,6 +1217,7 @@ sshpam_auth_passwd(Authctxt *authctxt, const char *password)
 
 	sshpam_err = pam_authenticate(sshpam_handle, flags);
 	sshpam_password = NULL;
+	free(fake);
 	if (sshpam_err == PAM_SUCCESS && authctxt->valid) {
 		debug("PAM: password authentication accepted for %.100s",
 		    authctxt->user);
From fcd135c9df440bcd2d5870405ad3311743d78d97 Mon Sep 17 00:00:00 2001
From: "dtucker@openbsd.org" <dtucker@openbsd.org>
Date: Thu, 21 Jul 2016 01:39:35 +0000
Subject: [PATCH] upstream commit

Skip passwords longer than 1k in length so clients can't
easily DoS sshd by sending very long passwords, causing it to spend CPU
hashing them. feedback djm@, ok markus@.

Brought to our attention by tomas.kuthan at oracle.com, shilei-c at
360.cn and coredump at autistici.org

Upstream-ID: d0af7d4a2190b63ba1d38eec502bc4be0be9e333
---
 auth-passwd.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/auth-passwd.c b/auth-passwd.c
index 530b5d4..996c2cf 100644
--- a/auth-passwd.c
+++ b/auth-passwd.c
@@ -66,6 +66,8 @@ extern login_cap_t *lc;
 #define DAY		(24L * 60 * 60) /* 1 day in seconds */
 #define TWO_WEEKS	(2L * 7 * DAY)	/* 2 weeks in seconds */
 
+#define MAX_PASSWORD_LEN	1024
+
 void
 disable_forwarding(void)
 {
@@ -87,6 +89,9 @@ auth_password(Authctxt *authctxt, const char *password)
 	static int expire_checked = 0;
 #endif
 
+	if (strlen(password) > MAX_PASSWORD_LEN)
+		return 0;
+
 #ifndef HAVE_CYGWIN
 	if (pw->pw_uid == 0 && options.permit_root_login != PERMIT_YES)
 		ok = 0;