From 02db55b4074e0ceebb87a75105e8ef79c3dcf032 Mon Sep 17 00:00:00 2001 From: Jeremy Allison Date: Fri, 15 Jun 2018 15:07:17 -0700 Subject: [PATCH 01/20] CVE-2018-10858: libsmb: Ensure smbc_urlencode() can't overwrite passed in buffer. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13453 CVE-2018-10858: Insufficient input validation on client directory listing in libsmbclient. Signed-off-by: Jeremy Allison Reviewed-by: Ralph Boehme --- source3/libsmb/libsmb_path.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/source3/libsmb/libsmb_path.c b/source3/libsmb/libsmb_path.c index 01b0a61e483..ed70ab37550 100644 --- a/source3/libsmb/libsmb_path.c +++ b/source3/libsmb/libsmb_path.c @@ -173,8 +173,13 @@ smbc_urlencode(char *dest, } } - *dest++ = '\0'; - max_dest_len--; + if (max_dest_len == 0) { + /* Ensure we return -1 if no null termination. */ + return -1; + } + + *dest++ = '\0'; + max_dest_len--; return max_dest_len; } -- 2.11.0 From 011d25d5f653246770fa58b7dcff26740369c6ef Mon Sep 17 00:00:00 2001 From: Jeremy Allison Date: Fri, 15 Jun 2018 15:08:17 -0700 Subject: [PATCH 02/20] CVE-2018-10858: libsmb: Harden smbc_readdir_internal() against returns from malicious servers. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13453 CVE-2018-10858: Insufficient input validation on client directory listing in libsmbclient. Signed-off-by: Jeremy Allison Reviewed-by: Ralph Boehme --- source3/libsmb/libsmb_dir.c | 57 ++++++++++++++++++++++++++++++++++++++------ source3/libsmb/libsmb_path.c | 2 +- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/source3/libsmb/libsmb_dir.c b/source3/libsmb/libsmb_dir.c index 72441c46736..54c2bcb3c73 100644 --- a/source3/libsmb/libsmb_dir.c +++ b/source3/libsmb/libsmb_dir.c @@ -943,27 +943,47 @@ SMBC_closedir_ctx(SMBCCTX *context, } -static void +static int smbc_readdir_internal(SMBCCTX * context, struct smbc_dirent *dest, struct smbc_dirent *src, int max_namebuf_len) { if (smbc_getOptionUrlEncodeReaddirEntries(context)) { + int remaining_len; /* url-encode the name. get back remaining buffer space */ - max_namebuf_len = + remaining_len = smbc_urlencode(dest->name, src->name, max_namebuf_len); + /* -1 means no null termination. */ + if (remaining_len < 0) { + return -1; + } + /* We now know the name length */ dest->namelen = strlen(dest->name); + if (dest->namelen + 1 < 1) { + /* Integer wrap. */ + return -1; + } + + if (dest->namelen + 1 >= max_namebuf_len) { + /* Out of space for comment. */ + return -1; + } + /* Save the pointer to the beginning of the comment */ dest->comment = dest->name + dest->namelen + 1; + if (remaining_len < 1) { + /* No room for comment null termination. */ + return -1; + } + /* Copy the comment */ - strncpy(dest->comment, src->comment, max_namebuf_len - 1); - dest->comment[max_namebuf_len - 1] = '\0'; + strlcpy(dest->comment, src->comment, remaining_len); /* Save other fields */ dest->smbc_type = src->smbc_type; @@ -973,10 +993,21 @@ smbc_readdir_internal(SMBCCTX * context, } else { /* No encoding. Just copy the entry as is. */ + if (src->dirlen > max_namebuf_len) { + return -1; + } memcpy(dest, src, src->dirlen); + if (src->namelen + 1 < 1) { + /* Integer wrap */ + return -1; + } + if (src->namelen + 1 >= max_namebuf_len) { + /* Comment off the end. */ + return -1; + } dest->comment = (char *)(&dest->name + src->namelen + 1); } - + return 0; } /* @@ -988,6 +1019,7 @@ SMBC_readdir_ctx(SMBCCTX *context, SMBCFILE *dir) { int maxlen; + int ret; struct smbc_dirent *dirp, *dirent; TALLOC_CTX *frame = talloc_stackframe(); @@ -1037,7 +1069,12 @@ SMBC_readdir_ctx(SMBCCTX *context, dirp = &context->internal->dirent; maxlen = sizeof(context->internal->_dirent_name); - smbc_readdir_internal(context, dirp, dirent, maxlen); + ret = smbc_readdir_internal(context, dirp, dirent, maxlen); + if (ret == -1) { + errno = EINVAL; + TALLOC_FREE(frame); + return NULL; + } dir->dir_next = dir->dir_next->next; @@ -1095,6 +1132,7 @@ SMBC_getdents_ctx(SMBCCTX *context, */ while ((dirlist = dir->dir_next)) { + int ret; struct smbc_dirent *dirent; struct smbc_dirent *currentEntry = (struct smbc_dirent *)ndir; @@ -1109,8 +1147,13 @@ SMBC_getdents_ctx(SMBCCTX *context, /* Do urlencoding of next entry, if so selected */ dirent = &context->internal->dirent; maxlen = sizeof(context->internal->_dirent_name); - smbc_readdir_internal(context, dirent, + ret = smbc_readdir_internal(context, dirent, dirlist->dirent, maxlen); + if (ret == -1) { + errno = EINVAL; + TALLOC_FREE(frame); + return -1; + } reqd = dirent->dirlen; diff --git a/source3/libsmb/libsmb_path.c b/source3/libsmb/libsmb_path.c index ed70ab37550..5b53b386a67 100644 --- a/source3/libsmb/libsmb_path.c +++ b/source3/libsmb/libsmb_path.c @@ -173,7 +173,7 @@ smbc_urlencode(char *dest, } } - if (max_dest_len == 0) { + if (max_dest_len <= 0) { /* Ensure we return -1 if no null termination. */ return -1; } -- 2.11.0 From 49d940f8e335b8af6daf65ac6d3cce45db09ca8e Mon Sep 17 00:00:00 2001 From: Andrew Bartlett Date: Mon, 30 Jul 2018 14:00:18 +1200 Subject: [PATCH 03/20] CVE-2018-10918: cracknames: Fix DoS (NULL pointer de-ref) when not servicePrincipalName is set on a user This regression was introduced in Samba 4.7 by bug 12842 and in master git commit eb2e77970e41c1cb62c041877565e939c78ff52d. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13552 CVE-2018-10918: Denial of Service Attack on AD DC DRSUAPI server. Signed-off-by: Andrew Bartlett Reviewed-by: Gary Lockyer --- source4/dsdb/samdb/cracknames.c | 8 ++++++- source4/torture/drs/python/cracknames.py | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/source4/dsdb/samdb/cracknames.c b/source4/dsdb/samdb/cracknames.c index d43f510b949..3b215ac0ec9 100644 --- a/source4/dsdb/samdb/cracknames.c +++ b/source4/dsdb/samdb/cracknames.c @@ -1253,7 +1253,13 @@ static WERROR DsCrackNameOneFilter(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ return WERR_OK; } case DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL: { - if (result->elements[0].num_values > 1) { + struct ldb_message_element *el + = ldb_msg_find_element(result, + "servicePrincipalName"); + if (el == NULL) { + info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND; + return WERR_OK; + } else if (el->num_values > 1) { info1->status = DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE; return WERR_OK; } diff --git a/source4/torture/drs/python/cracknames.py b/source4/torture/drs/python/cracknames.py index d8c8ae53d60..9bf90f9c997 100644 --- a/source4/torture/drs/python/cracknames.py +++ b/source4/torture/drs/python/cracknames.py @@ -149,6 +149,44 @@ class DrsCracknamesTestCase(drs_base.DrsBaseTestCase): self.ldb_dc1.delete(user) + def test_NoSPNAttribute(self): + """ + Verifies that, if we try and cracknames with the desired output + being an SPN, it returns + DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE. + """ + username = "Cracknames_no_SPN" + user = "cn=%s,%s" % (username, self.ou) + + user_record = { + "dn": user, + "objectclass": "user", + "sAMAccountName" : username, + "userPrincipalName" : "test4@test.com", + "displayName" : "test4"} + + self.ldb_dc1.add(user_record) + + (result, ctr) = self._do_cracknames(user, + drsuapi.DRSUAPI_DS_NAME_FORMAT_FQDN_1779, + drsuapi.DRSUAPI_DS_NAME_FORMAT_GUID) + + self.assertEquals(ctr.count, 1) + self.assertEquals(ctr.array[0].status, + drsuapi.DRSUAPI_DS_NAME_STATUS_OK) + + user_guid = ctr.array[0].result_name + + (result, ctr) = self._do_cracknames(user_guid, + drsuapi.DRSUAPI_DS_NAME_FORMAT_GUID, + drsuapi.DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL) + + self.assertEquals(ctr.count, 1) + self.assertEquals(ctr.array[0].status, + drsuapi.DRSUAPI_DS_NAME_STATUS_NOT_FOUND) + + self.ldb_dc1.delete(user) + def _do_cracknames(self, name, format_offered, format_desired): req = drsuapi.DsNameRequest1() names = drsuapi.DsNameString() -- 2.11.0 From 12f97f9f69d3ace751c9b49f739aecc4e452dd35 Mon Sep 17 00:00:00 2001 From: Tim Beale Date: Thu, 19 Jul 2018 16:03:36 +1200 Subject: [PATCH 04/20] CVE-2018-10919 security: Move object-specific access checks into separate function Object-specific access checks refer to a specific section of the MS-ADTS, and the code closely matches the spec. We need to extend this logic to properly handle the Control-Access Right (CR), so it makes sense to split the logic out into its own function. This patch just moves the code, and should not alter the logic (apart from ading in the boolean grant_access return variable. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13434 Signed-off-by: Tim Beale --- libcli/security/access_check.c | 86 +++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/libcli/security/access_check.c b/libcli/security/access_check.c index b4c850b613e..b4e62441542 100644 --- a/libcli/security/access_check.c +++ b/libcli/security/access_check.c @@ -375,6 +375,57 @@ static const struct GUID *get_ace_object_type(struct security_ace *ace) } /** + * Evaluates access rights specified in a object-specific ACE for an AD object. + * This logic corresponds to MS-ADTS 5.1.3.3.3 Checking Object-Specific Access. + * @param[in] ace - the ACE being processed + * @param[in/out] tree - remaining_access gets updated for the tree + * @param[out] grant_access - set to true if the ACE grants sufficient access + * rights to the object/attribute + * @returns NT_STATUS_OK, unless access was denied + */ +static NTSTATUS check_object_specific_access(struct security_ace *ace, + struct object_tree *tree, + bool *grant_access) +{ + struct object_tree *node = NULL; + const struct GUID *type = NULL; + + *grant_access = false; + + /* + * check only in case we have provided a tree, + * the ACE has an object type and that type + * is in the tree + */ + type = get_ace_object_type(ace); + + if (!tree) { + return NT_STATUS_OK; + } + + if (!type) { + node = tree; + } else { + if (!(node = get_object_tree_by_GUID(tree, type))) { + return NT_STATUS_OK; + } + } + + if (ace->type == SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT) { + object_tree_modify_access(node, ace->access_mask); + if (node->remaining_access == 0) { + *grant_access = true; + return NT_STATUS_OK; + } + } else { + if (node->remaining_access & ace->access_mask){ + return NT_STATUS_ACCESS_DENIED; + } + } + return NT_STATUS_OK; +} + +/** * @brief Perform directoryservice (DS) related access checks for a given user * * Perform DS access checks for the user represented by its security_token, on @@ -405,8 +456,6 @@ NTSTATUS sec_access_check_ds(const struct security_descriptor *sd, { uint32_t i; uint32_t bits_remaining; - struct object_tree *node; - const struct GUID *type; struct dom_sid self_sid; dom_sid_parse(SID_NT_SELF, &self_sid); @@ -456,6 +505,8 @@ NTSTATUS sec_access_check_ds(const struct security_descriptor *sd, for (i=0; bits_remaining && i < sd->dacl->num_aces; i++) { struct dom_sid *trustee; struct security_ace *ace = &sd->dacl->aces[i]; + NTSTATUS status; + bool grant_access = false; if (ace->flags & SEC_ACE_FLAG_INHERIT_ONLY) { continue; @@ -486,34 +537,15 @@ NTSTATUS sec_access_check_ds(const struct security_descriptor *sd, break; case SEC_ACE_TYPE_ACCESS_DENIED_OBJECT: case SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT: - /* - * check only in case we have provided a tree, - * the ACE has an object type and that type - * is in the tree - */ - type = get_ace_object_type(ace); - - if (!tree) { - continue; - } + status = check_object_specific_access(ace, tree, + &grant_access); - if (!type) { - node = tree; - } else { - if (!(node = get_object_tree_by_GUID(tree, type))) { - continue; - } + if (!NT_STATUS_IS_OK(status)) { + return status; } - if (ace->type == SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT) { - object_tree_modify_access(node, ace->access_mask); - if (node->remaining_access == 0) { - return NT_STATUS_OK; - } - } else { - if (node->remaining_access & ace->access_mask){ - return NT_STATUS_ACCESS_DENIED; - } + if (grant_access) { + return NT_STATUS_OK; } break; default: /* Other ACE types not handled/supported */ -- 2.11.0 From 81865e8584a0f597650a9df31d49bad3e7549d26 Mon Sep 17 00:00:00 2001 From: Tim Beale Date: Fri, 20 Jul 2018 13:13:50 +1200 Subject: [PATCH 05/20] CVE-2018-10919 security: Add more comments to the object-specific access checks Reading the spec and then reading the code makes sense, but we could comment the code more so it makes sense on its own. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13434 Signed-off-by: Tim Beale --- libcli/security/access_check.c | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/libcli/security/access_check.c b/libcli/security/access_check.c index b4e62441542..93eb85def91 100644 --- a/libcli/security/access_check.c +++ b/libcli/security/access_check.c @@ -392,32 +392,46 @@ static NTSTATUS check_object_specific_access(struct security_ace *ace, *grant_access = false; - /* - * check only in case we have provided a tree, - * the ACE has an object type and that type - * is in the tree - */ - type = get_ace_object_type(ace); - + /* if no tree was supplied, we can't do object-specific access checks */ if (!tree) { return NT_STATUS_OK; } + /* Get the ObjectType GUID this ACE applies to */ + type = get_ace_object_type(ace); + + /* + * If the ACE doesn't have a type, then apply it to the whole tree, i.e. + * treat 'OA' ACEs as 'A' and 'OD' as 'D' + */ if (!type) { node = tree; } else { - if (!(node = get_object_tree_by_GUID(tree, type))) { + + /* skip it if the ACE's ObjectType GUID is not in the tree */ + node = get_object_tree_by_GUID(tree, type); + if (!node) { return NT_STATUS_OK; } } if (ace->type == SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT) { + + /* apply the access rights to this node, and any children */ object_tree_modify_access(node, ace->access_mask); + + /* + * Currently all nodes in the tree request the same access mask, + * so we can use any node to check if processing this ACE now + * means the requested access has been granted + */ if (node->remaining_access == 0) { *grant_access = true; return NT_STATUS_OK; } } else { + + /* this ACE denies access to the requested object/attribute */ if (node->remaining_access & ace->access_mask){ return NT_STATUS_ACCESS_DENIED; } -- 2.11.0 From 49920e7b218770433708cd5889bbf1f9b51d30c0 Mon Sep 17 00:00:00 2001 From: Tim Beale Date: Mon, 9 Jul 2018 15:57:59 +1200 Subject: [PATCH 06/20] CVE-2018-10919 tests: Add tests for guessing confidential attributes Adds tests that assert that a confidential attribute cannot be guessed by an unprivileged user through wildcard DB searches. The tests basically consist of a set of DB searches/assertions that get run for: - basic searches against a confidential attribute - confidential attributes that get overridden by giving access to the user via an ACE (run against a variety of ACEs) - protecting a non-confidential attribute via an ACL that denies read- access (run against a variety of ACEs) - querying confidential attributes via the dirsync controls These tests all pass when run against a Windows Dc and all fail against a Samba DC. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13434 Signed-off-by: Tim Beale --- selftest/knownfail.d/confidential_attr | 15 + source4/dsdb/tests/python/confidential_attr.py | 920 +++++++++++++++++++++++++ source4/selftest/tests.py | 3 + 3 files changed, 938 insertions(+) create mode 100644 selftest/knownfail.d/confidential_attr create mode 100755 source4/dsdb/tests/python/confidential_attr.py diff --git a/selftest/knownfail.d/confidential_attr b/selftest/knownfail.d/confidential_attr new file mode 100644 index 00000000000..7a2f2aada57 --- /dev/null +++ b/selftest/knownfail.d/confidential_attr @@ -0,0 +1,15 @@ +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/tests/python/confidential_attr.py b/source4/dsdb/tests/python/confidential_attr.py new file mode 100755 index 00000000000..c3909d5fc2e --- /dev/null +++ b/source4/dsdb/tests/python/confidential_attr.py @@ -0,0 +1,920 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Tests that confidential attributes (or attributes protected by a ACL that +# denies read access) cannot be guessed through wildcard DB searches. +# +# Copyright (C) Catalyst.Net Ltd +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import optparse +import sys +sys.path.insert(0, "bin/python") + +import samba +import os +from samba.tests.subunitrun import SubunitOptions, TestProgram +import samba.getopt as options +from ldb import SCOPE_BASE, SCOPE_SUBTREE +from samba.dsdb import SEARCH_FLAG_CONFIDENTIAL +from ldb import Message, MessageElement, Dn +from ldb import FLAG_MOD_REPLACE, FLAG_MOD_ADD +from samba.auth import system_session +from samba import gensec, sd_utils +from samba.samdb import SamDB +from samba.credentials import Credentials, DONT_USE_KERBEROS +import samba.tests +from samba.tests import delete_force +import samba.dsdb + +parser = optparse.OptionParser("confidential_attr.py [options] ") +sambaopts = options.SambaOptions(parser) +parser.add_option_group(sambaopts) +parser.add_option_group(options.VersionOptions(parser)) + +# use command line creds if available +credopts = options.CredentialsOptions(parser) +parser.add_option_group(credopts) +subunitopts = SubunitOptions(parser) +parser.add_option_group(subunitopts) + +opts, args = parser.parse_args() + +if len(args) < 1: + parser.print_usage() + sys.exit(1) + +host = args[0] +if "://" not in host: + ldaphost = "ldap://%s" % host +else: + ldaphost = host + start = host.rindex("://") + host = host.lstrip(start + 3) + +lp = sambaopts.get_loadparm() +creds = credopts.get_credentials(lp) +creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL) + +# When a user does not have access rights to view the objects' attributes, +# Windows and Samba behave slightly differently. +# A windows DC will always act as if the hidden attribute doesn't exist AT ALL +# (for an unprivileged user). So, even for a user that lacks access rights, +# the inverse/'!' queries should return ALL objects. This is similar to the +# kludgeaclredacted behaviour on Samba. +# However, on Samba (for implementation simplicity) we never return a matching +# result for an unprivileged user. +# Either approach is OK, so long as it gets applied consistently and we don't +# disclose any sensitive details by varying what gets returned by the search. +DC_MODE_RETURN_NONE = 0 +DC_MODE_RETURN_ALL = 1 + +# +# Tests start here +# +class ConfidentialAttrCommon(samba.tests.TestCase): + + def setUp(self): + super(ConfidentialAttrCommon, self).setUp() + + self.ldb_admin = SamDB(ldaphost, credentials=creds, + session_info=system_session(lp), lp=lp) + self.user_pass = "samba123@" + self.base_dn = self.ldb_admin.domain_dn() + self.schema_dn = self.ldb_admin.get_schema_basedn() + self.sd_utils = sd_utils.SDUtils(self.ldb_admin) + + # the tests work by setting the 'Confidential' bit in the searchFlags + # for an existing schema attribute. This only works against Windows if + # the systemFlags does not have FLAG_SCHEMA_BASE_OBJECT set for the + # schema attribute being modified. There are only a few attributes that + # meet this criteria (most of which only apply to 'user' objects) + self.conf_attr = "homePostalAddress" + self.conf_attr_cn = "CN=Address-Home" + # schemaIdGuid for homePostalAddress: + self.conf_attr_guid = "16775781-47f3-11d1-a9c3-0000f80367c1" + self.conf_attr_sec_guid = "77b5b886-944a-11d1-aebd-0000f80367c1" + self.attr_dn = "{},{}".format(self.conf_attr_cn, self.schema_dn) + + userou = "OU=conf-attr-test" + self.ou = "{},{}".format(userou, self.base_dn) + self.ldb_admin.create_ou(self.ou) + + # use a common username prefix, so we can use sAMAccountName=CATC-* as + # a search filter to only return the users we're interested in + self.user_prefix = "catc-" + + # add a test object with this attribute set + self.conf_value = "abcdef" + self.conf_user = "{}conf-user".format(self.user_prefix) + self.ldb_admin.newuser(self.conf_user, self.user_pass, userou=userou) + self.conf_dn = self.get_user_dn(self.conf_user) + self.add_attr(self.conf_dn, self.conf_attr, self.conf_value) + + # add a sneaky user that will try to steal our secrets + self.user = "{}sneaky-user".format(self.user_prefix) + self.ldb_admin.newuser(self.user, self.user_pass, userou=userou) + self.ldb_user = self.get_ldb_connection(self.user, self.user_pass) + + self.all_users = [self.user, self.conf_user] + + # add some other users that also have confidential attributes, so we can + # check we don't disclose their details, particularly in '!' searches + for i in range(1, 3): + username = "{}other-user{}".format(self.user_prefix, i) + self.ldb_admin.newuser(username, self.user_pass, userou=userou) + userdn = self.get_user_dn(username) + self.add_attr(userdn, self.conf_attr, "xyz{}".format(i)) + self.all_users.append(username) + + # there are 4 users in the OU, plus the OU itself + self.test_dn = self.ou + self.total_objects = len(self.all_users) + 1 + self.objects_with_attr = 3 + + # sanity-check the flag is not already set (this'll cause problems if + # previous test run didn't clean up properly) + search_flags = self.get_attr_search_flags(self.attr_dn) + self.assertTrue(int(search_flags) & SEARCH_FLAG_CONFIDENTIAL == 0, + "{} searchFlags already {}".format(self.conf_attr, + search_flags)) + + def tearDown(self): + super(ConfidentialAttrCommon, self).tearDown() + self.ldb_admin.delete(self.ou, ["tree_delete:1"]) + + def add_attr(self, dn, attr, value): + m = Message() + m.dn = Dn(self.ldb_admin, dn) + m[attr] = MessageElement(value, FLAG_MOD_ADD, attr) + self.ldb_admin.modify(m) + + def set_schema_update_now(self): + ldif = """ +dn: +changetype: modify +add: schemaUpdateNow +schemaUpdateNow: 1 +""" + self.ldb_admin.modify_ldif(ldif) + + def set_attr_search_flags(self, attr_dn, flags): + """Modifies the searchFlags for an object in the schema""" + m = Message() + m.dn = Dn(self.ldb_admin, attr_dn) + m['searchFlags'] = MessageElement(flags, FLAG_MOD_REPLACE, + 'searchFlags') + self.ldb_admin.modify(m) + + # note we have to update the schema for this change to take effect (on + # Windows, at least) + self.set_schema_update_now() + + def get_attr_search_flags(self, attr_dn): + """Marks the attribute under test as being confidential""" + res = self.ldb_admin.search(attr_dn, scope=SCOPE_BASE, + attrs=['searchFlags']) + return res[0]['searchFlags'][0] + + def make_attr_confidential(self): + """Marks the attribute under test as being confidential""" + + # work out the original 'searchFlags' value before we overwrite it + old_value = self.get_attr_search_flags(self.attr_dn) + + self.set_attr_search_flags(self.attr_dn, str(SEARCH_FLAG_CONFIDENTIAL)) + + # reset the value after the test completes + self.addCleanup(self.set_attr_search_flags, self.attr_dn, old_value) + + # The behaviour of the DC can differ in some cases, depending on whether + # we're talking to a Windows DC or a Samba DC + def guess_dc_mode(self): + # if we're in selftest, we can be pretty sure it's a Samba DC + if os.environ.get('SAMBA_SELFTEST') == '1': + return DC_MODE_RETURN_NONE + + searches = self.get_negative_match_all_searches() + res = self.ldb_user.search(self.test_dn, expression=searches[0], + scope=SCOPE_SUBTREE) + + # we default to DC_MODE_RETURN_NONE (samba).Update this if it + # looks like we're talking to a Windows DC + if len(res) == self.total_objects: + return DC_MODE_RETURN_ALL + + # otherwise assume samba DC behaviour + return DC_MODE_RETURN_NONE + + def get_user_dn(self, name): + return "CN={},{}".format(name, self.ou) + + def get_user_sid_string(self, username): + user_dn = self.get_user_dn(username) + user_sid = self.sd_utils.get_object_sid(user_dn) + return str(user_sid) + + def get_ldb_connection(self, target_username, target_password): + creds_tmp = Credentials() + creds_tmp.set_username(target_username) + creds_tmp.set_password(target_password) + creds_tmp.set_domain(creds.get_domain()) + creds_tmp.set_realm(creds.get_realm()) + creds_tmp.set_workstation(creds.get_workstation()) + features = creds_tmp.get_gensec_features() | gensec.FEATURE_SEAL + creds_tmp.set_gensec_features(features) + creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) + ldb_target = SamDB(url=ldaphost, credentials=creds_tmp, lp=lp) + return ldb_target + + def assert_not_in_result(self, res, exclude_dn): + for msg in res: + self.assertNotEqual(msg.dn, exclude_dn, + "Search revealed object {}".format(exclude_dn)) + + def assert_search_result(self, expected_num, expr, samdb): + + # try asking for different attributes back: None/all, the confidential + # attribute itself, and a random unrelated attribute + attr_filters = [None, ["*"], [self.conf_attr], ['name']] + for attr in attr_filters: + res = samdb.search(self.test_dn, expression=expr, + scope=SCOPE_SUBTREE, attrs=attr) + self.assertTrue(len(res) == expected_num, + "%u results, not %u for search %s, attr %s" % + (len(res), expected_num, expr, str(attr))) + + # return a selection of searches that match exactly against the test object + def get_exact_match_searches(self): + first_char = self.conf_value[:1] + last_char = self.conf_value[-1:] + test_attr = self.conf_attr + + searches = [ + # search for the attribute using a sub-string wildcard + # (which could reveal the attribute's actual value) + "({}={}*)".format(test_attr, first_char), + "({}=*{})".format(test_attr, last_char), + + # sanity-check equality against an exact match on value + "({}={})".format(test_attr, self.conf_value), + + # '~=' searches don't work against Samba + # sanity-check an approx search against an exact match on value + # "({}~={})".format(test_attr, self.conf_value), + + # check wildcard in an AND search... + "(&({}={}*)(objectclass=*))".format(test_attr, first_char), + + # ...an OR search (against another term that will never match) + "(|({}={}*)(objectclass=banana))".format(test_attr, first_char)] + + return searches + + # return searches that match any object with the attribute under test + def get_match_all_searches(self): + searches = [ + # check a full wildcard against the confidential attribute + # (which could reveal the attribute's presence/absence) + "({}=*)".format(self.conf_attr), + + # check wildcard in an AND search... + "(&(objectclass=*)({}=*))".format(self.conf_attr), + + # ...an OR search (against another term that will never match) + "(|(objectclass=banana)({}=*))".format(self.conf_attr), + + # check <=, and >= expressions that would normally find a match + "({}>=0)".format(self.conf_attr), + "({}<=ZZZZZZZZZZZ)".format(self.conf_attr)] + + return searches + + def assert_conf_attr_searches(self, has_rights_to=0, samdb=None): + """Check searches against the attribute under test work as expected""" + + if samdb is None: + samdb = self.ldb_user + + if has_rights_to == "all": + has_rights_to = self.objects_with_attr + + # these first few searches we just expect to match against the one + # object under test that we're trying to guess the value of + expected_num = 1 if has_rights_to > 0 else 0 + for search in self.get_exact_match_searches(): + self.assert_search_result(expected_num, search, samdb) + + # these next searches will match any objects we have rights to see + expected_num = has_rights_to + for search in self.get_match_all_searches(): + self.assert_search_result(expected_num, search, samdb) + + # The following are double negative searches (i.e. NOT non-matching- + # condition) which will therefore match ALL objects, including the test + # object(s). + def get_negative_match_all_searches(self): + first_char = self.conf_value[:1] + last_char = self.conf_value[-1:] + not_first_char = chr(ord(first_char) + 1) + not_last_char = chr(ord(last_char) + 1) + + searches = [ + "(!({}={}*))".format(self.conf_attr, not_first_char), + "(!({}=*{}))".format(self.conf_attr, not_last_char)] + return searches + + # the following searches will not match against the test object(s). So + # a user with sufficient rights will see an inverse sub-set of objects. + # (An unprivileged user would either see all objects on Windows, or no + # objects on Samba) + def get_inverse_match_searches(self): + first_char = self.conf_value[:1] + last_char = self.conf_value[-1:] + searches = [ + "(!({}={}*))".format(self.conf_attr, first_char), + "(!({}=*{}))".format(self.conf_attr, last_char)] + return searches + + def negative_searches_all_rights(self, total_objects=None): + expected_results = {} + + if total_objects is None: + total_objects = self.total_objects + + # these searches should match ALL objects (including the OU) + for search in self.get_negative_match_all_searches(): + expected_results[search] = total_objects + + # a ! wildcard should only match the objects without the attribute + search = "(!({}=*))".format(self.conf_attr) + expected_results[search] = total_objects - self.objects_with_attr + + # whereas the inverse searches should match all objects *except* the + # one under test + for search in self.get_inverse_match_searches(): + expected_results[search] = total_objects - 1 + + return expected_results + + # Returns the expected negative (i.e. '!') search behaviour when talking to + # a DC with DC_MODE_RETURN_ALL behaviour, i.e. we assert that users + # without rights always see ALL objects in '!' searches + def negative_searches_return_all(self, has_rights_to=0, + total_objects=None): + """Asserts user without rights cannot see objects in '!' searches""" + expected_results = {} + + if total_objects is None: + total_objects = self.total_objects + + # Windows 'hides' objects by always returning all of them, so negative + # searches that match all objects will simply return all objects + for search in self.get_negative_match_all_searches(): + expected_results[search] = total_objects + + # if the search is matching on an inverse subset (everything except the + # object under test), the + inverse_searches = self.get_inverse_match_searches() + inverse_searches += ["(!({}=*))".format(self.conf_attr)] + + for search in inverse_searches: + expected_results[search] = total_objects - has_rights_to + + return expected_results + + # Returns the expected negative (i.e. '!') search behaviour when talking to + # a DC with DC_MODE_RETURN_NONE behaviour, i.e. we assert that users + # without rights cannot see objects in '!' searches at all + def negative_searches_return_none(self, has_rights_to=0): + expected_results = {} + + # the 'match-all' searches should only return the objects we have + # access rights to (if any) + for search in self.get_negative_match_all_searches(): + expected_results[search] = has_rights_to + + # for inverse matches, we should NOT be told about any objects at all + inverse_searches = self.get_inverse_match_searches() + inverse_searches += ["(!({}=*))".format(self.conf_attr)] + for search in inverse_searches: + expected_results[search] = 0 + + return expected_results + + # Returns the expected negative (i.e. '!') search behaviour. This varies + # depending on what type of DC we're talking to (i.e. Windows or Samba) + # and what access rights the user has + def negative_search_expected_results(self, has_rights_to, dc_mode, + total_objects=None): + + if has_rights_to == "all": + expect_results = self.negative_searches_all_rights(total_objects) + + # if it's a Samba DC, we only expect the 'match-all' searches to return + # the objects that we have access rights to (all others are hidden). + # Whereas Windows 'hides' the objects by always returning all of them + elif dc_mode == DC_MODE_RETURN_NONE: + expect_results = self.negative_searches_return_none(has_rights_to) + else: + expect_results = self.negative_searches_return_all(has_rights_to, + total_objects) + return expect_results + + def assert_negative_searches(self, has_rights_to=0, + dc_mode=DC_MODE_RETURN_NONE, samdb=None): + """Asserts user without rights cannot see objects in '!' searches""" + + if samdb is None: + samdb = self.ldb_user + + # build a dictionary of key=search-expr, value=expected_num assertions + expected_results = self.negative_search_expected_results(has_rights_to, + dc_mode) + + for search, expected_num in expected_results.items(): + self.assert_search_result(expected_num, search, samdb) + + def assert_attr_returned(self, expect_attr, samdb, attrs): + # does a query that should always return a successful result, and + # checks whether the confidential attribute is present + res = samdb.search(self.conf_dn, expression="(objectClass=*)", + scope=SCOPE_SUBTREE, attrs=attrs) + self.assertTrue(len(res) == 1) + + attr_returned = False + for msg in res: + if self.conf_attr in msg: + attr_returned = True + self.assertEqual(expect_attr, attr_returned) + + def assert_attr_visible(self, expect_attr, samdb=None): + if samdb is None: + samdb = self.ldb_user + + # sanity-check confidential attribute is/isn't returned as expected + # based on the filter attributes we ask for + self.assert_attr_returned(expect_attr, samdb, attrs=None) + self.assert_attr_returned(expect_attr, samdb, attrs=["*"]) + self.assert_attr_returned(expect_attr, samdb, attrs=[self.conf_attr]) + + # filtering on a different attribute should never return the conf_attr + self.assert_attr_returned(expect_attr=False, samdb=samdb, + attrs=['name']) + + def assert_attr_visible_to_admin(self): + # sanity-check the admin user can always see the confidential attribute + self.assert_conf_attr_searches(has_rights_to="all", samdb=self.ldb_admin) + self.assert_negative_searches(has_rights_to="all", samdb=self.ldb_admin) + self.assert_attr_visible(expect_attr=True, samdb=self.ldb_admin) + + +class ConfidentialAttrTest(ConfidentialAttrCommon): + def test_basic_search(self): + """Basic test confidential attributes aren't disclosed via searches""" + + # check we can see a non-confidential attribute in a basic searches + self.assert_conf_attr_searches(has_rights_to="all") + self.assert_negative_searches(has_rights_to="all") + self.assert_attr_visible(expect_attr=True) + + # now make the attribute confidential. Repeat the tests and check that + # an ordinary user can't see the attribute, or indirectly match on the + # attribute via the search expression + self.make_attr_confidential() + + self.assert_conf_attr_searches(has_rights_to=0) + dc_mode = self.guess_dc_mode() + self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode) + self.assert_attr_visible(expect_attr=False) + + # sanity-check we haven't hidden the attribute from the admin as well + self.assert_attr_visible_to_admin() + + def _test_search_with_allow_acl(self, allow_ace): + """Checks a ACE with 'CR' rights can override a confidential attr""" + # make the test attribute confidential and check user can't see it + self.make_attr_confidential() + + self.assert_conf_attr_searches(has_rights_to=0) + dc_mode = self.guess_dc_mode() + self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode) + self.assert_attr_visible(expect_attr=False) + + # apply the allow ACE to the object under test + self.sd_utils.dacl_add_ace(self.conf_dn, allow_ace) + + # the user should now be able to see the attribute for the one object + # we gave it rights to + self.assert_conf_attr_searches(has_rights_to=1) + self.assert_negative_searches(has_rights_to=1, dc_mode=dc_mode) + self.assert_attr_visible(expect_attr=True) + + # sanity-check the admin can still see the attribute + self.assert_attr_visible_to_admin() + + def test_search_with_attr_acl_override(self): + """Make the confidential attr visible via an OA attr ACE""" + + # set the SEC_ADS_CONTROL_ACCESS bit ('CR') for the user for the + # attribute under test, so the user can see it once more + user_sid = self.get_user_sid_string(self.user) + ace = "(OA;;CR;{};;{})".format(self.conf_attr_guid, user_sid) + + self._test_search_with_allow_acl(ace) + + def test_search_with_propset_acl_override(self): + """Make the confidential attr visible via a Property-set ACE""" + + # set the SEC_ADS_CONTROL_ACCESS bit ('CR') for the user for the + # property-set containing the attribute under test (i.e. the + # attributeSecurityGuid), so the user can see it once more + user_sid = self.get_user_sid_string(self.user) + ace = "(OA;;CR;{};;{})".format(self.conf_attr_sec_guid, user_sid) + + self._test_search_with_allow_acl(ace) + + def test_search_with_acl_override(self): + """Make the confidential attr visible via a general 'allow' ACE""" + + # set the allow SEC_ADS_CONTROL_ACCESS bit ('CR') for the user + user_sid = self.get_user_sid_string(self.user) + ace = "(A;;CR;;;{})".format(user_sid) + + self._test_search_with_allow_acl(ace) + + def test_search_with_blanket_oa_acl(self): + """Make the confidential attr visible via a non-specific OA ACE""" + + # this just checks that an Object Access (OA) ACE without a GUID + # specified will work the same as an 'Access' (A) ACE + user_sid = self.get_user_sid_string(self.user) + ace = "(OA;;CR;;;{})".format(user_sid) + + self._test_search_with_allow_acl(ace) + + def _test_search_with_neutral_acl(self, neutral_ace): + """Checks that a user does NOT gain access via an unrelated ACE""" + + # make the test attribute confidential and check user can't see it + self.make_attr_confidential() + + self.assert_conf_attr_searches(has_rights_to=0) + dc_mode = self.guess_dc_mode() + self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode) + self.assert_attr_visible(expect_attr=False) + + # apply the ACE to the object under test + self.sd_utils.dacl_add_ace(self.conf_dn, neutral_ace) + + # this should make no difference to the user's ability to see the attr + self.assert_conf_attr_searches(has_rights_to=0) + self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode) + self.assert_attr_visible(expect_attr=False) + + # sanity-check the admin can still see the attribute + self.assert_attr_visible_to_admin() + + def test_search_with_neutral_acl(self): + """Give the user all rights *except* CR for any attributes""" + + # give the user all rights *except* CR and check it makes no difference + user_sid = self.get_user_sid_string(self.user) + ace = "(A;;RPWPCCDCLCLORCWOWDSDDTSW;;;{})".format(user_sid) + self._test_search_with_neutral_acl(ace) + + def test_search_with_neutral_attr_acl(self): + """Give the user all rights *except* CR for the attribute under test""" + + # giving user all OA rights *except* CR should make no difference + user_sid = self.get_user_sid_string(self.user) + rights = "RPWPCCDCLCLORCWOWDSDDTSW" + ace = "(OA;;{};{};;{})".format(rights, self.conf_attr_guid, user_sid) + self._test_search_with_neutral_acl(ace) + + def test_search_with_neutral_cr_acl(self): + """Give the user CR rights for *another* unrelated attribute""" + + # giving user object-access CR rights to an unrelated attribute + user_sid = self.get_user_sid_string(self.user) + # use the GUID for sAMAccountName here (for no particular reason) + unrelated_attr = "3e0abfd0-126a-11d0-a060-00aa006c33ed" + ace = "(OA;;CR;{};;{})".format(unrelated_attr, user_sid) + self._test_search_with_neutral_acl(ace) + + +# Check that a Deny ACL on an attribute doesn't reveal confidential info +class ConfidentialAttrTestDenyAcl(ConfidentialAttrCommon): + + def assert_not_in_result(self, res, exclude_dn): + for msg in res: + self.assertNotEqual(msg.dn, exclude_dn, + "Search revealed object {}".format(exclude_dn)) + + # deny ACL tests are slightly different as we are only denying access to + # the one object under test (rather than any objects with that attribute). + # Therefore we need an extra check that we don't reveal the test object + # in the search, if we're not supposed to + def assert_search_result(self, expected_num, expr, samdb, + excl_testobj=False): + + # try asking for different attributes back: None/all, the confidential + # attribute itself, and a random unrelated attribute + attr_filters = [None, ["*"], [self.conf_attr], ['name']] + for attr in attr_filters: + res = samdb.search(self.test_dn, expression=expr, + scope=SCOPE_SUBTREE, attrs=attr) + self.assertTrue(len(res) == expected_num, + "%u results, not %u for search %s, attr %s" % + (len(res), expected_num, expr, str(attr))) + + # assert we haven't revealed the hidden test-object + if excl_testobj: + self.assert_not_in_result(res, exclude_dn=self.conf_dn) + + # we make a few tweaks to the regular version of this function to cater to + # denying specifically one object via an ACE + def assert_conf_attr_searches(self, has_rights_to=0, samdb=None): + """Check searches against the attribute under test work as expected""" + + if samdb is None: + samdb = self.ldb_user + + # make sure the test object is not returned if we've been denied rights + # to it via an ACE + excl_testobj = True if has_rights_to == "deny-one" else False + + # these first few searches we just expect to match against the one + # object under test that we're trying to guess the value of + expected_num = 1 if has_rights_to == "all" else 0 + + for search in self.get_exact_match_searches(): + self.assert_search_result(expected_num, search, samdb, + excl_testobj) + + # these next searches will match any objects with the attribute that + # we have rights to see (i.e. all except the object under test) + if has_rights_to == "all": + expected_num = self.objects_with_attr + elif has_rights_to == "deny-one": + expected_num = self.objects_with_attr - 1 + + for search in self.get_match_all_searches(): + self.assert_search_result(expected_num, search, samdb, + excl_testobj) + + def negative_searches_return_none(self, has_rights_to=0): + expected_results = {} + + # on Samba we will see the objects we have rights to, but the one we + # are denied access to will be hidden + searches = self.get_negative_match_all_searches() + searches += self.get_inverse_match_searches() + for search in searches: + expected_results[search] = self.total_objects - 1 + + # The wildcard returns the objects without this attribute as normal. + search = "(!({}=*))".format(self.conf_attr) + expected_results[search] = self.total_objects - self.objects_with_attr + return expected_results + + def negative_searches_return_all(self, has_rights_to=0, + total_objects=None): + expected_results = {} + + # When a user lacks access rights to an object, Windows 'hides' it in + # '!' searches by always returning it, regardless of whether it matches + searches = self.get_negative_match_all_searches() + searches += self.get_inverse_match_searches() + for search in searches: + expected_results[search] = self.total_objects + + # in the wildcard case, the one object we don't have rights to gets + # bundled in with the objects that don't have the attribute at all + search = "(!({}=*))".format(self.conf_attr) + has_rights_to = self.objects_with_attr - 1 + expected_results[search] = self.total_objects - has_rights_to + return expected_results + + def assert_negative_searches(self, has_rights_to=0, + dc_mode=DC_MODE_RETURN_NONE, samdb=None): + """Asserts user without rights cannot see objects in '!' searches""" + + if samdb is None: + samdb = self.ldb_user + + # As the deny ACL is only denying access to one particular object, add + # an extra check that the denied object is not returned. (We can only + # assert this if the '!'/negative search behaviour is to suppress any + # objects we don't have access rights to) + excl_testobj = False + if has_rights_to != "all" and dc_mode == DC_MODE_RETURN_NONE: + excl_testobj = True + + # build a dictionary of key=search-expr, value=expected_num assertions + expected_results = self.negative_search_expected_results(has_rights_to, + dc_mode) + + for search, expected_num in expected_results.items(): + self.assert_search_result(expected_num, search, samdb, + excl_testobj=excl_testobj) + + def _test_search_with_deny_acl(self, ace): + # check the user can see the attribute initially + self.assert_conf_attr_searches(has_rights_to="all") + self.assert_negative_searches(has_rights_to="all") + self.assert_attr_visible(expect_attr=True) + + # add the ACE that denies access to the attr under test + self.sd_utils.dacl_add_ace(self.conf_dn, ace) + + # the user shouldn't be able to see the attribute anymore + self.assert_conf_attr_searches(has_rights_to="deny-one") + dc_mode = self.guess_dc_mode() + self.assert_negative_searches(has_rights_to="deny-one", + dc_mode=dc_mode) + self.assert_attr_visible(expect_attr=False) + + # sanity-check we haven't hidden the attribute from the admin as well + self.assert_attr_visible_to_admin() + + def test_search_with_deny_attr_acl(self): + """Checks a deny ACE works the same way as a confidential attribute""" + + # add an ACE that denies the user Read Property (RP) access to the attr + # (which is similar to making the attribute confidential) + user_sid = self.get_user_sid_string(self.user) + ace = "(OD;;RP;{};;{})".format(self.conf_attr_guid, user_sid) + + # check the user cannot see the attribute anymore + self._test_search_with_deny_acl(ace) + + def test_search_with_deny_acl(self): + """Checks a blanket deny ACE denies access to an object's attributes""" + + # add an blanket deny ACE for Read Property (RP) rights + user_dn = self.get_user_dn(self.user) + user_sid = self.sd_utils.get_object_sid(user_dn) + ace = "(D;;RP;;;{})".format(str(user_sid)) + + # check the user cannot see the attribute anymore + self._test_search_with_deny_acl(ace) + + def test_search_with_deny_propset_acl(self): + """Checks a deny ACE on the attribute's Property-Set""" + + # add an blanket deny ACE for Read Property (RP) rights + user_sid = self.get_user_sid_string(self.user) + ace = "(OD;;RP;{};;{})".format(self.conf_attr_sec_guid, user_sid) + + # check the user cannot see the attribute anymore + self._test_search_with_deny_acl(ace) + + def test_search_with_blanket_oa_deny_acl(self): + """Checks a non-specific 'OD' ACE works the same as a 'D' ACE""" + + # this just checks that adding a 'Object Deny' (OD) ACE without + # specifying a GUID will work the same way as a 'Deny' (D) ACE + user_sid = self.get_user_sid_string(self.user) + ace = "(OD;;RP;;;{})".format(user_sid) + + # check the user cannot see the attribute anymore + self._test_search_with_deny_acl(ace) + + +# Check that using the dirsync controls doesn't reveal confidential attributes +class ConfidentialAttrTestDirsync(ConfidentialAttrCommon): + + def setUp(self): + super(ConfidentialAttrTestDirsync, self).setUp() + self.dirsync = ["dirsync:1:1:1000"] + + def assert_search_result(self, expected_num, expr, samdb, base_dn=None): + + # Note dirsync must always search on the partition base DN + if base_dn is None: + base_dn = self.base_dn + + attr_filters = [None, ["*"], ["name"]] + + # Note dirsync behaviour is slighty different for the attribute under + # test - when you have full access rights, it only returns the objects + # that actually have this attribute (i.e. it doesn't return an empty + # message with just the DN). So we add the 'name' attribute into the + # attribute filter to avoid complicating our assertions further + attr_filters += [[self.conf_attr, "name"]] + for attr in attr_filters: + res = samdb.search(base_dn, expression=expr, scope=SCOPE_SUBTREE, + attrs=attr, controls=self.dirsync) + + self.assertTrue(len(res) == expected_num, + "%u results, not %u for search %s, attr %s" % + (len(res), expected_num, expr, str(attr))) + + + def assert_attr_returned(self, expect_attr, samdb, attrs, + no_result_ok=False): + + # When using dirsync, the base DN we search on needs to be a naming + # context. Add an extra filter to ignore all the objects we aren't + # interested in + expr = "(&(samaccountname={})(!(isDeleted=*)))".format(self.conf_user) + res = samdb.search(self.base_dn, expression=expr, scope=SCOPE_SUBTREE, + attrs=attrs, controls=self.dirsync) + self.assertTrue(len(res) == 1 or no_result_ok) + + attr_returned = False + for msg in res: + if self.conf_attr in msg and len(msg[self.conf_attr]) > 0: + attr_returned = True + self.assertEqual(expect_attr, attr_returned) + + def assert_attr_visible(self, expect_attr, samdb=None): + if samdb is None: + samdb = self.ldb_user + + # sanity-check confidential attribute is/isn't returned as expected + # based on the filter attributes we ask for + self.assert_attr_returned(expect_attr, samdb, attrs=None) + self.assert_attr_returned(expect_attr, samdb, attrs=["*"]) + + if expect_attr: + self.assert_attr_returned(expect_attr, samdb, + attrs=[self.conf_attr]) + else: + # The behaviour with dirsync when asking solely for an attribute + # that you don't have rights to is a bit strange. Samba returns + # no result rather than an empty message with just the DN. + # Presumably this is due to dirsync module behaviour. It's not + # disclosive in that the DC behaves the same way as if you asked + # for a garbage/non-existent attribute + self.assert_attr_returned(expect_attr, samdb, + attrs=[self.conf_attr], + no_result_ok=True) + self.assert_attr_returned(expect_attr, samdb, + attrs=["garbage"], no_result_ok=True) + + # filtering on a different attribute should never return the conf_attr + self.assert_attr_returned(expect_attr=False, samdb=samdb, + attrs=['name']) + + def assert_negative_searches(self, has_rights_to=0, + dc_mode=DC_MODE_RETURN_NONE, samdb=None): + """Asserts user without rights cannot see objects in '!' searches""" + + if samdb is None: + samdb = self.ldb_user + + # because we need to search on the base DN when using the dirsync + # controls, we need an extra filter for the inverse ('!') search, + # so we don't get thousands of objects returned + extra_filter = \ + "(samaccountname={}*)(!(isDeleted=*))".format(self.user_prefix) + + # because of this extra filter, the total objects we expect here only + # includes the user objects (not the parent OU) + total_objects = len(self.all_users) + expected_results = self.negative_search_expected_results(has_rights_to, + dc_mode, + total_objects) + + for search, expected_num in expected_results.items(): + search = "(&{}{})".format(search, extra_filter) + self.assert_search_result(expected_num, search, samdb) + + def test_search_with_dirsync(self): + """Checks dirsync controls don't reveal confidential attributes""" + + self.assert_conf_attr_searches(has_rights_to="all") + self.assert_attr_visible(expect_attr=True) + self.assert_negative_searches(has_rights_to="all") + + # make the test attribute confidential and check user can't see it, + # even if they use the dirsync controls + self.make_attr_confidential() + + self.assert_conf_attr_searches(has_rights_to=0) + self.assert_attr_visible(expect_attr=False) + dc_mode = self.guess_dc_mode() + self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode) + + # as a final sanity-check, make sure the admin can still see the attr + self.assert_conf_attr_searches(has_rights_to="all", + samdb=self.ldb_admin) + self.assert_attr_visible(expect_attr=True, samdb=self.ldb_admin) + self.assert_negative_searches(has_rights_to="all", + samdb=self.ldb_admin) + +TestProgram(module=__name__, opts=subunitopts) diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py index 4efe9630f61..9f2dff0f40c 100755 --- a/source4/selftest/tests.py +++ b/source4/selftest/tests.py @@ -740,6 +740,9 @@ for env in ["ad_dc_ntvfs", "fl2000dc", "fl2003dc", "fl2008r2dc"]: # therefore skip it in that configuration plantestsuite_loadlist("samba4.ldap.passwords.python(%s)" % env, env, [python, os.path.join(samba4srcdir, "dsdb/tests/python/passwords.py"), "$SERVER", '-U"$USERNAME%$PASSWORD"', "-W$DOMAIN", '$LOADLIST', '$LISTOPT']) +env = "ad_dc_ntvfs" +plantestsuite_loadlist("samba4.ldap.confidential_attr.python(%s)" % env, env, [python, os.path.join(samba4srcdir, "dsdb/tests/python/confidential_attr.py"), '$SERVER', '-U"$USERNAME%$PASSWORD"', '--workgroup=$DOMAIN', '$LOADLIST', '$LISTOPT']) + for env in ["ad_dc_ntvfs"]: # This test takes a lot of time, so we run it against a minimum of # environments, please only add new ones if there's really a -- 2.11.0 From 938a55cf348bd95a5a9d940e1894d5a6df3251db Mon Sep 17 00:00:00 2001 From: Tim Beale Date: Tue, 31 Jul 2018 14:14:20 +1200 Subject: [PATCH 07/20] CVE-2018-10919 tests: Add test case for object visibility with limited rights Currently Samba is a bit disclosive with LDB_OP_PRESENT (i.e. attribute=*) searches compared to Windows. All the acl.py tests are based on objectClass=* searches, where Windows will happily tell a user about objects they have List Contents rights, but not Read Property rights for. However, if you change the attribute being searched for, suddenly the objects are no longer visible on Windows (whereas they are on Samba). This is a problem, because Samba can tell you about which objects have confidential attributes, which in itself could be disclosive. This patch adds a acl.py test-case that highlights this behaviour. The test passes against Windows but fails against Samba. Signed-off-by: Tim Beale --- selftest/knownfail.d/acl | 1 + source4/dsdb/tests/python/acl.py | 68 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 selftest/knownfail.d/acl diff --git a/selftest/knownfail.d/acl b/selftest/knownfail.d/acl new file mode 100644 index 00000000000..6772ea1f943 --- /dev/null +++ b/selftest/knownfail.d/acl @@ -0,0 +1 @@ +^samba4.ldap.acl.python.*test_search7 diff --git a/source4/dsdb/tests/python/acl.py b/source4/dsdb/tests/python/acl.py index ec042eeec6b..2038dad586d 100755 --- a/source4/dsdb/tests/python/acl.py +++ b/source4/dsdb/tests/python/acl.py @@ -981,6 +981,74 @@ class AclSearchTests(AclTests): res_list = res[0].keys() self.assertEquals(sorted(res_list), sorted(ok_list)) + def assert_search_on_attr(self, dn, samdb, attr, expected_list): + + expected_num = len(expected_list) + res = samdb.search(dn, expression="(%s=*)" % attr, scope=SCOPE_SUBTREE) + self.assertEquals(len(res), expected_num) + + res_list = [ x["dn"] for x in res if x["dn"] in expected_list ] + self.assertEquals(sorted(res_list), sorted(expected_list)) + + def test_search7(self): + """Checks object search visibility when users don't have full rights""" + self.create_clean_ou("OU=ou1," + self.base_dn) + mod = "(A;;LC;;;%s)(A;;LC;;;%s)" % (str(self.user_sid), + str(self.group_sid)) + self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod) + tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod, + self.domain_sid) + self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc) + self.ldb_admin.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, + sd=tmp_desc) + self.ldb_admin.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn, + sd=tmp_desc) + self.ldb_admin.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn, + sd=tmp_desc) + self.ldb_admin.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn, + sd=tmp_desc) + + ou2_dn = Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn) + ou1_dn = Dn(self.ldb_admin, "OU=ou1," + self.base_dn) + + # even though unprivileged users can't read these attributes for OU2, + # the object should still be visible in searches, because they have + # 'List Contents' rights still. This isn't really disclosive because + # ALL objects have these attributes + visible_attrs = ["objectClass", "distinguishedName", "name", + "objectGUID"] + two_objects = [ou2_dn, ou1_dn] + + for attr in visible_attrs: + # a regular user should just see the 2 objects + self.assert_search_on_attr(str(ou1_dn), self.ldb_user3, attr, + expected_list=two_objects) + + # whereas the following users have LC rights for all the objects, + # so they should see them all + self.assert_search_on_attr(str(ou1_dn), self.ldb_user, attr, + expected_list=self.full_list) + self.assert_search_on_attr(str(ou1_dn), self.ldb_user2, attr, + expected_list=self.full_list) + + # however when searching on the following attributes, objects will not + # be visible unless the user has Read Property rights + hidden_attrs = ["objectCategory", "instanceType", "ou", "uSNChanged", + "uSNCreated", "whenCreated"] + one_object = [ou1_dn] + + for attr in hidden_attrs: + self.assert_search_on_attr(str(ou1_dn), self.ldb_user3, attr, + expected_list=one_object) + self.assert_search_on_attr(str(ou1_dn), self.ldb_user, attr, + expected_list=one_object) + self.assert_search_on_attr(str(ou1_dn), self.ldb_user2, attr, + expected_list=one_object) + + # admin has RP rights so can still see all the objects + self.assert_search_on_attr(str(ou1_dn), self.ldb_admin, attr, + expected_list=self.full_list) + #tests on ldap delete operations class AclDeleteTests(AclTests): -- 2.11.0 From 1594cade555d96461b5b9db9965d8cdf9f5e45e0 Mon Sep 17 00:00:00 2001 From: Tim Beale Date: Fri, 20 Jul 2018 13:01:00 +1200 Subject: [PATCH 08/20] CVE-2018-10919 security: Fix checking of object-specific CONTROL_ACCESS rights An 'Object Access Allowed' ACE that assigned 'Control Access' (CR) rights to a specific attribute would not actually grant access. What was happening was the remaining_access mask for the object_tree nodes would be Read Property (RP) + Control Access (CR). The ACE mapped to the schemaIDGUID for a given attribute, which would end up being a child node in the tree. So the CR bit was cleared for a child node, but not the rest of the tree. We would then check the user had the RP access right, which it did. However, the RP right was cleared for another node in the tree, which still had the CR bit set in its remaining_access bitmap, so Samba would not grant access. Generally, the remaining_access only ever has one bit set, which means this isn't a problem normally. However, in the Control Access case there are 2 separate bits being checked, i.e. RP + CR. One option to fix this problem would be to clear the remaining_access for the tree instead of just the node. However, the Windows spec is actually pretty clear on this: if the ACE has a CR right present, then you can stop any further access checks. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13434 Signed-off-by: Tim Beale --- libcli/security/access_check.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libcli/security/access_check.c b/libcli/security/access_check.c index 93eb85def91..03a7dca4adf 100644 --- a/libcli/security/access_check.c +++ b/libcli/security/access_check.c @@ -429,6 +429,16 @@ static NTSTATUS check_object_specific_access(struct security_ace *ace, *grant_access = true; return NT_STATUS_OK; } + + /* + * As per 5.1.3.3.4 Checking Control Access Right-Based Access, + * if the CONTROL_ACCESS right is present, then we can grant + * access and stop any further access checks + */ + if (ace->access_mask & SEC_ADS_CONTROL_ACCESS) { + *grant_access = true; + return NT_STATUS_OK; + } } else { /* this ACE denies access to the requested object/attribute */ -- 2.11.0 From ddd6279e122405e87770db173234a26c5d81a616 Mon Sep 17 00:00:00 2001 From: Gary Lockyer Date: Fri, 3 Aug 2018 15:51:28 +1200 Subject: [PATCH 09/20] CVE-2018-10919 tests: test ldap searches for non-existent attributes. It is perfectly legal to search LDAP for an attribute that is not part of the schema. That part of the query should simply not match. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13434 Signed-off-by: Gary Lockyer --- source4/dsdb/tests/python/ldap.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/source4/dsdb/tests/python/ldap.py b/source4/dsdb/tests/python/ldap.py index 4235541fdbe..2514d0a9d72 100755 --- a/source4/dsdb/tests/python/ldap.py +++ b/source4/dsdb/tests/python/ldap.py @@ -599,6 +599,15 @@ class BasicTests(samba.tests.TestCase): except LdbError, (num, _): self.assertEquals(num, ERR_NO_SUCH_ATTRIBUTE) + # + # When searching the unknown attribute should be ignored + expr = "(|(cn=ldaptestgroup)(thisdoesnotexist=x))" + res = ldb.search(base=self.base_dn, + expression=expr, + scope=SCOPE_SUBTREE) + self.assertTrue(len(res) == 1, + "Search including unknown attribute failed") + delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn) # attributes not in objectclasses and mandatory attributes missing test -- 2.11.0 From e95c621a7f243058a24f00a02e25d5edde35565d Mon Sep 17 00:00:00 2001 From: Tim Beale Date: Fri, 20 Jul 2018 13:52:24 +1200 Subject: [PATCH 10/20] CVE-2018-10919 acl_read: Split access_mask logic out into helper function So we can re-use the same logic laster for checking the search-ops. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13434 Signed-off-by: Tim Beale --- source4/dsdb/samdb/ldb_modules/acl_read.c | 54 ++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/source4/dsdb/samdb/ldb_modules/acl_read.c b/source4/dsdb/samdb/ldb_modules/acl_read.c index 3c9cf7c0672..f42b131948c 100644 --- a/source4/dsdb/samdb/ldb_modules/acl_read.c +++ b/source4/dsdb/samdb/ldb_modules/acl_read.c @@ -227,6 +227,40 @@ static int aclread_get_sd_from_ldb_message(struct aclread_context *ac, return LDB_SUCCESS; } +/* + * Returns the access mask required to read a given attribute + */ +static uint32_t get_attr_access_mask(const struct dsdb_attribute *attr, + uint32_t sd_flags) +{ + + uint32_t access_mask = 0; + bool is_sd; + + /* nTSecurityDescriptor is a special case */ + is_sd = (ldb_attr_cmp("nTSecurityDescriptor", + attr->lDAPDisplayName) == 0); + + if (is_sd) { + if (sd_flags & (SECINFO_OWNER|SECINFO_GROUP)) { + access_mask |= SEC_STD_READ_CONTROL; + } + if (sd_flags & SECINFO_DACL) { + access_mask |= SEC_STD_READ_CONTROL; + } + if (sd_flags & SECINFO_SACL) { + access_mask |= SEC_FLAG_SYSTEM_SECURITY; + } + } else { + access_mask = SEC_ADS_READ_PROP; + } + + if (attr->searchFlags & SEARCH_FLAG_CONFIDENTIAL) { + access_mask |= SEC_ADS_CONTROL_ACCESS; + } + + return access_mask; +} static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares) { @@ -342,26 +376,8 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares) aclread_mark_inaccesslible(&msg->elements[i]); continue; } - /* nTSecurityDescriptor is a special case */ - if (is_sd) { - access_mask = 0; - - if (ac->sd_flags & (SECINFO_OWNER|SECINFO_GROUP)) { - access_mask |= SEC_STD_READ_CONTROL; - } - if (ac->sd_flags & SECINFO_DACL) { - access_mask |= SEC_STD_READ_CONTROL; - } - if (ac->sd_flags & SECINFO_SACL) { - access_mask |= SEC_FLAG_SYSTEM_SECURITY; - } - } else { - access_mask = SEC_ADS_READ_PROP; - } - if (attr->searchFlags & SEARCH_FLAG_CONFIDENTIAL) { - access_mask |= SEC_ADS_CONTROL_ACCESS; - } + access_mask = get_attr_access_mask(attr, ac->sd_flags); if (access_mask == 0) { aclread_mark_inaccesslible(&msg->elements[i]); -- 2.11.0 From df6c1dbeb27ab4c7dedc2461a9d20a6b67ffdda4 Mon Sep 17 00:00:00 2001 From: Tim Beale Date: Thu, 26 Jul 2018 12:20:49 +1200 Subject: [PATCH 11/20] CVE-2018-10919 acl_read: Small refactor to aclread_callback() Flip the dirsync check (to avoid a double negative), and use a helper boolean variable. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13434 Signed-off-by: Tim Beale --- source4/dsdb/samdb/ldb_modules/acl_read.c | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/source4/dsdb/samdb/ldb_modules/acl_read.c b/source4/dsdb/samdb/ldb_modules/acl_read.c index f42b131948c..17d6492cd35 100644 --- a/source4/dsdb/samdb/ldb_modules/acl_read.c +++ b/source4/dsdb/samdb/ldb_modules/acl_read.c @@ -398,18 +398,12 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares) * in anycase. */ if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { - if (!ac->indirsync) { - /* - * do not return this entry if attribute is - * part of the search filter - */ - if (dsdb_attr_in_parse_tree(ac->req->op.search.tree, - msg->elements[i].name)) { - talloc_free(tmp_ctx); - return LDB_SUCCESS; - } - aclread_mark_inaccesslible(&msg->elements[i]); - } else { + bool in_search_filter; + + in_search_filter = dsdb_attr_in_parse_tree(ac->req->op.search.tree, + msg->elements[i].name); + + if (ac->indirsync) { /* * We are doing dirysnc answers * and the object shouldn't be returned (normally) @@ -418,13 +412,22 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares) * (remove the object if it is not deleted, or return * just the objectGUID if it's deleted). */ - if (dsdb_attr_in_parse_tree(ac->req->op.search.tree, - msg->elements[i].name)) { + if (in_search_filter) { ldb_msg_remove_attr(msg, "replPropertyMetaData"); break; } else { aclread_mark_inaccesslible(&msg->elements[i]); } + } else { + /* + * do not return this entry if attribute is + * part of the search filter + */ + if (in_search_filter) { + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + aclread_mark_inaccesslible(&msg->elements[i]); } } else if (ret != LDB_SUCCESS) { ldb_debug_set(ldb, LDB_DEBUG_FATAL, -- 2.11.0 From 717bde3288704d501368ca650963e2648d005c55 Mon Sep 17 00:00:00 2001 From: Tim Beale Date: Mon, 30 Jul 2018 16:00:15 +1200 Subject: [PATCH 12/20] CVE-2018-10919 acl_read: Flip the logic in the dirsync check This better reflects the special case we're making for dirsync, and gets rid of a 'if-else' clause. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13434 Signed-off-by: Tim Beale --- source4/dsdb/samdb/ldb_modules/acl_read.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/source4/dsdb/samdb/ldb_modules/acl_read.c b/source4/dsdb/samdb/ldb_modules/acl_read.c index 17d6492cd35..9607ed05ee7 100644 --- a/source4/dsdb/samdb/ldb_modules/acl_read.c +++ b/source4/dsdb/samdb/ldb_modules/acl_read.c @@ -400,10 +400,12 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares) if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { bool in_search_filter; + /* check if attr is part of the search filter */ in_search_filter = dsdb_attr_in_parse_tree(ac->req->op.search.tree, msg->elements[i].name); - if (ac->indirsync) { + if (in_search_filter) { + /* * We are doing dirysnc answers * and the object shouldn't be returned (normally) @@ -412,21 +414,16 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares) * (remove the object if it is not deleted, or return * just the objectGUID if it's deleted). */ - if (in_search_filter) { + if (ac->indirsync) { ldb_msg_remove_attr(msg, "replPropertyMetaData"); break; } else { - aclread_mark_inaccesslible(&msg->elements[i]); - } - } else { - /* - * do not return this entry if attribute is - * part of the search filter - */ - if (in_search_filter) { + + /* do not return this entry */ talloc_free(tmp_ctx); return LDB_SUCCESS; } + } else { aclread_mark_inaccesslible(&msg->elements[i]); } } else if (ret != LDB_SUCCESS) { -- 2.11.0 From 9b17ce9a1f46e8519302eb6ec72f1104560bf953 Mon Sep 17 00:00:00 2001 From: Tim Beale Date: Fri, 20 Jul 2018 15:42:36 +1200 Subject: [PATCH 13/20] 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 --- selftest/knownfail.d/acl | 1 - selftest/knownfail.d/confidential_attr | 15 -- source4/dsdb/samdb/ldb_modules/acl_read.c | 247 ++++++++++++++++++++++++++++++ 3 files changed, 247 insertions(+), 16 deletions(-) delete mode 100644 selftest/knownfail.d/acl delete mode 100644 selftest/knownfail.d/confidential_attr 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 9607ed05ee7..280845a47a5 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; @@ -262,6 +264,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; @@ -275,6 +490,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); @@ -436,6 +652,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++; -- 2.11.0 From e0bb0b6f74e32a7a0ddd7251f1c305eb38363359 Mon Sep 17 00:00:00 2001 From: Tim Beale Date: Wed, 1 Aug 2018 13:51:42 +1200 Subject: [PATCH 14/20] CVE-2018-10919 tests: Add extra test for dirsync deleted object corner-case The acl_read.c code contains a special case to allow dirsync to work-around having insufficient access rights. We had a concern that the dirsync module could leak sensitive information for deleted objects. This patch adds a test-case to prove whether or not this is happening. The new test case is similar to the existing dirsync test except: - We make the confidential attribute also preserve-on-delete, so it hangs around for deleted objcts. Because the attributes now persist across test case runs, I've used a different attribute to normal. (Technically, the dirsync search expressions are now specific enough that the regular attribute could be used, but it would make things quite fragile if someone tried to add a new test case). - To handle searching for deleted objects, the search expressions are now more complicated. Currently dirsync adds an extra-filter to the '!' searches to exclude deleted objects, i.e. samaccountname matches the test-objects AND the object is not deleted. We now extend this to include deleted objects with lastKnownParent equal to the test OU. The search expression matches either case so that we can use the same expression throughout the test (regardless of whether the object is deleted yet or not). This test proves that the dirsync corner-case does not actually leak sensitive information on Samba. This is due to a bug in the dirsync code - when the buggy line is removed, this new test promptly fails. Test also passes against Windows. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13434 Signed-off-by: Tim Beale --- source4/dsdb/tests/python/confidential_attr.py | 157 +++++++++++++++++++++---- 1 file changed, 131 insertions(+), 26 deletions(-) diff --git a/source4/dsdb/tests/python/confidential_attr.py b/source4/dsdb/tests/python/confidential_attr.py index c3909d5fc2e..1e1cf6c48f8 100755 --- a/source4/dsdb/tests/python/confidential_attr.py +++ b/source4/dsdb/tests/python/confidential_attr.py @@ -28,7 +28,7 @@ import os from samba.tests.subunitrun import SubunitOptions, TestProgram import samba.getopt as options from ldb import SCOPE_BASE, SCOPE_SUBTREE -from samba.dsdb import SEARCH_FLAG_CONFIDENTIAL +from samba.dsdb import SEARCH_FLAG_CONFIDENTIAL, SEARCH_FLAG_PRESERVEONDELETE from ldb import Message, MessageElement, Dn from ldb import FLAG_MOD_REPLACE, FLAG_MOD_ADD from samba.auth import system_session @@ -102,11 +102,11 @@ class ConfidentialAttrCommon(samba.tests.TestCase): # schema attribute being modified. There are only a few attributes that # meet this criteria (most of which only apply to 'user' objects) self.conf_attr = "homePostalAddress" - self.conf_attr_cn = "CN=Address-Home" - # schemaIdGuid for homePostalAddress: + attr_cn = "CN=Address-Home" + # schemaIdGuid for homePostalAddress (used for ACE tests) self.conf_attr_guid = "16775781-47f3-11d1-a9c3-0000f80367c1" self.conf_attr_sec_guid = "77b5b886-944a-11d1-aebd-0000f80367c1" - self.attr_dn = "{},{}".format(self.conf_attr_cn, self.schema_dn) + self.attr_dn = "{},{}".format(attr_cn, self.schema_dn) userou = "OU=conf-attr-test" self.ou = "{},{}".format(userou, self.base_dn) @@ -801,28 +801,42 @@ class ConfidentialAttrTestDirsync(ConfidentialAttrCommon): super(ConfidentialAttrTestDirsync, self).setUp() self.dirsync = ["dirsync:1:1:1000"] - def assert_search_result(self, expected_num, expr, samdb, base_dn=None): - - # Note dirsync must always search on the partition base DN - if base_dn is None: - base_dn = self.base_dn + # because we need to search on the base DN when using the dirsync + # controls, we need an extra filter for the inverse ('!') search, + # so we don't get thousands of objects returned + self.extra_filter = \ + "(&(samaccountname={}*)(!(isDeleted=*)))".format(self.user_prefix) + self.single_obj_filter = \ + "(&(samaccountname={})(!(isDeleted=*)))".format(self.conf_user) - attr_filters = [None, ["*"], ["name"]] + self.attr_filters = [None, ["*"], ["name"]] # Note dirsync behaviour is slighty different for the attribute under # test - when you have full access rights, it only returns the objects # that actually have this attribute (i.e. it doesn't return an empty # message with just the DN). So we add the 'name' attribute into the # attribute filter to avoid complicating our assertions further - attr_filters += [[self.conf_attr, "name"]] - for attr in attr_filters: - res = samdb.search(base_dn, expression=expr, scope=SCOPE_SUBTREE, - attrs=attr, controls=self.dirsync) + self.attr_filters += [[self.conf_attr, "name"]] + + def assert_search_result(self, expected_num, expr, samdb, base_dn=None): + # Note dirsync must always search on the partition base DN + if base_dn is None: + base_dn = self.base_dn + + # we need an extra filter for dirsync because: + # - we search on the base DN, so otherwise the '!' searches return + # thousands of unrelated results, and + # - we make the test attribute preserve-on-delete in one case, so we + # want to weed out results from any previous test runs + search = "(&{}{})".format(expr, self.extra_filter) + + for attr in self.attr_filters: + res = samdb.search(base_dn, expression=search, scope=SCOPE_SUBTREE, + attrs=attr, controls=self.dirsync) self.assertTrue(len(res) == expected_num, "%u results, not %u for search %s, attr %s" % - (len(res), expected_num, expr, str(attr))) - + (len(res), expected_num, search, str(attr))) def assert_attr_returned(self, expect_attr, samdb, attrs, no_result_ok=False): @@ -830,7 +844,7 @@ class ConfidentialAttrTestDirsync(ConfidentialAttrCommon): # When using dirsync, the base DN we search on needs to be a naming # context. Add an extra filter to ignore all the objects we aren't # interested in - expr = "(&(samaccountname={})(!(isDeleted=*)))".format(self.conf_user) + expr = self.single_obj_filter res = samdb.search(self.base_dn, expression=expr, scope=SCOPE_SUBTREE, attrs=attrs, controls=self.dirsync) self.assertTrue(len(res) == 1 or no_result_ok) @@ -877,21 +891,14 @@ class ConfidentialAttrTestDirsync(ConfidentialAttrCommon): if samdb is None: samdb = self.ldb_user - # because we need to search on the base DN when using the dirsync - # controls, we need an extra filter for the inverse ('!') search, - # so we don't get thousands of objects returned - extra_filter = \ - "(samaccountname={}*)(!(isDeleted=*))".format(self.user_prefix) - - # because of this extra filter, the total objects we expect here only - # includes the user objects (not the parent OU) + # because dirsync uses an extra filter, the total objects we expect + # here only includes the user objects (not the parent OU) total_objects = len(self.all_users) expected_results = self.negative_search_expected_results(has_rights_to, dc_mode, total_objects) for search, expected_num in expected_results.items(): - search = "(&{}{})".format(search, extra_filter) self.assert_search_result(expected_num, search, samdb) def test_search_with_dirsync(self): @@ -917,4 +924,102 @@ class ConfidentialAttrTestDirsync(ConfidentialAttrCommon): self.assert_negative_searches(has_rights_to="all", samdb=self.ldb_admin) + def get_guid(self, dn): + """Returns an object's GUID (in string format)""" + res = self.ldb_admin.search(base=dn, attrs=["objectGUID"], + scope=SCOPE_BASE) + guid = res[0]['objectGUID'][0] + return self.ldb_admin.schema_format_value("objectGUID", guid) + + def make_attr_preserve_on_delete(self): + """Marks the attribute under test as being preserve on delete""" + + # work out the original 'searchFlags' value before we overwrite it + search_flags = int(self.get_attr_search_flags(self.attr_dn)) + + # check we've already set the confidential flag + self.assertTrue(search_flags & SEARCH_FLAG_CONFIDENTIAL != 0) + search_flags |= SEARCH_FLAG_PRESERVEONDELETE + + self.set_attr_search_flags(self.attr_dn, str(search_flags)) + + def change_attr_under_test(self, attr_name, attr_cn): + # change the attribute that the test code uses + self.conf_attr = attr_name + self.attr_dn = "{},{}".format(attr_cn, self.schema_dn) + + # set the new attribute for the user-under-test + self.add_attr(self.conf_dn, self.conf_attr, self.conf_value) + + # 2 other users also have the attribute-under-test set (to a randomish + # value). Set the new attribute for them now (normally this gets done + # in the setUp()) + for username in self.all_users: + if "other-user" in username: + dn = self.get_user_dn(username) + self.add_attr(dn, self.conf_attr, "xyz-blah") + + def test_search_with_dirsync_deleted_objects(self): + """Checks dirsync doesn't reveal confidential info for deleted objs""" + + # change the attribute we're testing (we'll preserve on delete for this + # test case, which means the attribute-under-test hangs around after + # the test case finishes, and would interfere with the searches for + # subsequent other test cases) + self.change_attr_under_test("carLicense", "CN=carLicense") + + # Windows dirsync behaviour is a little strange when you request + # attributes that deleted objects no longer have, so just request 'all + # attributes' to simplify the test logic + self.attr_filters = [None, ["*"]] + + # normally dirsync uses extra filters to exclude deleted objects that + # we're not interested in. Override these filters so they WILL include + # deleted objects, but only from this particular test run. We can do + # this by matching lastKnownParent against this test case's OU, which + # will match any deleted child objects. + ou_guid = self.get_guid(self.ou) + deleted_filter = "(lastKnownParent=)".format(ou_guid) + + # the extra-filter will get combined via AND with the search expression + # we're testing, i.e. filter on the confidential attribute AND only + # include non-deleted objects, OR deleted objects from this test run + exclude_deleted_objs_filter = self.extra_filter + self.extra_filter = "(|{}{})".format(exclude_deleted_objs_filter, + deleted_filter) + + # for matching on a single object, the search expresseion becomes: + # match exactly by account-name AND either a non-deleted object OR a + # deleted object from this test run + match_by_name = "(samaccountname={})".format(self.conf_user) + not_deleted = "(!(isDeleted=*))" + self.single_obj_filter = "(&{}(|{}{}))".format(match_by_name, + not_deleted, + deleted_filter) + + # check that the search filters work as expected + self.assert_conf_attr_searches(has_rights_to="all") + self.assert_attr_visible(expect_attr=True) + self.assert_negative_searches(has_rights_to="all") + + # make the test attribute confidential *and* preserve on delete. + self.make_attr_confidential() + self.make_attr_preserve_on_delete() + + # check we can't see the objects now, even with using dirsync controls + self.assert_conf_attr_searches(has_rights_to=0) + self.assert_attr_visible(expect_attr=False) + dc_mode = self.guess_dc_mode() + self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode) + + # now delete the users (except for the user whose LDB connection + # we're currently using) + for user in self.all_users: + if user != self.user: + self.ldb_admin.delete(self.get_user_dn(user)) + + # check we still can't see the objects + self.assert_conf_attr_searches(has_rights_to=0) + self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode) + TestProgram(module=__name__, opts=subunitopts) -- 2.11.0 From b2a68d6badbf8ea8662f788c903ebe3f802cea53 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Thu, 22 Feb 2018 11:54:45 +1300 Subject: [PATCH 15/20] selftest/tests.py: remove always-needed, never-set with_cmocka flag We have cmocka in third_party, so we are never without it. Signed-off-by: Douglas Bagnall Reviewed-by: Andrew Bartlett (Backported from commit 33ef0e57a4f08eae5ea06f482374fbc0a1014de6 by Andrew Bartlett) --- selftest/tests.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/selftest/tests.py b/selftest/tests.py index a94fc8c0ad2..5745cee033b 100644 --- a/selftest/tests.py +++ b/selftest/tests.py @@ -38,7 +38,6 @@ finally: f.close() have_man_pages_support = ("XSLTPROC_MANPAGES" in config_hash) -with_cmocka = ("HAVE_CMOCKA" in config_hash) with_pam = ("WITH_PAM" in config_hash) pam_wrapper_so_path=config_hash["LIBPAM_WRAPPER_SO_PATH"] @@ -153,13 +152,12 @@ if with_pam: valgrindify(python), pam_wrapper_so_path, "$DOMAIN", "$DC_USERNAME", "$DC_PASSWORD"]) -if with_cmocka: - plantestsuite("samba.unittests.krb5samba", "none", - [os.path.join(bindir(), "default/testsuite/unittests/test_krb5samba")]) - plantestsuite("samba.unittests.sambafs_srv_pipe", "none", - [os.path.join(bindir(), "default/testsuite/unittests/test_sambafs_srv_pipe")]) - plantestsuite("samba.unittests.lib_util_modules", "none", - [os.path.join(bindir(), "default/testsuite/unittests/test_lib_util_modules")]) +plantestsuite("samba.unittests.krb5samba", "none", + [os.path.join(bindir(), "default/testsuite/unittests/test_krb5samba")]) +plantestsuite("samba.unittests.sambafs_srv_pipe", "none", + [os.path.join(bindir(), "default/testsuite/unittests/test_sambafs_srv_pipe")]) +plantestsuite("samba.unittests.lib_util_modules", "none", + [os.path.join(bindir(), "default/testsuite/unittests/test_lib_util_modules")]) - plantestsuite("samba.unittests.smb1cli_session", "none", - [os.path.join(bindir(), "default/libcli/smb/test_smb1cli_session")]) +plantestsuite("samba.unittests.smb1cli_session", "none", + [os.path.join(bindir(), "default/libcli/smb/test_smb1cli_session")]) -- 2.11.0 From a5fe27c10e776ad59288762330ab513439efbfb2 Mon Sep 17 00:00:00 2001 From: Andrew Bartlett Date: Fri, 27 Jul 2018 08:44:24 +1200 Subject: [PATCH 16/20] CVE-2018-1139 libcli/auth: Add initial tests for ntlm_password_check() BUG: https://bugzilla.samba.org/show_bug.cgi?id=13360 Signed-off-by: Andrew Bartlett --- libcli/auth/tests/ntlm_check.c | 413 +++++++++++++++++++++++++++++++++++++++++ libcli/auth/wscript_build | 13 ++ selftest/knownfail.d/ntlm | 2 + selftest/tests.py | 2 + 4 files changed, 430 insertions(+) create mode 100644 libcli/auth/tests/ntlm_check.c create mode 100644 selftest/knownfail.d/ntlm diff --git a/libcli/auth/tests/ntlm_check.c b/libcli/auth/tests/ntlm_check.c new file mode 100644 index 00000000000..e87a0a276d4 --- /dev/null +++ b/libcli/auth/tests/ntlm_check.c @@ -0,0 +1,413 @@ +/* + * Unit tests for the ntlm_check password hash check library. + * + * Copyright (C) Andrew Bartlett 2018 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +/* + * from cmocka.c: + * These headers or their equivalents should be included prior to + * including + * this header file. + * + * #include + * #include + * #include + * + * This allows test applications to use custom definitions of C standard + * library functions and types. + * + */ + +/* + * Note that the messaging routines (audit_message_send and get_event_server) + * are not tested by these unit tests. Currently they are for integration + * test support, and as such are exercised by the integration tests. + */ +#include +#include +#include +#include + +#include "includes.h" +#include "../lib/crypto/crypto.h" +#include "librpc/gen_ndr/netlogon.h" +#include "libcli/auth/libcli_auth.h" +#include "auth/credentials/credentials.h" + +struct ntlm_state { + const char *username; + const char *domain; + DATA_BLOB challenge; + DATA_BLOB ntlm; + DATA_BLOB lm; + DATA_BLOB ntlm_key; + DATA_BLOB lm_key; + const struct samr_Password *nt_hash; +}; + +static int test_ntlm_setup_with_options(void **state, + int flags, bool upn) +{ + NTSTATUS status; + DATA_BLOB challenge = { + .data = discard_const_p(uint8_t, "I am a teapot"), + .length = 8 + }; + struct ntlm_state *ntlm_state = talloc(NULL, struct ntlm_state); + DATA_BLOB target_info = NTLMv2_generate_names_blob(ntlm_state, + NULL, + "serverdom"); + struct cli_credentials *creds = cli_credentials_init(ntlm_state); + cli_credentials_set_username(creds, + "testuser", + CRED_SPECIFIED); + cli_credentials_set_domain(creds, + "testdom", + CRED_SPECIFIED); + cli_credentials_set_workstation(creds, + "testwksta", + CRED_SPECIFIED); + cli_credentials_set_password(creds, + "testpass", + CRED_SPECIFIED); + + if (upn) { + cli_credentials_set_principal(creds, + "testuser@samba.org", + CRED_SPECIFIED); + } + + cli_credentials_get_ntlm_username_domain(creds, + ntlm_state, + &ntlm_state->username, + &ntlm_state->domain); + + status = cli_credentials_get_ntlm_response(creds, + ntlm_state, + &flags, + challenge, + NULL, + target_info, + &ntlm_state->lm, + &ntlm_state->ntlm, + &ntlm_state->lm_key, + &ntlm_state->ntlm_key); + ntlm_state->challenge = challenge; + + ntlm_state->nt_hash = cli_credentials_get_nt_hash(creds, + ntlm_state); + + if (!NT_STATUS_IS_OK(status)) { + return -1; + } + + *state = ntlm_state; + return 0; +} + +static int test_ntlm_setup(void **state) { + return test_ntlm_setup_with_options(state, 0, false); +} + +static int test_ntlm_and_lm_setup(void **state) { + return test_ntlm_setup_with_options(state, + CLI_CRED_LANMAN_AUTH, + false); +} + +static int test_ntlm2_setup(void **state) { + return test_ntlm_setup_with_options(state, + CLI_CRED_NTLM2, + false); +} + +static int test_ntlmv2_setup(void **state) { + return test_ntlm_setup_with_options(state, + CLI_CRED_NTLMv2_AUTH, + false); +} + +static int test_ntlm_teardown(void **state) +{ + struct ntlm_state *ntlm_state + = talloc_get_type_abort(*state, + struct ntlm_state); + TALLOC_FREE(ntlm_state); + *state = NULL; + return 0; +} + +static void test_ntlm_allowed(void **state) +{ + DATA_BLOB user_sess_key, lm_sess_key; + struct ntlm_state *ntlm_state + = talloc_get_type_abort(*state, + struct ntlm_state); + NTSTATUS status; + status = ntlm_password_check(ntlm_state, + false, + NTLM_AUTH_ON, + 0, + &ntlm_state->challenge, + &ntlm_state->lm, + &ntlm_state->ntlm, + ntlm_state->username, + ntlm_state->username, + ntlm_state->domain, + NULL, + ntlm_state->nt_hash, + &user_sess_key, + &lm_sess_key); + + assert_int_equal(NT_STATUS_V(status), NT_STATUS_V(NT_STATUS_OK)); +} + +static void test_ntlm_allowed_lm_supplied(void **state) +{ + return test_ntlm_allowed(state); +} + +static void test_ntlm_disabled(void **state) +{ + DATA_BLOB user_sess_key, lm_sess_key; + struct ntlm_state *ntlm_state + = talloc_get_type_abort(*state, + struct ntlm_state); + NTSTATUS status; + status = ntlm_password_check(ntlm_state, + false, + NTLM_AUTH_DISABLED, + 0, + &ntlm_state->challenge, + &ntlm_state->lm, + &ntlm_state->ntlm, + ntlm_state->username, + ntlm_state->username, + ntlm_state->domain, + NULL, + ntlm_state->nt_hash, + &user_sess_key, + &lm_sess_key); + + assert_int_equal(NT_STATUS_V(status), NT_STATUS_V(NT_STATUS_NTLM_BLOCKED)); +} + +static void test_ntlm2(void **state) +{ + DATA_BLOB user_sess_key, lm_sess_key; + struct ntlm_state *ntlm_state + = talloc_get_type_abort(*state, + struct ntlm_state); + NTSTATUS status; + status = ntlm_password_check(ntlm_state, + false, + NTLM_AUTH_ON, + 0, + &ntlm_state->challenge, + &ntlm_state->lm, + &ntlm_state->ntlm, + ntlm_state->username, + ntlm_state->username, + ntlm_state->domain, + NULL, + ntlm_state->nt_hash, + &user_sess_key, + &lm_sess_key); + + /* + * NTLM2 session security (where the real challenge is the + * MD5(challenge, client-challenge) (in the first 8 bytes of + * the lm) isn't decoded by ntlm_password_check(), it must + * first be converted back into normal NTLM by the NTLMSSP + * layer + */ + assert_int_equal(NT_STATUS_V(status), + NT_STATUS_V(NT_STATUS_WRONG_PASSWORD)); +} + +static void test_ntlm_mschapv2_only_allowed(void **state) +{ + DATA_BLOB user_sess_key, lm_sess_key; + struct ntlm_state *ntlm_state + = talloc_get_type_abort(*state, + struct ntlm_state); + NTSTATUS status; + status = ntlm_password_check(ntlm_state, + false, + NTLM_AUTH_MSCHAPv2_NTLMV2_ONLY, + MSV1_0_ALLOW_MSVCHAPV2, + &ntlm_state->challenge, + &ntlm_state->lm, + &ntlm_state->ntlm, + ntlm_state->username, + ntlm_state->username, + ntlm_state->domain, + NULL, + ntlm_state->nt_hash, + &user_sess_key, + &lm_sess_key); + + assert_int_equal(NT_STATUS_V(status), NT_STATUS_V(NT_STATUS_OK)); +} + +static void test_ntlm_mschapv2_only_denied(void **state) +{ + DATA_BLOB user_sess_key, lm_sess_key; + struct ntlm_state *ntlm_state + = talloc_get_type_abort(*state, + struct ntlm_state); + NTSTATUS status; + status = ntlm_password_check(ntlm_state, + false, + NTLM_AUTH_MSCHAPv2_NTLMV2_ONLY, + 0, + &ntlm_state->challenge, + &ntlm_state->lm, + &ntlm_state->ntlm, + ntlm_state->username, + ntlm_state->username, + ntlm_state->domain, + NULL, + ntlm_state->nt_hash, + &user_sess_key, + &lm_sess_key); + + assert_int_equal(NT_STATUS_V(status), + NT_STATUS_V(NT_STATUS_WRONG_PASSWORD)); +} + +static void test_ntlmv2_only_ntlmv2(void **state) +{ + DATA_BLOB user_sess_key, lm_sess_key; + struct ntlm_state *ntlm_state + = talloc_get_type_abort(*state, + struct ntlm_state); + NTSTATUS status; + status = ntlm_password_check(ntlm_state, + false, + NTLM_AUTH_NTLMV2_ONLY, + 0, + &ntlm_state->challenge, + &ntlm_state->lm, + &ntlm_state->ntlm, + ntlm_state->username, + ntlm_state->username, + ntlm_state->domain, + NULL, + ntlm_state->nt_hash, + &user_sess_key, + &lm_sess_key); + + assert_int_equal(NT_STATUS_V(status), NT_STATUS_V(NT_STATUS_OK)); +} + +static void test_ntlmv2_only_ntlm(void **state) +{ + DATA_BLOB user_sess_key, lm_sess_key; + struct ntlm_state *ntlm_state + = talloc_get_type_abort(*state, + struct ntlm_state); + NTSTATUS status; + status = ntlm_password_check(ntlm_state, + false, + NTLM_AUTH_NTLMV2_ONLY, + 0, + &ntlm_state->challenge, + &ntlm_state->lm, + &ntlm_state->ntlm, + ntlm_state->username, + ntlm_state->username, + ntlm_state->domain, + NULL, + ntlm_state->nt_hash, + &user_sess_key, + &lm_sess_key); + + assert_int_equal(NT_STATUS_V(status), + NT_STATUS_V(NT_STATUS_WRONG_PASSWORD)); +} + +static void test_ntlmv2_only_ntlm_and_lanman(void **state) +{ + return test_ntlmv2_only_ntlm(state); +} + +static void test_ntlmv2_only_ntlm_once(void **state) +{ + DATA_BLOB user_sess_key, lm_sess_key; + struct ntlm_state *ntlm_state + = talloc_get_type_abort(*state, + struct ntlm_state); + NTSTATUS status; + status = ntlm_password_check(ntlm_state, + false, + NTLM_AUTH_NTLMV2_ONLY, + 0, + &ntlm_state->challenge, + &data_blob_null, + &ntlm_state->ntlm, + ntlm_state->username, + ntlm_state->username, + ntlm_state->domain, + NULL, + ntlm_state->nt_hash, + &user_sess_key, + &lm_sess_key); + + assert_int_equal(NT_STATUS_V(status), + NT_STATUS_V(NT_STATUS_WRONG_PASSWORD)); +} + +int main(int argc, const char **argv) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_ntlm_allowed, + test_ntlm_setup, + test_ntlm_teardown), + cmocka_unit_test_setup_teardown(test_ntlm_allowed_lm_supplied, + test_ntlm_and_lm_setup, + test_ntlm_teardown), + cmocka_unit_test_setup_teardown(test_ntlm_disabled, + test_ntlm_setup, + test_ntlm_teardown), + cmocka_unit_test_setup_teardown(test_ntlm2, + test_ntlm2_setup, + test_ntlm_teardown), + cmocka_unit_test_setup_teardown(test_ntlm_mschapv2_only_allowed, + test_ntlm_setup, + test_ntlm_teardown), + cmocka_unit_test_setup_teardown(test_ntlm_mschapv2_only_denied, + test_ntlm_setup, + test_ntlm_teardown), + cmocka_unit_test_setup_teardown(test_ntlmv2_only_ntlm, + test_ntlm_setup, + test_ntlm_teardown), + cmocka_unit_test_setup_teardown(test_ntlmv2_only_ntlm_and_lanman, + test_ntlm_and_lm_setup, + test_ntlm_teardown), + cmocka_unit_test_setup_teardown(test_ntlmv2_only_ntlm_once, + test_ntlm_setup, + test_ntlm_teardown), + cmocka_unit_test_setup_teardown(test_ntlmv2_only_ntlmv2, + test_ntlmv2_setup, + test_ntlm_teardown) + }; + + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/libcli/auth/wscript_build b/libcli/auth/wscript_build index 475b7d69406..d319d9b879e 100644 --- a/libcli/auth/wscript_build +++ b/libcli/auth/wscript_build @@ -41,3 +41,16 @@ bld.SAMBA_SUBSYSTEM('PAM_ERRORS', bld.SAMBA_SUBSYSTEM('SPNEGO_PARSE', source='spnego_parse.c', deps='asn1util') + +bld.SAMBA_BINARY( + 'test_ntlm_check', + source='tests/ntlm_check.c', + deps=''' + NTLM_CHECK + CREDENTIALS_NTLM + samba-credentials + cmocka + talloc + ''', + install=False + ) diff --git a/selftest/knownfail.d/ntlm b/selftest/knownfail.d/ntlm new file mode 100644 index 00000000000..c6e6a3739ba --- /dev/null +++ b/selftest/knownfail.d/ntlm @@ -0,0 +1,2 @@ +^samba.unittests.ntlm_check.test_ntlm_mschapv2_only_denied +^samba.unittests.ntlm_check.test_ntlmv2_only_ntlm\( diff --git a/selftest/tests.py b/selftest/tests.py index 5745cee033b..1188c6402d6 100644 --- a/selftest/tests.py +++ b/selftest/tests.py @@ -161,3 +161,5 @@ plantestsuite("samba.unittests.lib_util_modules", "none", plantestsuite("samba.unittests.smb1cli_session", "none", [os.path.join(bindir(), "default/libcli/smb/test_smb1cli_session")]) +plantestsuite("samba.unittests.ntlm_check", "none", + [os.path.join(bindir(), "default/libcli/auth/test_ntlm_check")]) -- 2.11.0 From 29f2fe7d072d37644592e1cf6bc069de60c5f607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Deschner?= Date: Wed, 14 Mar 2018 15:36:05 +0100 Subject: [PATCH 17/20] CVE-2018-1139 libcli/auth: fix debug messages in hash_password_check() BUG: https://bugzilla.samba.org/show_bug.cgi?id=13360 CVE-2018-1139: Weak authentication protocol allowed. Guenther Signed-off-by: Guenther Deschner Reviewed-by: Andreas Schneider --- libcli/auth/ntlm_check.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libcli/auth/ntlm_check.c b/libcli/auth/ntlm_check.c index 3b02adc1d48..1c6499bd210 100644 --- a/libcli/auth/ntlm_check.c +++ b/libcli/auth/ntlm_check.c @@ -224,7 +224,7 @@ NTSTATUS hash_password_check(TALLOC_CTX *mem_ctx, const struct samr_Password *stored_nt) { if (stored_nt == NULL) { - DEBUG(3,("ntlm_password_check: NO NT password stored for user %s.\n", + DEBUG(3,("hash_password_check: NO NT password stored for user %s.\n", username)); } @@ -232,14 +232,14 @@ NTSTATUS hash_password_check(TALLOC_CTX *mem_ctx, if (memcmp(client_nt->hash, stored_nt->hash, sizeof(stored_nt->hash)) == 0) { return NT_STATUS_OK; } else { - DEBUG(3,("ntlm_password_check: Interactive logon: NT password check failed for user %s\n", + DEBUG(3,("hash_password_check: Interactive logon: NT password check failed for user %s\n", username)); return NT_STATUS_WRONG_PASSWORD; } } else if (client_lanman && stored_lanman) { if (!lanman_auth) { - DEBUG(3,("ntlm_password_check: Interactive logon: only LANMAN password supplied for user %s, and LM passwords are disabled!\n", + DEBUG(3,("hash_password_check: Interactive logon: only LANMAN password supplied for user %s, and LM passwords are disabled!\n", username)); return NT_STATUS_WRONG_PASSWORD; } @@ -250,7 +250,7 @@ NTSTATUS hash_password_check(TALLOC_CTX *mem_ctx, if (memcmp(client_lanman->hash, stored_lanman->hash, sizeof(stored_lanman->hash)) == 0) { return NT_STATUS_OK; } else { - DEBUG(3,("ntlm_password_check: Interactive logon: LANMAN password check failed for user %s\n", + DEBUG(3,("hash_password_check: Interactive logon: LANMAN password check failed for user %s\n", username)); return NT_STATUS_WRONG_PASSWORD; } -- 2.11.0 From 304ad864cf81d6064f57b98b7ac6cd2642e9d6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Deschner?= Date: Wed, 14 Mar 2018 15:35:01 +0100 Subject: [PATCH 18/20] CVE-2018-1139 s3-utils: use enum ntlm_auth_level in ntlm_password_check(). BUG: https://bugzilla.samba.org/show_bug.cgi?id=13360 CVE-2018-1139: Weak authentication protocol allowed. Guenther Signed-off-by: Guenther Deschner Reviewed-by: Andreas Schneider --- source3/utils/ntlm_auth.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/source3/utils/ntlm_auth.c b/source3/utils/ntlm_auth.c index 5a10e27719f..d094ab4fa3e 100644 --- a/source3/utils/ntlm_auth.c +++ b/source3/utils/ntlm_auth.c @@ -1008,7 +1008,7 @@ static NTSTATUS local_pw_check(struct auth4_context *auth4_context, *pauthoritative = 1; nt_status = ntlm_password_check(mem_ctx, - true, true, 0, + true, NTLM_AUTH_ON, 0, &auth4_context->challenge.data, &user_info->password.response.lanman, &user_info->password.response.nt, @@ -1717,7 +1717,9 @@ static void manage_ntlm_server_1_request(enum stdio_helper_mode stdio_helper_mod nt_lm_owf_gen (opt_password, nt_pw.hash, lm_pw.hash); nt_status = ntlm_password_check(mem_ctx, - true, true, 0, + true, + NTLM_AUTH_ON, + 0, &challenge, &lm_response, &nt_response, -- 2.11.0 From cd2e11d9036782d9bf2ac553285694211cce856c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Deschner?= Date: Fri, 16 Mar 2018 17:25:12 +0100 Subject: [PATCH 19/20] CVE-2018-1139 selftest: verify whether ntlmv1 can be used via SMB1 when it is disabled. Right now, this test will succeed. BUG: https://bugzilla.samba.org/show_bug.cgi?id=13360 CVE-2018-1139: Weak authentication protocol allowed. Guenther Signed-off-by: Guenther Deschner Reviewed-by: Andreas Schneider --- source3/selftest/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source3/selftest/tests.py b/source3/selftest/tests.py index 59a3a551d3f..bc73441234c 100755 --- a/source3/selftest/tests.py +++ b/source3/selftest/tests.py @@ -184,7 +184,7 @@ for env in ["nt4_dc", "nt4_member", "ad_member", "ad_dc", "ad_dc_ntvfs", "s4memb plantestsuite("samba3.blackbox.smbclient_machine_auth.plain (%s:local)" % env, "%s:local" % env, [os.path.join(samba3srcdir, "script/tests/test_smbclient_machine_auth.sh"), '$SERVER', smbclient3, configuration]) plantestsuite("samba3.blackbox.smbclient_ntlm.plain (%s)" % env, env, [os.path.join(samba3srcdir, "script/tests/test_smbclient_ntlm.sh"), '$SERVER', '$DC_USERNAME', '$DC_PASSWORD', "never", smbclient3, configuration]) -for options in ["--option=clientntlmv2auth=no", "--option=clientusespnego=no --option=clientntlmv2auth=no", ""]: +for options in ["--option=clientntlmv2auth=no", "--option=clientusespnego=no --option=clientntlmv2auth=no", "--option=clientusespnego=no --option=clientntlmv2auth=no -mNT1", ""]: for env in ["nt4_member", "ad_member"]: plantestsuite("samba3.blackbox.smbclient_auth.plain (%s) %s" % (env, options), env, [os.path.join(samba3srcdir, "script/tests/test_smbclient_auth.sh"), '$SERVER', '$SERVER_IP', '$DC_USERNAME', '$DC_PASSWORD', smbclient3, configuration, options]) plantestsuite("samba3.blackbox.smbclient_auth.plain (%s) %s member creds" % (env, options), env, [os.path.join(samba3srcdir, "script/tests/test_smbclient_auth.sh"), '$SERVER', '$SERVER_IP', '$SERVER/$USERNAME', '$PASSWORD', smbclient3, configuration, options]) -- 2.11.0 From 9ff1d906d0945c644b964f2e577547927387ac6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Deschner?= Date: Tue, 13 Mar 2018 16:56:20 +0100 Subject: [PATCH 20/20] CVE-2018-1139 libcli/auth: Do not allow ntlmv1 over SMB1 when it is disabled via "ntlm auth". This fixes a regression that came in via 00db3aba6cf9ebaafdf39ee2f9c7ba5ec2281ea0. Found by Vivek Das (Red Hat QE). In order to demonstrate simply run: smbclient //server/share -U user%password -mNT1 -c quit \ --option="client ntlmv2 auth"=no \ --option="client use spnego"=no against a server that uses "ntlm auth = ntlmv2-only" (our default setting). BUG: https://bugzilla.samba.org/show_bug.cgi?id=13360 CVE-2018-1139: Weak authentication protocol allowed. Guenther Pair-Programmed-With: Stefan Metzmacher Signed-off-by: Guenther Deschner Reviewed-by: Andreas Schneider --- libcli/auth/ntlm_check.c | 2 +- selftest/knownfail | 3 ++- selftest/knownfail.d/ntlm | 2 -- 3 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 selftest/knownfail.d/ntlm diff --git a/libcli/auth/ntlm_check.c b/libcli/auth/ntlm_check.c index 1c6499bd210..b68e9c87888 100644 --- a/libcli/auth/ntlm_check.c +++ b/libcli/auth/ntlm_check.c @@ -572,7 +572,7 @@ NTSTATUS ntlm_password_check(TALLOC_CTX *mem_ctx, - I think this is related to Win9X pass-though authentication */ DEBUG(4,("ntlm_password_check: Checking NT MD4 password in LM field\n")); - if (ntlm_auth) { + if (ntlm_auth == NTLM_AUTH_ON) { if (smb_pwd_check_ntlmv1(mem_ctx, lm_response, stored_nt->hash, challenge, diff --git a/selftest/knownfail b/selftest/knownfail index 267c928955e..4aa224492ec 100644 --- a/selftest/knownfail +++ b/selftest/knownfail @@ -294,8 +294,9 @@ ^samba4.smb.signing.*disabled.*signing=off.*\(ad_dc\) # fl2000dc doesn't support AES ^samba4.krb5.kdc.*as-req-aes.*fl2000dc -# nt4_member and ad_member don't support ntlmv1 +# nt4_member and ad_member don't support ntlmv1 (not even over SMB1) ^samba3.blackbox.smbclient_auth.plain.*_member.*option=clientntlmv2auth=no.member.creds.*as.user +^samba3.blackbox.smbclient_auth.plain.*_member.*option=clientntlmv2auth=no.*mNT1.member.creds.*as.user #nt-vfs server blocks read with execute access ^samba4.smb2.read.access #ntvfs server blocks copychunk with execute access on read handle diff --git a/selftest/knownfail.d/ntlm b/selftest/knownfail.d/ntlm deleted file mode 100644 index c6e6a3739ba..00000000000 --- a/selftest/knownfail.d/ntlm +++ /dev/null @@ -1,2 +0,0 @@ -^samba.unittests.ntlm_check.test_ntlm_mschapv2_only_denied -^samba.unittests.ntlm_check.test_ntlmv2_only_ntlm\( -- 2.11.0