CVE-2023-42465 (published 2023-12-22T16:15Z)

Sudo before 1.9.15 might allow row hammer attacks (for authentication bypass or privilege escalation) because application logic sometimes is based on not equaling an error value (instead of equaling a success value), and because the values do not resist flips of a single bit.

CVSS v3 base score: 7.0, HIGH (CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/sudo-project/sudo
commit_details: repo_url: https://github.com/sudo-project/sudo
author: Todd C. Miller
author_date: 2023-09-09 14:07:04-06:00
msg: Try to make sudo less vulnerable to ROWHAMMER attacks.

We now use ROWHAMMER-resistent values for ALLOW, DENY, AUTH_SUCCESS,
AUTH_FAILURE, AUTH_ERROR and AUTH_NONINTERACTIVE.  In addition, we
explicitly test for expected values instead of using a negated test
against an error value.  In the parser match functions this means
explicitly checking for ALLOW or DENY instead of accepting anything
that is not set to UNSPEC.

Thanks to Andrew J. Adiletta, M. Caner Tol, Yarkin Doroz, and Berk
Sunar, all affiliated with the Vernam Applied Cryptography and
Cybersecurity Lab at Worcester Polytechnic Institute, for the report.
Paper preprint: https://arxiv.org/abs/2309.02545
repository: repo_url: https://github.com/sudo-project/sudo
repo_name: sudo-project/sudo
description: Utility to execute a command as another user
repo_language: C
owner: sudo-project
file_changes: filename: passwd.c
change_type: ModificationType.MODIFY
diff: @@ -68,7 +68,7 @@ sudo_passwd_verify(const struct sudoers_context *ctx, struct passwd *pw,
     char des_pass[9], *epass;
     char *pw_epasswd = auth->data;
     size_t pw_len;
-    int matched = 0;
+    int ret;
     debug_decl(sudo_passwd_verify, SUDOERS_DEBUG_AUTH);
 
     /* An empty plain-text password must match an empty encrypted password. */
@@ -80,7 +80,7 @@ sudo_passwd_verify(const struct sudoers_context *ctx, struct passwd *pw,
      */
     pw_len = strlen(pw_epasswd);
     if (pw_len == DESLEN || HAS_AGEINFO(pw_epasswd, pw_len)) {
-	strlcpy(des_pass, pass, sizeof(des_pass));
+	(void)strlcpy(des_pass, pass, sizeof(des_pass));
 	pass = des_pass;
     }
 
@@ -90,16 +90,20 @@ sudo_passwd_verify(const struct sudoers_context *ctx, struct passwd *pw,
      * only compare the first DESLEN characters in that case.
      */
     epass = (char *) crypt(pass, pw_epasswd);
+    ret = AUTH_FAILURE;
     if (epass != NULL) {
-	if (HAS_AGEINFO(pw_epasswd, pw_len) && strlen(epass) == DESLEN)
-	    matched = !strncmp(pw_epasswd, epass, DESLEN);
-	else
-	    matched = !strcmp(pw_epasswd, epass);
+	if (HAS_AGEINFO(pw_epasswd, pw_len) && strlen(epass) == DESLEN) {
+	    if (strncmp(pw_epasswd, epass, DESLEN) == 0)
+		ret = AUTH_SUCCESS;
+	} else {
+	    if (strcmp(pw_epasswd, epass) == 0)
+		ret = AUTH_SUCCESS;
+	}
     }
 
     explicit_bzero(des_pass, sizeof(des_pass));
 
-    debug_return_int(matched ? AUTH_SUCCESS : AUTH_FAILURE);
+    debug_return_int(ret);
 }
 #else
 int
@@ -107,13 +111,16 @@ sudo_passwd_verify(const struct sudoers_context *ctx, struct passwd *pw,
     const char *pass, sudo_auth *auth, struct sudo_conv_callback *callback)
 {
     char *pw_passwd = auth->data;
-    int matched;
+    int ret;
     debug_decl(sudo_passwd_verify, SUDOERS_DEBUG_AUTH);
 
     /* Simple string compare for systems without crypt(). */
-    matched = !strcmp(pass, pw_passwd);
+    if (strcmp(pass, pw_passwd) == 0)
+	ret = AUTH_SUCCESS;
+    else
+	ret = AUTH_FAILURE;
 
-    debug_return_int(matched ? AUTH_SUCCESS : AUTH_FAILURE);
+    debug_return_int(ret);
 }
 #endif
 

programming_language: C
filename: sudo_auth.c
change_type: ModificationType.MODIFY
diff: @@ -116,10 +116,16 @@ sudo_auth_init(const struct sudoers_context *ctx, struct passwd *pw,
 	if (auth->init && !IS_DISABLED(auth)) {
 	    /* Disable if it failed to init unless there was a fatal error. */
 	    status = (auth->init)(ctx, pw, auth);
-	    if (status == AUTH_FAILURE)
+	    switch (status) {
+	    case AUTH_SUCCESS:
+		break;
+	    case AUTH_FAILURE:
 		SET(auth->flags, FLAG_DISABLED);
-	    else if (status == AUTH_ERROR)
-		break;		/* assume error msg already printed */
+		break;
+	    default:
+		/* Assume error msg already printed. */
+		debug_return_int(-1);
+	    }
 	}
     }
 
@@ -166,7 +172,7 @@ sudo_auth_init(const struct sudoers_context *ctx, struct passwd *pw,
 	}
     }
 
-    debug_return_int(status == AUTH_ERROR ? -1 : 0);
+    debug_return_int(0);
 }
 
 /*
@@ -209,7 +215,7 @@ sudo_auth_cleanup(const struct sudoers_context *ctx, struct passwd *pw,
     for (auth = auth_switch; auth->name; auth++) {
 	if (auth->cleanup && !IS_DISABLED(auth)) {
 	    int status = (auth->cleanup)(ctx, pw, auth, force);
-	    if (status == AUTH_ERROR) {
+	    if (status != AUTH_SUCCESS) {
 		/* Assume error msg already printed. */
 		debug_return_int(-1);
 	    }
