340 lines
9.8 KiB
Diff
340 lines
9.8 KiB
Diff
|
|
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
|
||
|
|
|