From b95431ab2303eb258e37e88d8841f2fb79fc4af5 Mon Sep 17 00:00:00 2001
From: Joseph Sutton <josephsutton@catalyst.net.nz>
Date: Wed, 1 Jun 2022 16:08:42 +1200
Subject: [PATCH 06/15] CVE-2022-32743 dsdb: Implement validated dNSHostName
 write

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

Signed-off-by: Joseph Sutton <josephsutton@catalyst.net.nz>
Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
---
 selftest/knownfail.d/validated-dns-host-name |  12 --
 source4/dsdb/samdb/ldb_modules/acl.c         | 283 +++++++++++++++++++++++++++
 2 files changed, 283 insertions(+), 12 deletions(-)

diff --git a/selftest/knownfail.d/validated-dns-host-name b/selftest/knownfail.d/validated-dns-host-name
index ee51f44..4b61658 100644
--- a/selftest/knownfail.d/validated-dns-host-name
+++ b/selftest/knownfail.d/validated-dns-host-name
@@ -1,15 +1,3 @@
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_account_no_dollar\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_allowed_suffixes\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_case\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_dollar\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_empty_string\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_invalid\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_no_suffix\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_no_value\(
 ^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_spn\(
 ^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_spn_matching_account_name_new\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_spn_matching_account_name_original\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_wrong_prefix\(
-^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_dns_host_name_wrong_suffix\(
 ^samba4.ldap.acl.python.*__main__.AclModifyTests.test_modify_spn_matching_dns_host_name_invalid\(
diff --git a/source4/dsdb/samdb/ldb_modules/acl.c b/source4/dsdb/samdb/ldb_modules/acl.c
index 1fc6dbf..50802ae 100644
--- a/source4/dsdb/samdb/ldb_modules/acl.c
+++ b/source4/dsdb/samdb/ldb_modules/acl.c
@@ -802,6 +802,277 @@ static int acl_check_spn(TALLOC_CTX *mem_ctx,
 	return LDB_SUCCESS;
 }
 
+static int acl_check_dns_host_name(TALLOC_CTX *mem_ctx,
+				   struct ldb_module *module,
+				   struct ldb_request *req,
+				   const struct ldb_message_element *el,
+				   struct security_descriptor *sd,
+				   struct dom_sid *sid,
+				   const struct dsdb_attribute *attr,
+				   const struct dsdb_class *objectclass)
+{
+	int ret;
+	unsigned i;
+	TALLOC_CTX *tmp_ctx = NULL;
+	struct ldb_context *ldb = ldb_module_get_ctx(module);
+	const struct dsdb_schema *schema = NULL;
+	const struct ldb_message_element *allowed_suffixes = NULL;
+	struct ldb_result *nc_res = NULL;
+	struct ldb_dn *nc_root = NULL;
+	const char *nc_dns_name = NULL;
+	const char *dnsHostName_str = NULL;
+	size_t dns_host_name_len;
+	size_t account_name_len;
+	const struct ldb_message *msg = NULL;
+	const struct ldb_message *search_res = NULL;
+	const struct ldb_val *samAccountName = NULL;
+	const struct ldb_val *dnsHostName = NULL;
+	const struct dsdb_class *computer_objectclass = NULL;
+	bool is_subclass;
+
+	static const char *nc_attrs[] = {
+		"msDS-AllowedDNSSuffixes",
+		NULL
+	};
+
+	if (el->num_values == 0) {
+		return LDB_SUCCESS;
+	}
+	dnsHostName = &el->values[0];
+
+	tmp_ctx = talloc_new(mem_ctx);
+	if (tmp_ctx == NULL) {
+		return ldb_oom(ldb);
+	}
+
+	/* if we have wp, we can do whatever we like */
+	ret = acl_check_access_on_attribute(module,
+					    tmp_ctx,
+					    sd,
+					    sid,
+					    SEC_ADS_WRITE_PROP,
+					    attr, objectclass);
+	if (ret == LDB_SUCCESS) {
+		talloc_free(tmp_ctx);
+		return LDB_SUCCESS;
+	}
+
+	ret = acl_check_extended_right(tmp_ctx,
+				       module,
+				       req,
+				       objectclass,
+				       sd,
+				       acl_user_token(module),
+				       GUID_DRS_DNS_HOST_NAME,
+				       SEC_ADS_SELF_WRITE,
+				       sid);
+
+	if (ret != LDB_SUCCESS) {
+		dsdb_acl_debug(sd, acl_user_token(module),
+			       req->op.mod.message->dn,
+			       true,
+			       10);
+		talloc_free(tmp_ctx);
+		return ret;
+	}
+
+	/*
+	 * If we have "validated write dnshostname", allow delete of
+	 * any existing value (this keeps constrained delete to the
+	 * same rules as unconstrained)
+	 */
+	if (req->operation == LDB_MODIFY) {
+		struct ldb_result *acl_res = NULL;
+
+		static const char *acl_attrs[] = {
+			"sAMAccountName",
+			NULL
+		};
+
+		msg = req->op.mod.message;
+
+		/*
+		 * If not add or replace (eg delete),
+		 * return success
+		 */
+		if ((el->flags
+		     & (LDB_FLAG_MOD_ADD|LDB_FLAG_MOD_REPLACE)) == 0)
+		{
+			talloc_free(tmp_ctx);
+			return LDB_SUCCESS;
+		}
+
+		ret = dsdb_module_search_dn(module, tmp_ctx,
+					    &acl_res, msg->dn,
+					    acl_attrs,
+					    DSDB_FLAG_NEXT_MODULE |
+					    DSDB_FLAG_AS_SYSTEM |
+					    DSDB_SEARCH_SHOW_RECYCLED,
+					    req);
+		if (ret != LDB_SUCCESS) {
+			talloc_free(tmp_ctx);
+			return ret;
+		}
+
+		search_res = acl_res->msgs[0];
+	} else if (req->operation == LDB_ADD) {
+		msg = req->op.add.message;
+		search_res = msg;
+	} else {
+		talloc_free(tmp_ctx);
+		return LDB_ERR_OPERATIONS_ERROR;
+	}
+
+        /* Check if the account has objectclass 'computer' or 'server'. */
+
+	schema = dsdb_get_schema(ldb, req);
+	if (schema == NULL) {
+		talloc_free(tmp_ctx);
+		return ldb_operr(ldb);
+	}
+
+	computer_objectclass = dsdb_class_by_lDAPDisplayName(schema, "computer");
+	if (computer_objectclass == NULL) {
+		talloc_free(tmp_ctx);
+		return ldb_operr(ldb);
+	}
+
+	is_subclass = dsdb_is_subclass_of(schema, objectclass, computer_objectclass);
+	if (!is_subclass) {
+		/* The account is not a computer -- check if it's a server. */
+
+		const struct dsdb_class *server_objectclass = NULL;
+
+		server_objectclass = dsdb_class_by_lDAPDisplayName(schema, "server");
+		if (server_objectclass == NULL) {
+			talloc_free(tmp_ctx);
+			return ldb_operr(ldb);
+		}
+
+		is_subclass = dsdb_is_subclass_of(schema, objectclass, server_objectclass);
+		if (!is_subclass) {
+			/* Not a computer or server, so no need to validate. */
+			talloc_free(tmp_ctx);
+			return LDB_SUCCESS;
+		}
+	}
+
+	samAccountName = ldb_msg_find_ldb_val(search_res, "sAMAccountName");
+
+	ret = dsdb_msg_get_single_value(msg,
+					"sAMAccountName",
+					samAccountName,
+					&samAccountName,
+					req->operation);
+	if (ret != LDB_SUCCESS) {
+		talloc_free(tmp_ctx);
+		return ret;
+	}
+
+	account_name_len = samAccountName->length;
+	if (account_name_len && samAccountName->data[account_name_len - 1] == '$') {
+		/* Account for the '$' character. */
+		--account_name_len;
+	}
+
+	dnsHostName_str = (const char *)dnsHostName->data;
+	dns_host_name_len = dnsHostName->length;
+
+	/* Check that sAMAccountName matches the new dNSHostName. */
+
+	if (dns_host_name_len < account_name_len) {
+		goto fail;
+	}
+	if (strncasecmp(dnsHostName_str,
+			(const char *)samAccountName->data,
+			account_name_len) != 0)
+	{
+		goto fail;
+	}
+
+	dnsHostName_str += account_name_len;
+	dns_host_name_len -= account_name_len;
+
+	/* Check the '.' character */
+
+	if (dns_host_name_len == 0 || *dnsHostName_str != '.') {
+		goto fail;
+	}
+
+	++dnsHostName_str;
+	--dns_host_name_len;
+
+	/* Now we check the suffix. */
+
+	ret = dsdb_find_nc_root(ldb,
+				tmp_ctx,
+				search_res->dn,
+				&nc_root);
+	if (ret != LDB_SUCCESS) {
+		talloc_free(tmp_ctx);
+		return ret;
+	}
+
+	nc_dns_name = samdb_dn_to_dns_domain(tmp_ctx, nc_root);
+	if (nc_dns_name == NULL) {
+		talloc_free(tmp_ctx);
+		return ldb_operr(ldb);
+	}
+
+	if (strlen(nc_dns_name) == dns_host_name_len &&
+	    strncasecmp(dnsHostName_str,
+			nc_dns_name,
+			dns_host_name_len) == 0)
+	{
+		/* It matches -- success. */
+		talloc_free(tmp_ctx);
+		return LDB_SUCCESS;
+	}
+
+	/* We didn't get a match, so now try msDS-AllowedDNSSuffixes. */
+
+	ret = dsdb_module_search_dn(module, tmp_ctx,
+				    &nc_res, nc_root,
+				    nc_attrs,
+				    DSDB_FLAG_NEXT_MODULE |
+				    DSDB_FLAG_AS_SYSTEM |
+				    DSDB_SEARCH_SHOW_RECYCLED,
+				    req);
+	if (ret != LDB_SUCCESS) {
+		talloc_free(tmp_ctx);
+		return ret;
+	}
+
+	allowed_suffixes = ldb_msg_find_element(nc_res->msgs[0],
+						"msDS-AllowedDNSSuffixes");
+	if (allowed_suffixes == NULL) {
+		goto fail;
+	}
+
+	for (i = 0; i < allowed_suffixes->num_values; ++i) {
+		const struct ldb_val *suffix = &allowed_suffixes->values[i];
+
+		if (suffix->length == dns_host_name_len &&
+		    strncasecmp(dnsHostName_str,
+				(const char *)suffix->data,
+				dns_host_name_len) == 0)
+		{
+			/* It matches -- success. */
+			talloc_free(tmp_ctx);
+			return LDB_SUCCESS;
+		}
+	}
+
+fail:
+	ldb_debug_set(ldb, LDB_DEBUG_WARNING,
+		      "acl: hostname validation failed for "
+		      "hostname[%.*s] account[%.*s]\n",
+		      (int)dnsHostName->length, dnsHostName->data,
+		      (int)samAccountName->length, samAccountName->data);
+	talloc_free(tmp_ctx);
+	return LDB_ERR_CONSTRAINT_VIOLATION;
+}
+
 static int acl_add(struct ldb_module *module, struct ldb_request *req)
 {
 	int ret;
@@ -1536,6 +1807,18 @@ static int acl_modify(struct ldb_module *module, struct ldb_request *req)
 			if (ret != LDB_SUCCESS) {
 				goto fail;
 			}
+		} else if (ldb_attr_cmp("dnsHostName", el->name) == 0) {
+			ret = acl_check_dns_host_name(tmp_ctx,
+						      module,
+						      req,
+						      el,
+						      sd,
+						      sid,
+						      attr,
+						      objectclass);
+			if (ret != LDB_SUCCESS) {
+				goto fail;
+			}
 		} else if (is_undelete != NULL && (ldb_attr_cmp("isDeleted", el->name) == 0)) {
 			/*
 			 * in case of undelete op permissions on
-- 
1.8.3.1

