From 4439ac7bb6e8fcb1610fa94923c3daaed3e4c958 Mon Sep 17 00:00:00 2001
From: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Date: Fri, 22 Oct 2021 13:17:34 +1300
Subject: [PATCH 153/266] CVE-2020-25722 s4/dsdb/samldb: check for clashes in
 UPNs/samaccountnames

We already know duplicate sAMAccountNames and UserPrincipalNames are bad,
but we also have to check against the values these imply in each other.

For example, imagine users with SAM account names "Alice" and "Bob" in
the realm "example.com". If they do not have explicit UPNs, by the logic
of MS-ADTS 5.1.1.1.1 they use the implict UPNs "alice@example.com" and
"bob@example.com", respectively. If Bob's UPN gets set to
"alice@example.com", it will clash with Alice's implicit one.

Therefore we refuse to allow a UPN that implies an existing SAM account
name and vice versa.

BUG: https://bugzilla.samba.org/show_bug.cgi?id=14564

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>

Conflict:remove test
Reference:https://gitlab.com/samba-team/samba/-/commit/4439ac7bb6e8fcb1610fa94923c3daaed3e4c958

---
 source4/dsdb/samdb/ldb_modules/samldb.c   | 206 +++++++++++++++++++++-
 1 files changed, 203 insertions(+), 3 deletions(-)

diff --git a/source4/dsdb/samdb/ldb_modules/samldb.c b/source4/dsdb/samdb/ldb_modules/samldb.c
index a03fc6eb07c..0cf00e2b19e 100644
--- a/source4/dsdb/samdb/ldb_modules/samldb.c
+++ b/source4/dsdb/samdb/ldb_modules/samldb.c
@@ -235,8 +235,9 @@ static int samldb_unique_attr_check(struct samldb_ctx *ac, const char *attr,
 		return ldb_module_oom(ac->module);
 	}
 
