summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Beale <timbeale@catalyst.net.nz>2018-07-20 15:42:36 +1200
committerKarolin Seeger <kseeger@samba.org>2018-08-13 09:13:36 +0200
commit246e79f6ed34e6585eb22bb68aa558b85e0a6522 (patch)
treed45b6951a54c06ec4a87528d6d5d79a2484ad3e5
parent9605ecc7e1a79fa1b7cdf9f53048595ee00008b2 (diff)
downloadsamba-246e79f6ed34e6585eb22bb68aa558b85e0a6522.tar.gz
CVE-2018-10919 acl_read: Fix unauthorized attribute access via searches
A user that doesn't have access to view an attribute can still guess the attribute's value via repeated LDAP searches. This affects confidential attributes, as well as ACLs applied to an object/attribute to deny access. Currently the code will hide objects if the attribute filter contains an attribute they are not authorized to see. However, the code still returns objects as results if confidential attribute is in the search expression itself, but not in the attribute filter. To fix this problem we have to check the access rights on the attributes in the search-tree, as well as the attributes returned in the message. Points of note: - I've preserved the existing dirsync logic (the dirsync module code suppresses the result as long as the replPropertyMetaData attribute is removed). However, there doesn't appear to be any test that highlights that this functionality is required for dirsync. - To avoid this fix breaking the acl.py tests, we need to still permit searches like 'objectClass=*', even though we don't have Read Property access rights for the objectClass attribute. The logic that Windows uses does not appear to be clearly documented, so I've made a best guess that seems to mirror Windows behaviour. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13434 Signed-off-by: Tim Beale <timbeale@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abartlet@samba.org> Reviewed-by: Gary Lockyer <gary@catalyst.net.nz>
-rw-r--r--selftest/knownfail.d/acl1
-rw-r--r--selftest/knownfail.d/confidential_attr15
-rw-r--r--source4/dsdb/samdb/ldb_modules/acl_read.c247
3 files changed, 247 insertions, 16 deletions
diff --git a/selftest/knownfail.d/acl b/selftest/knownfail.d/acl
deleted file mode 100644
index 6772ea1f943..00000000000
--- a/selftest/knownfail.d/acl
+++ /dev/null
@@ -1 +0,0 @@
-^samba4.ldap.acl.python.*test_search7
diff --git a/selftest/knownfail.d/confidential_attr b/selftest/knownfail.d/confidential_attr
deleted file mode 100644
index 7a2f2aada57..00000000000
--- a/selftest/knownfail.d/confidential_attr
+++ /dev/null
@@ -1,15 +0,0 @@
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTest.test_basic_search\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTest.test_search_with_acl_override\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTest.test_search_with_attr_acl_override\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTest.test_search_with_blanket_oa_acl\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTest.test_search_with_neutral_acl\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTest.test_search_with_neutral_acl\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTest.test_search_with_neutral_attr_acl\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTest.test_search_with_neutral_cr_acl\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTest.test_search_with_propset_acl_override\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTestDenyAcl.test_search_with_blanket_oa_deny_acl\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTestDenyAcl.test_search_with_deny_acl\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTestDenyAcl.test_search_with_deny_attr_acl\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTestDenyAcl.test_search_with_deny_propset_acl\(ad_dc_ntvfs\)
-samba4.ldap.confidential_attr.python\(ad_dc_ntvfs\).__main__.ConfidentialAttrTestDirsync.test_search_with_dirsync\(ad_dc_ntvfs\)
-
diff --git a/source4/dsdb/samdb/ldb_modules/acl_read.c b/source4/dsdb/samdb/ldb_modules/acl_read.c
index 00203f21dae..eb8676d215d 100644
--- a/source4/dsdb/samdb/ldb_modules/acl_read.c
+++ b/source4/dsdb/samdb/ldb_modules/acl_read.c
@@ -38,6 +38,8 @@
#include "param/param.h"
#include "dsdb/samdb/ldb_modules/util.h"
+/* The attributeSecurityGuid for the Public Information Property-Set */
+#define PUBLIC_INFO_PROPERTY_SET "e48d0154-bcf8-11d1-8702-00c04fb96050"
struct aclread_context {
struct ldb_module *module;
@@ -99,6 +101,219 @@ static uint32_t get_attr_access_mask(const struct dsdb_attribute *attr,
return access_mask;
}
+/* helper struct for traversing the attributes in the search-tree */
+struct parse_tree_aclread_ctx {
+ struct aclread_context *ac;
+ TALLOC_CTX *mem_ctx;
+ struct dom_sid *sid;
+ struct ldb_dn *dn;
+ struct security_descriptor *sd;
+ const struct dsdb_class *objectclass;
+ bool suppress_result;
+};
+
+/*
+ * Checks that the user has sufficient access rights to view an attribute
+ */
+static int check_attr_access_rights(TALLOC_CTX *mem_ctx, const char *attr_name,
+ struct aclread_context *ac,
+ struct security_descriptor *sd,
+ const struct dsdb_class *objectclass,
+ struct dom_sid *sid, struct ldb_dn *dn,
+ bool *is_public_info)
+{
+ int ret;
+ const struct dsdb_attribute *attr = NULL;
+ uint32_t access_mask;
+ struct ldb_context *ldb = ldb_module_get_ctx(ac->module);
+
+ *is_public_info = false;
+
+ attr = dsdb_attribute_by_lDAPDisplayName(ac->schema, attr_name);
+ if (!attr) {
+ ldb_debug_set(ldb,
+ LDB_DEBUG_TRACE,
+ "acl_read: %s cannot find attr[%s] in schema,"
+ "ignoring\n",
+ ldb_dn_get_linearized(dn), attr_name);
+ return LDB_SUCCESS;
+ }
+
+ /*
+ * If we have no Read Property (RP) rights for a child object, it should
+ * still appear as a visible object in 'objectClass=*' searches,
+ * as long as we have List Contents (LC) rights for it.
+ * This is needed for the acl.py tests (e.g. test_search1()).
+ * I couldn't find the Windows behaviour documented in the specs, so
+ * this is a guess, but it seems to only apply to attributes in the
+ * Public Information Property Set that have the systemOnly flag set to
+ * TRUE. (This makes sense in a way, as it's not disclosive to find out
+ * that a child object has a 'objectClass' or 'name' attribute, as every
+ * object has these attributes).
+ */
+ if (attr->systemOnly) {
+ struct GUID public_info_guid;
+ NTSTATUS status;
+
+ status = GUID_from_string(PUBLIC_INFO_PROPERTY_SET,
+ &public_info_guid);
+ if (!NT_STATUS_IS_OK(status)) {
+ ldb_set_errstring(ldb, "Public Info GUID parse error");
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ if (GUID_compare(&attr->attributeSecurityGUID,
+ &public_info_guid) == 0) {
+ *is_public_info = true;
+ }
+ }
+
+ access_mask = get_attr_access_mask(attr, ac->sd_flags);
+
+ /* the access-mask should be non-zero. Skip attribute otherwise */
+ if (access_mask == 0) {
+ DBG_ERR("Could not determine access mask for attribute %s\n",
+ attr_name);
+ return LDB_SUCCESS;
+ }
+
+ ret = acl_check_access_on_attribute(ac->module, mem_ctx, sd, sid,
+ access_mask, attr, objectclass);
+
+ if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) {
+ return ret;
+ }
+
+ if (ret != LDB_SUCCESS) {
+ ldb_debug_set(ldb, LDB_DEBUG_FATAL,
+ "acl_read: %s check attr[%s] gives %s - %s\n",
+ ldb_dn_get_linearized(dn), attr_name,
+ ldb_strerror(ret), ldb_errstring(ldb));
+ return ret;
+ }
+
+ return LDB_SUCCESS;
+}
+
+/*
+ * Returns the attribute name for this particular level of a search operation
+ * parse-tree.
+ */
+static const char * parse_tree_get_attr(struct ldb_parse_tree *tree)
+{
+ const char *attr = NULL;
+
+ switch (tree->operation) {
+ case LDB_OP_EQUALITY:
+ case LDB_OP_GREATER:
+ case LDB_OP_LESS:
+ case LDB_OP_APPROX:
+ attr = tree->u.equality.attr;
+ break;
+ case LDB_OP_SUBSTRING:
+ attr = tree->u.substring.attr;
+ break;
+ case LDB_OP_PRESENT:
+ attr = tree->u.present.attr;
+ break;
+ case LDB_OP_EXTENDED:
+ attr = tree->u.extended.attr;
+ break;
+
+ /* we'll check LDB_OP_AND/_OR/_NOT children later on in the walk */
+ default:
+ break;
+ }
+ return attr;
+}
+
+/*
+ * Checks a single attribute in the search parse-tree to make sure the user has
+ * sufficient rights to view it.
+ */
+static int parse_tree_check_attr_access(struct ldb_parse_tree *tree,
+ void *private_context)
+{
+ struct parse_tree_aclread_ctx *ctx = NULL;
+ const char *attr_name = NULL;
+ bool is_public_info = false;
+ int ret;
+
+ ctx = (struct parse_tree_aclread_ctx *)private_context;
+
+ /*
+ * we can skip any further checking if we already know that this object
+ * shouldn't be visible in this user's search
+ */
+ if (ctx->suppress_result) {
+ return LDB_SUCCESS;
+ }
+
+ /* skip this level of the search-tree if it has no attribute to check */
+ attr_name = parse_tree_get_attr(tree);
+ if (attr_name == NULL) {
+ return LDB_SUCCESS;
+ }
+
+ ret = check_attr_access_rights(ctx->mem_ctx, attr_name, ctx->ac,
+ ctx->sd, ctx->objectclass, ctx->sid,
+ ctx->dn, &is_public_info);
+
+ /*
+ * if the user does not have the rights to view this attribute, then we
+ * should not return the object as a search result, i.e. act as if the
+ * object doesn't exist (for this particular user, at least)
+ */
+ if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) {
+
+ /*
+ * We make an exception for attribute=* searches involving the
+ * Public Information property-set. This allows searches like
+ * objectClass=* to return visible objects, even if the user
+ * doesn't have Read Property rights on the attribute
+ */
+ if (tree->operation == LDB_OP_PRESENT && is_public_info) {
+ return LDB_SUCCESS;
+ }
+
+ ctx->suppress_result = true;
+ return LDB_SUCCESS;
+ }
+
+ return ret;
+}
+
+/*
+ * Traverse the search-tree to check that the user has sufficient access rights
+ * to view all the attributes.
+ */
+static int check_search_ops_access(struct aclread_context *ac,
+ TALLOC_CTX *mem_ctx,
+ struct security_descriptor *sd,
+ const struct dsdb_class *objectclass,
+ struct dom_sid *sid, struct ldb_dn *dn,
+ bool *suppress_result)
+{
+ int ret;
+ struct parse_tree_aclread_ctx ctx = { 0 };
+ struct ldb_parse_tree *tree = ac->req->op.search.tree;
+
+ ctx.ac = ac;
+ ctx.mem_ctx = mem_ctx;
+ ctx.suppress_result = false;
+ ctx.sid = sid;
+ ctx.dn = dn;
+ ctx.sd = sd;
+ ctx.objectclass = objectclass;
+
+ /* walk the search tree, checking each attribute as we go */
+ ret = ldb_parse_tree_walk(tree, parse_tree_check_attr_access, &ctx);
+
+ /* return whether this search result should be hidden to this user */
+ *suppress_result = ctx.suppress_result;
+ return ret;
+}
+
static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
{
struct ldb_context *ldb;
@@ -112,6 +327,7 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
TALLOC_CTX *tmp_ctx;
uint32_t instanceType;
const struct dsdb_class *objectclass;
+ bool suppress_result = false;
ac = talloc_get_type(req->context, struct aclread_context);
ldb = ldb_module_get_ctx(ac->module);
@@ -277,6 +493,37 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
goto fail;
}
}
+
+ /*
+ * check access rights for the search attributes, as well as the
+ * attribute values actually being returned
+ */
+ ret = check_search_ops_access(ac, tmp_ctx, sd, objectclass, sid,
+ msg->dn, &suppress_result);
+ if (ret != LDB_SUCCESS) {
+ ldb_debug_set(ldb, LDB_DEBUG_FATAL,
+ "acl_read: %s check search ops %s - %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_strerror(ret), ldb_errstring(ldb));
+ goto fail;
+ }
+
+ if (suppress_result) {
+
+ /*
+ * As per the above logic, we strip replPropertyMetaData
+ * out of the msg so that the dirysync module will do
+ * what is needed (return just the objectGUID if it's,
+ * deleted, or remove the object if it is not).
+ */
+ if (ac->indirsync) {
+ ldb_msg_remove_attr(msg, "replPropertyMetaData");
+ } else {
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+ }
+ }
+
for (i=0; i < msg->num_elements; i++) {
if (!aclread_is_inaccessible(&msg->elements[i])) {
num_of_attrs++;