@@ -306,7 +312,7 @@ verify_user(const struct sudoers_context *ctx, struct passwd *pw, char *prompt,
 		    SET(auth->flags, FLAG_DISABLED);
 		else if (status == AUTH_NONINTERACTIVE)
 		    goto done;
-		else if (status == AUTH_ERROR || user_interrupted())
+		else if (status != AUTH_SUCCESS || user_interrupted())
 		    goto done;		/* assume error msg already printed */
 	    }
 	}
@@ -365,7 +371,6 @@ done:
 	case AUTH_NONINTERACTIVE:
 	    SET(validated, FLAG_NO_USER_INPUT);
 	    FALLTHROUGH;
-	case AUTH_ERROR:
 	default:
 	    log_auth_failure(ctx, validated, 0);
 	    ret = -1;
@@ -377,25 +382,33 @@ done:
 
 /*
  * Call authentication method begin session hooks.
- * Returns 1 on success and -1 on error.
+ * Returns true on success, false on failure and -1 on error.
  */
 int
 sudo_auth_begin_session(const struct sudoers_context *ctx, struct passwd *pw,
     char **user_env[])
 {
     sudo_auth *auth;
+    int ret = true;
     debug_decl(sudo_auth_begin_session, SUDOERS_DEBUG_AUTH);
 
     for (auth = auth_switch; auth->name; auth++) {
 	if (auth->begin_session && !IS_DISABLED(auth)) {
 	    int status = (auth->begin_session)(ctx, pw, user_env, auth);
-	    if (status != AUTH_SUCCESS) {
+	    switch (status) {
+	    case AUTH_SUCCESS:
+		break;
+	    case AUTH_FAILURE:
+		ret = false;
+		break;
+	    default:
 		/* Assume error msg already printed. */
-		debug_return_int(-1);
+		ret = -1;
+		break;
 	    }
 	}
     }
-    debug_return_int(1);
+    debug_return_int(ret);
 }
 
 bool
@@ -416,25 +429,33 @@ sudo_auth_needs_end_session(void)
 
 /*
  * Call authentication method end session hooks.
- * Returns 1 on success and -1 on error.
+ * Returns true on success, false on failure and -1 on error.
  */
 int
 sudo_auth_end_session(void)
 {
     sudo_auth *auth;
+    int ret = true;
     int status;
     debug_decl(sudo_auth_end_session, SUDOERS_DEBUG_AUTH);
 
     for (auth = auth_switch; auth->name; auth++) {
 	if (auth->end_session && !IS_DISABLED(auth)) {
 	    status = (auth->end_session)(auth);
-	    if (status == AUTH_ERROR) {
+	    switch (status) {
+	    case AUTH_SUCCESS:
+		break;
+	    case AUTH_FAILURE:
+		ret = false;
+		break;
+	    default:
 		/* Assume error msg already printed. */
-		debug_return_int(-1);
+		ret = -1;
+		break;
 	    }
 	}
     }
-    debug_return_int(1);
+    debug_return_int(ret);
 }
 
 /*

programming_language: C
filename: sudo_auth.h
change_type: ModificationType.MODIFY
diff: @@ -19,12 +19,12 @@
 #ifndef SUDO_AUTH_H
 #define SUDO_AUTH_H
 
-/* Auth function return values.  */
-#define AUTH_SUCCESS		0
-#define AUTH_FAILURE		1
-#define AUTH_INTR		2
-#define AUTH_ERROR		3
-#define AUTH_NONINTERACTIVE	4
+/* Auth function return values (rowhammer resistent).  */
+#define AUTH_SUCCESS		0x52a2925	/* 0101001010100010100100100101 */
+#define AUTH_FAILURE		0xad5d6da	/* 1010110101011101011011011010 */
+#define AUTH_INTR		0x69d61fc8	/* 1101001110101100001111111001000 */
+#define AUTH_ERROR		0x1629e037	/* 0010110001010011110000000110111 */
+#define AUTH_NONINTERACTIVE	0x1fc8d3ac	/* 11111110010001101001110101100 */
 
 typedef struct sudo_auth {
     unsigned int flags;		/* various flags, see below */

programming_language: C
filename: lookup.c
change_type: ModificationType.MODIFY
diff: @@ -100,7 +100,7 @@ sudoers_lookup_pseudo(struct sudo_nss_list *snl, struct sudoers_context *ctx,
 	    int user_match = userlist_matches(nss->parse_tree, ctx->user.pw,
 		&us->users);
 	    if (user_match != ALLOW) {
-		if (callback != NULL && user_match != UNSPEC) {
+		if (callback != NULL && user_match == DENY) {
 		    callback(nss->parse_tree, us, user_match, NULL, UNSPEC,
 			NULL, UNSPEC, UNSPEC, UNSPEC, cb_data);
 		}
@@ -189,7 +189,7 @@ sudoers_lookup_pseudo(struct sudo_nss_list *snl, struct sudoers_context *ctx,
 			    host_match, cs, date_match, runas_match,
 			    cmnd_match, cb_data);
 		    }
-		    if (cmnd_match != UNSPEC) {
+		    if (SPECIFIED(cmnd_match)) {
 			/*
 			 * We take the last match but must process
 			 * the entire policy for pwcheck == all.
@@ -245,7 +245,7 @@ sudoers_lookup_check(struct sudo_nss *nss, struct sudoers_context *ctx,
     TAILQ_FOREACH_REVERSE(us, &nss->parse_tree->userspecs, userspec_list, entries) {
 	int user_match = userlist_matches(nss->parse_tree, ctx->user.pw, &us->users);
 	if (user_match != ALLOW) {
-	    if (callback != NULL && user_match != UNSPEC) {
+	    if (callback != NULL && user_match == DENY) {
 		callback(nss->parse_tree, us, user_match, NULL, UNSPEC, NULL,
 		    UNSPEC, UNSPEC, UNSPEC, cb_data);
 	    }
@@ -290,7 +290,7 @@ sudoers_lookup_check(struct sudo_nss *nss, struct sudoers_context *ctx,
 			cs, date_match, runas_match, cmnd_match, cb_data);
 		}
 
-		if (cmnd_match != UNSPEC) {
+		if (SPECIFIED(cmnd_match)) {
 		    /*
 		     * If user is running command as themselves,
 		     * set ctx->runas.pw = ctx->user.pw.
@@ -542,7 +542,7 @@ sudoers_lookup(struct sudo_nss_list *snl, struct sudoers_context *ctx,
 
 	m = sudoers_lookup_check(nss, ctx, &validated, &info, now, callback,
 	    cb_data, &cs, &defs);
-	if (m != UNSPEC) {
+	if (SPECIFIED(m)) {
 	    match = m;
 	    parse_tree = nss->parse_tree;
 	}
@@ -550,7 +550,7 @@ sudoers_lookup(struct sudo_nss_list *snl, struct sudoers_context *ctx,
 	if (!sudo_nss_can_continue(nss, m))
 	    break;
     }
-    if (match != UNSPEC) {
+    if (SPECIFIED(match)) {
 	if (info.cmnd_path != NULL) {
 	    /* Update cmnd, cmnd_stat, cmnd_status from matching entry. */
 	    free(ctx->user.cmnd);

programming_language: C
filename: match.c
change_type: ModificationType.MODIFY
diff: @@ -91,7 +91,7 @@ user_matches(const struct sudoers_parse_tree *parse_tree,
 	    if ((a = alias_get(parse_tree, m->name, USERALIAS)) != NULL) {
 		/* XXX */
 		const int rc = userlist_matches(parse_tree, pw, &a->members);
-		if (rc != UNSPEC) {
+		if (SPECIFIED(rc)) {
 		    if (m->negated) {
 			matched = rc == ALLOW ? DENY : ALLOW;
 		    } else {
@@ -123,7 +123,8 @@ userlist_matches(const struct sudoers_parse_tree *parse_tree,
     debug_decl(userlist_matches, SUDOERS_DEBUG_MATCH);
 
     TAILQ_FOREACH_REVERSE(m, list, member_list, entries) {
-	if ((matched = user_matches(parse_tree, pw, m)) != UNSPEC)
+	matched = user_matches(parse_tree, pw, m);
+	if (SPECIFIED(matched))
 	    break;
     }
     debug_return_int(matched);
@@ -184,7 +185,7 @@ runas_userlist_matches(const struct sudoers_parse_tree *parse_tree,
 		if (a != NULL) {
 		    const int rc = runas_userlist_matches(parse_tree,
 			&a->members, matching_user);
-		    if (rc != UNSPEC) {
+		    if (SPECIFIED(rc)) {
 			if (m->negated) {
 			    user_matched = rc == ALLOW ? DENY : ALLOW;
 			} else {
@@ -211,7 +212,7 @@ runas_userlist_matches(const struct sudoers_parse_tree *parse_tree,
 		    user_matched = m->negated ? DENY : ALLOW;
 		break;
 	}
-	if (user_matched != UNSPEC) {
+	if (SPECIFIED(user_matched)) {
 	    if (matching_user != NULL && m->type != ALIAS)
 		*matching_user = m;
 	    break;
@@ -246,7 +247,7 @@ runas_grouplist_matches(const struct sudoers_parse_tree *parse_tree,
 		    if (a != NULL) {
 			const int rc = runas_grouplist_matches(parse_tree,
 			    &a->members, matching_group);
-			if (rc != UNSPEC) {
+			if (SPECIFIED(rc)) {
 			    if (m->negated) {
 				group_matched = rc == ALLOW ? DENY : ALLOW;
 			    } else {
@@ -262,14 +263,14 @@ runas_grouplist_matches(const struct sudoers_parse_tree *parse_tree,
 			group_matched = m->negated ? DENY : ALLOW;
 		    break;
 	    }
-	    if (group_matched != UNSPEC) {
+	    if (SPECIFIED(group_matched)) {
 		if (matching_group != NULL && m->type != ALIAS)
 		    *matching_group = m;
 		break;
 	    }
 	}
     }
-    if (group_matched == UNSPEC) {
+    if (!SPECIFIED(group_matched)) {
 	struct gid_list *runas_groups;
 	/*
 	 * The runas group was not explicitly allowed by sudoers.
@@ -349,7 +350,7 @@ hostlist_matches_int(const struct sudoers_parse_tree *parse_tree,
 
     TAILQ_FOREACH_REVERSE(m, list, member_list, entries) {
 	matched = host_matches(parse_tree, pw, lhost, shost, m);
-	if (matched != UNSPEC)
+	if (SPECIFIED(matched))
 	    break;
     }
     debug_return_int(matched);
@@ -402,7 +403,7 @@ host_matches(const struct sudoers_parse_tree *parse_tree,
 		/* XXX */
 		const int rc = hostlist_matches_int(parse_tree, pw, lhost,
 		    shost, &a->members);
-		if (rc != UNSPEC) {
+		if (SPECIFIED(rc)) {
 		    if (m->negated) {
 			matched = rc == ALLOW ? DENY : ALLOW;
 		    } else {
@@ -440,7 +441,7 @@ cmndlist_matches(const struct sudoers_parse_tree *parse_tree,
 
     TAILQ_FOREACH_REVERSE(m, list, member_list, entries) {
 	matched = cmnd_matches(parse_tree, m, runchroot, info);
-	if (matched != UNSPEC)
+	if (SPECIFIED(matched))
 	    break;
     }
     debug_return_int(matched);
@@ -471,7 +472,7 @@ cmnd_matches(const struct sudoers_parse_tree *parse_tree,
 	    a = alias_get(parse_tree, m->name, CMNDALIAS);
 	    if (a != NULL) {
 		rc = cmndlist_matches(parse_tree, &a->members, runchroot, info);
-		if (rc != UNSPEC) {
+		if (SPECIFIED(rc)) {
 		    if (m->negated) {
 			matched = rc == ALLOW ? DENY : ALLOW;
 		    } else {
@@ -511,7 +512,7 @@ cmnd_matches_all(const struct sudoers_parse_tree *parse_tree,
 	    if (a != NULL) {
 		TAILQ_FOREACH_REVERSE(m, &a->members, member_list, entries) {
 		    matched = cmnd_matches_all(parse_tree, m, runchroot, info);
-		    if (matched != UNSPEC) {
+		    if (SPECIFIED(matched)) {
 			if (negated)
 			    matched = matched == ALLOW ? DENY : ALLOW;
 			break;

programming_language: C
filename: parse.h
change_type: ModificationType.MODIFY
diff: @@ -36,15 +36,28 @@
 # define SUDOERS_NAME_MATCH
 #endif
 
+/* Allowed by policy (rowhammer resistent). */
+#undef ALLOW
+#define ALLOW	 0x52a2925	/* 0101001010100010100100100101 */
+
+/* Denied by policy (rowhammer resistent). */
+#undef DENY
+#define DENY	 0xad5d6da	/* 1010110101011101011011011010 */
+
+/* Neither allowed, nor denied. */
 #undef UNSPEC
 #define UNSPEC	-1
-#undef DENY
-#define DENY	 0
-#undef ALLOW
-#define ALLOW	 1
+
+/* Tag implied by root access (SETENV only). */
 #undef IMPLIED
 #define IMPLIED	 2
 
+/*
+ * We must explicitly check against ALLOW and DENY instead testing
+ * that the value is not UNSPEC to avoid potential ROWHAMMER issues.
+ */
+#define SPECIFIED(_v)	((_v) == ALLOW || (_v) == DENY)
+
 /*
  * Initialize all tags to UNSPEC.
  */
@@ -94,7 +107,7 @@
  * Returns true if the specified tag is not UNSPEC or IMPLIED, else false.
  */
 #define TAG_SET(tt) \
-    ((tt) != UNSPEC && (tt) != IMPLIED)
+    ((tt) == true || (tt) == false)
 
 /*
  * Returns true if any tags set in nt differ between ot and nt, else false.

programming_language: C