-	/* Make sure that attr (eg) "sAMAccountName" is only used once */
-
+	/*
+	 * No other object should have the attribute with this value.
+	 */
 	if (attr_conflict != NULL) {
 		ret = dsdb_module_search(ac->module, ac, &res,
 					 base_dn,
@@ -270,6 +271,193 @@ static int samldb_unique_attr_check(struct samldb_ctx *ac, const char *attr,
 	return LDB_SUCCESS;
 }
 
+
+
+static inline int samldb_sam_account_upn_clash_sub_search(
+	struct samldb_ctx *ac,
+	TALLOC_CTX *mem_ctx,
+	struct ldb_dn *base_dn,
+	const char *attr,
+	const char *value,
+	const char *err_msg
+	)
+{
+	/*
+	 * A very specific helper function for samldb_sam_account_upn_clash(),
+	 * where we end up doing this same thing several times in a row.
+	 */
+	const char * const no_attrs[] = { NULL };
+	struct ldb_context *ldb = ldb_module_get_ctx(ac->module);
+	struct ldb_result *res = NULL;
+	int ret;
+	char *enc_value = ldb_binary_encode_string(ac, value);
+	if (enc_value == NULL) {
+		return ldb_module_oom(ac->module);
+	}
+	ret = dsdb_module_search(ac->module, mem_ctx, &res,
+				 base_dn,
+				 LDB_SCOPE_SUBTREE, no_attrs,
+				 DSDB_FLAG_NEXT_MODULE, ac->req,
+				 "(%s=%s)",
+				 attr, enc_value);
+	talloc_free(enc_value);
+
+	if (ret != LDB_SUCCESS) {
+		return ret;
+	} else if (res->count > 1) {
+		return ldb_operr(ldb);
+	} else if (res->count == 1) {
+		if (ldb_dn_compare(res->msgs[0]->dn, ac->msg->dn) != 0){
+			ldb_asprintf_errstring(ldb,
+					       "samldb: %s '%s' "
+					       "is already in use %s",
+					       attr, value, err_msg);
+			/* different errors for different attrs */
+			if (strcasecmp("userPrincipalName", attr) == 0) {
+				return LDB_ERR_CONSTRAINT_VIOLATION;
+			}
+			return LDB_ERR_ENTRY_ALREADY_EXISTS;
+		}
+	}
+	return LDB_SUCCESS;
+}
+
+static int samldb_sam_account_upn_clash(struct samldb_ctx *ac)
+{
+	struct ldb_context *ldb = ldb_module_get_ctx(ac->module);
+	int ret;
+	struct ldb_dn *base_dn = ldb_get_default_basedn(ldb);
+	TALLOC_CTX *tmp_ctx = NULL;
+	const char *real_sam = NULL;
+	const char *real_upn = NULL;
+	char *implied_sam = NULL;
+	char *implied_upn = NULL;
+	const char *realm = NULL;
+
+	ret = samldb_get_single_valued_attr(ldb, ac,
+					    "sAMAccountName",
+					    &real_sam);
+	if (ret != LDB_SUCCESS) {
+		return ret;
+	}
+	ret = samldb_get_single_valued_attr(ldb, ac,
+					    "userPrincipalName",
+					    &real_upn);
+	if (ret != LDB_SUCCESS) {
+		return ret;
+	}
+	if (real_upn == NULL && real_sam == NULL) {
+		/* Not changing these things, so we're done */
+		return LDB_SUCCESS;
+	}
+
+	tmp_ctx = talloc_new(ac);
+	realm = samdb_dn_to_dns_domain(tmp_ctx, base_dn);
+	if (realm == NULL) {
+		talloc_free(tmp_ctx);
+		return ldb_operr(ldb);
+	}
+
+	if (real_upn != NULL) {
+		/*
+		 * note we take the last @ in the upn because the first (i.e.
+		 * sAMAccountName equivalent) part can contain @.
+		 *
+		 * It is also OK (per Windows) for a UPN to have zero @s.
+		 */
+		char *at = NULL;
+		char *upn_realm = NULL;
+		implied_sam = talloc_strdup(tmp_ctx, real_upn);
+		if (implied_sam == NULL) {
+			talloc_free(tmp_ctx);
+			return ldb_module_oom(ac->module);
+		}
+
+		at = strrchr(implied_sam, '@');
+		if (at == NULL) {
+			/*
+			 * there is no @ in this UPN, so we treat the whole
+			 * thing as a sAMAccountName for the purposes of a
+			 * clash.
+			 */
+			DBG_INFO("samldb: userPrincipalName '%s' contains "
+				 "no '@' character\n", implied_sam);
+		} else {
+			/*
+			 * Now, this upn only implies a sAMAccountName if the
+			 * realm is our realm. So we need to compare the tail
+			 * of the upn to the realm.
+			 */
+			*at = '\0';
+			upn_realm = at + 1;
+			if (strcasecmp(upn_realm, realm) != 0) {
+				/* implied_sam is not the implied
+				 * sAMAccountName after all, because it is
+				 * from a different realm. */
+				TALLOC_FREE(implied_sam);
+			}
+		}
+	}
+
+	if (real_sam != NULL) {
+		implied_upn = talloc_asprintf(tmp_ctx, "%s@%s",
+					      real_sam, realm);
+		if (implied_upn == NULL) {
+			talloc_free(tmp_ctx);
+			return ldb_module_oom(ac->module);
+		}
+	}
+
+	/*
+	 * Now we have all of the actual and implied names, in which to search
+	 * for conflicts.
+	 */
+	if (real_sam != NULL) {
+		ret = samldb_sam_account_upn_clash_sub_search(
+			ac, tmp_ctx, base_dn, "sAMAccountName",
+			real_sam, "");
+
+		if (ret != LDB_SUCCESS) {
+			talloc_free(tmp_ctx);
+			return ret;
+		}
+	}
+	if (implied_upn != NULL) {
+		ret = samldb_sam_account_upn_clash_sub_search(
+			ac, tmp_ctx, base_dn, "userPrincipalName", implied_upn,
+			"(implied by sAMAccountName)");
+
+		if (ret != LDB_SUCCESS) {
+			talloc_free(tmp_ctx);
+			return ret;
+		}
+	}
+	if (real_upn != NULL) {
+		ret = samldb_sam_account_upn_clash_sub_search(
+			ac, tmp_ctx, base_dn, "userPrincipalName",
+			real_upn, "");
+
+		if (ret != LDB_SUCCESS) {
+			talloc_free(tmp_ctx);
+			return ret;
+		}
+	}
+	if (implied_sam != NULL) {
+		ret = samldb_sam_account_upn_clash_sub_search(
+			ac, tmp_ctx, base_dn, "sAMAccountName", implied_sam,
+			"(implied by userPrincipalName)");
+		if (ret != LDB_SUCCESS) {
+			talloc_free(tmp_ctx);
+			return ret;
+		}
+	}
+
+	talloc_free(tmp_ctx);
+	return LDB_SUCCESS;
+}
+
+
+/* This is run during an add or modify */
 static int samldb_sam_accountname_valid_check(struct samldb_ctx *ac)
 {
 	int ret = 0;
@@ -303,7 +491,11 @@ static int samldb_sam_accountname_valid_check(struct samldb_ctx *ac)
 	} else if (ret == LDB_ERR_OBJECT_CLASS_VIOLATION) {
 		ret = LDB_ERR_CONSTRAINT_VIOLATION;
 	}
+	if (ret != LDB_SUCCESS) {
+		return ret;
+	}
 
+	ret = samldb_sam_account_upn_clash(ac);
 	if (ret != LDB_SUCCESS) {
 		return ret;
 	}
@@ -4175,7 +4367,6 @@ static int samldb_modify(struct ldb_module *module, struct ldb_request *req)
 		if (ret != LDB_SUCCESS) {
 			return ret;
 		}
-
 		user_account_control
 			= ldb_msg_find_attr_as_uint(res->msgs[0],
 						    "userAccountControl",
@@ -4199,6 +4390,15 @@ static int samldb_modify(struct ldb_module *module, struct ldb_request *req)
 		}
 	}
 
+	el = ldb_msg_find_element(ac->msg, "userPrincipalName");
+	if (el != NULL) {
+		ret = samldb_sam_account_upn_clash(ac);
+		if (ret != LDB_SUCCESS) {
+			talloc_free(ac);
+			return ret;
+		}
+	}
+
 	el = ldb_msg_find_element(ac->msg, "ldapDisplayName");
 	if (el != NULL) {
 		ret = samldb_schema_ldapdisplayname_valid_check(ac);
-- 
2.23.0

