def run(self, ou_dn, credopts=None, sambaopts=None, versionopts=None, H=None, description=None): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp, fallback_machine=True) samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp) try: full_ou_dn = samdb.normalize_dn_in_domain(ou_dn) except Exception as e: raise CommandError('Invalid ou_dn "%s": %s' % (ou_dn, e)) try: samdb.create_ou(full_ou_dn, description=description) except Exception as e: raise CommandError('Failed to create ou "%s"' % full_ou_dn, e) self.outf.write('Created ou "%s"\n' % full_ou_dn)
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 = "******" 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" 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(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 = "******".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 = "******".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 = "******".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_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.ldb_admin.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 UserAccountControlTests(samba.tests.TestCase): def add_computer_ldap(self, computername, others=None, samdb=None): if samdb is None: samdb = self.samdb dn = "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn) domainname = ldb.Dn(self.samdb, self.samdb.domain_dn()).canonical_str().replace("/", "") samaccountname = "%s$" % computername dnshostname = "%s.%s" % (computername, domainname) msg_dict = { "dn": dn, "objectclass": "computer"} if others is not None: msg_dict = dict(msg_dict.items() + others.items()) msg = ldb.Message.from_dict(self.samdb, msg_dict ) msg["sAMAccountName"] = samaccountname print "Adding computer account %s" % computername samdb.add(msg) def get_creds(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()) creds_tmp.set_gensec_features(creds_tmp.get_gensec_features() | gensec.FEATURE_SEAL) creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop return creds_tmp def setUp(self): super(UserAccountControlTests, self).setUp() self.admin_creds = creds self.admin_samdb = SamDB(url=ldaphost, session_info=system_session(), credentials=self.admin_creds, lp=lp) self.unpriv_user = "******" self.unpriv_user_pw = "samba123@" self.unpriv_creds = self.get_creds(self.unpriv_user, self.unpriv_user_pw) self.admin_samdb.newuser(self.unpriv_user, self.unpriv_user_pw) res = self.admin_samdb.search("CN=%s,CN=Users,%s" % (self.unpriv_user, self.admin_samdb.domain_dn()), scope=SCOPE_BASE, attrs=["objectSid"]) self.assertEqual(1, len(res)) self.unpriv_user_sid = ndr_unpack(security.dom_sid, res[0]["objectSid"][0]) self.unpriv_user_dn = res[0].dn self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp) self.domain_sid = security.dom_sid(self.samdb.get_domain_sid()) self.base_dn = self.samdb.domain_dn() self.samr = samr.samr("ncacn_ip_tcp:%s[sign]" % host, lp, self.unpriv_creds) self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED) self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid) self.sd_utils = sd_utils.SDUtils(self.admin_samdb) self.admin_samdb.create_ou("OU=test_computer_ou1," + self.base_dn) self.unpriv_user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.unpriv_user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) self.add_computer_ldap("testcomputer-t") self.sd_utils.modify_sd_on_dn("OU=test_computer_ou1," + self.base_dn, old_sd) self.computernames = ["testcomputer-0"] # Get the SD of the template account, then force it to match # what we expect for SeMachineAccountPrivilege accounts, so we # can confirm we created the accounts correctly self.sd_reference_cc = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) self.sd_reference_modify = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) for ace in self.sd_reference_modify.dacl.aces: if ace.type == security.SEC_ACE_TYPE_ACCESS_ALLOWED and ace.trustee == self.unpriv_user_sid: ace.access_mask = ace.access_mask | security.SEC_ADS_SELF_WRITE | security.SEC_ADS_WRITE_PROP # Now reconnect without domain admin rights self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp) def tearDown(self): super(UserAccountControlTests, self).tearDown() for computername in self.computernames: delete_force(self.admin_samdb, "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn)) delete_force(self.admin_samdb, "CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) delete_force(self.admin_samdb, "OU=test_computer_ou1,%s" % (self.base_dn)) delete_force(self.admin_samdb, "CN=%s,CN=Users,%s" % (self.unpriv_user, self.base_dn)) def test_add_computer_sd_cc(self): user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) computername=self.computernames[0] sd = ldb.MessageElement((ndr_pack(self.sd_reference_modify)), ldb.FLAG_MOD_ADD, "nTSecurityDescriptor") self.add_computer_ldap(computername, others={"nTSecurityDescriptor": sd}) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["ntSecurityDescriptor"]) desc = res[0]["nTSecurityDescriptor"][0] desc = ndr_unpack(security.descriptor, desc, allow_remaining=True) sddl = desc.as_sddl(self.domain_sid) self.assertEqual(self.sd_reference_modify.as_sddl(self.domain_sid), sddl) m = ldb.Message() m.dn = res[0].dn m["description"]= ldb.MessageElement( ("A description"), ldb.FLAG_MOD_REPLACE, "description") self.samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail("Unexpectedly able to set userAccountControl to be a DC on %s" % m.dn) except LdbError, (enum, estr): self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT|samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail("Unexpectedly able to set userAccountControl to be an RODC on %s" % m.dn) except LdbError, (enum, estr): self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
class UserAccountControlTests(samba.tests.TestCase): def add_computer_ldap(self, computername, others=None, samdb=None): if samdb is None: samdb = self.samdb dn = "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn) domainname = ldb.Dn(self.samdb, self.samdb.domain_dn()).canonical_str().replace( "/", "") samaccountname = "%s$" % computername dnshostname = "%s.%s" % (computername, domainname) msg_dict = {"dn": dn, "objectclass": "computer"} if others is not None: msg_dict = dict(list(msg_dict.items()) + list(others.items())) msg = ldb.Message.from_dict(self.samdb, msg_dict) msg["sAMAccountName"] = samaccountname print("Adding computer account %s" % computername) samdb.add(msg) def get_creds(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()) creds_tmp.set_gensec_features(creds_tmp.get_gensec_features() | gensec.FEATURE_SEAL) creds_tmp.set_kerberos_state( DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop return creds_tmp def setUp(self): super(UserAccountControlTests, self).setUp() self.admin_creds = creds self.admin_samdb = SamDB(url=ldaphost, session_info=system_session(), credentials=self.admin_creds, lp=lp) self.domain_sid = security.dom_sid(self.admin_samdb.get_domain_sid()) self.base_dn = self.admin_samdb.domain_dn() self.unpriv_user = "******" self.unpriv_user_pw = "samba123@" self.unpriv_creds = self.get_creds(self.unpriv_user, self.unpriv_user_pw) delete_force( self.admin_samdb, "CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) delete_force(self.admin_samdb, "OU=test_computer_ou1,%s" % (self.base_dn)) delete_force(self.admin_samdb, "CN=%s,CN=Users,%s" % (self.unpriv_user, self.base_dn)) self.admin_samdb.newuser(self.unpriv_user, self.unpriv_user_pw) res = self.admin_samdb.search( "CN=%s,CN=Users,%s" % (self.unpriv_user, self.admin_samdb.domain_dn()), scope=SCOPE_BASE, attrs=["objectSid"]) self.assertEqual(1, len(res)) self.unpriv_user_sid = ndr_unpack(security.dom_sid, res[0]["objectSid"][0]) self.unpriv_user_dn = res[0].dn self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp) self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % host, lp, self.unpriv_creds) self.samr_handle = self.samr.Connect2( None, security.SEC_FLAG_MAXIMUM_ALLOWED) self.samr_domain = self.samr.OpenDomain( self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid) self.sd_utils = sd_utils.SDUtils(self.admin_samdb) self.admin_samdb.create_ou("OU=test_computer_ou1," + self.base_dn) self.unpriv_user_sid = self.sd_utils.get_object_sid( self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str( self.unpriv_user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) self.add_computer_ldap("testcomputer-t") self.sd_utils.modify_sd_on_dn("OU=test_computer_ou1," + self.base_dn, old_sd) self.computernames = ["testcomputer-0"] # Get the SD of the template account, then force it to match # what we expect for SeMachineAccountPrivilege accounts, so we # can confirm we created the accounts correctly self.sd_reference_cc = self.sd_utils.read_sd_on_dn( "CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) self.sd_reference_modify = self.sd_utils.read_sd_on_dn( "CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) for ace in self.sd_reference_modify.dacl.aces: if ace.type == security.SEC_ACE_TYPE_ACCESS_ALLOWED and ace.trustee == self.unpriv_user_sid: ace.access_mask = ace.access_mask | security.SEC_ADS_SELF_WRITE | security.SEC_ADS_WRITE_PROP # Now reconnect without domain admin rights self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp) def tearDown(self): super(UserAccountControlTests, self).tearDown() for computername in self.computernames: delete_force( self.admin_samdb, "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn)) delete_force( self.admin_samdb, "CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) delete_force(self.admin_samdb, "OU=test_computer_ou1,%s" % (self.base_dn)) delete_force(self.admin_samdb, "CN=%s,CN=Users,%s" % (self.unpriv_user, self.base_dn)) def test_add_computer_sd_cc(self): user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str( user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) computername = self.computernames[0] sd = ldb.MessageElement((ndr_pack(self.sd_reference_modify)), ldb.FLAG_MOD_ADD, "nTSecurityDescriptor") self.add_computer_ldap(computername, others={"nTSecurityDescriptor": sd}) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["ntSecurityDescriptor"]) desc = res[0]["nTSecurityDescriptor"][0] desc = ndr_unpack(security.descriptor, desc, allow_remaining=True) sddl = desc.as_sddl(self.domain_sid) self.assertEqual(self.sd_reference_modify.as_sddl(self.domain_sid), sddl) m = ldb.Message() m.dn = res[0].dn m["description"] = ldb.MessageElement( ("A description"), ldb.FLAG_MOD_REPLACE, "description") self.samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail( "Unexpectedly able to set userAccountControl to be a DC on %s" % m.dn) except LdbError as e5: (enum, estr) = e5.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT | samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail( "Unexpectedly able to set userAccountControl to be an RODC on %s" % m.dn) except LdbError as e6: (enum, estr) = e6.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail( "Unexpectedly able to set userAccountControl to be an Workstation on %s" % m.dn) except LdbError as e7: (enum, estr) = e7.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(samba.dsdb.UF_NORMAL_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["primaryGroupID"] = ldb.MessageElement( str(security.DOMAIN_RID_ADMINS), ldb.FLAG_MOD_REPLACE, "primaryGroupID") try: self.samdb.modify(m) except LdbError as e8: (enum, estr) = e8.args self.assertEqual(ldb.ERR_UNWILLING_TO_PERFORM, enum) return self.fail() def test_mod_computer_cc(self): user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str( user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) computername = self.computernames[0] self.add_computer_ldap(computername) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=[]) m = ldb.Message() m.dn = res[0].dn m["description"] = ldb.MessageElement( ("A description"), ldb.FLAG_MOD_REPLACE, "description") self.samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT | samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail("Unexpectedly able to set userAccountControl on %s" % m.dn) except LdbError as e9: (enum, estr) = e9.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail() except LdbError as e10: (enum, estr) = e10.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(samba.dsdb.UF_NORMAL_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail( "Unexpectedly able to set userAccountControl to be an Workstation on %s" % m.dn) except LdbError as e11: (enum, estr) = e11.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) def test_admin_mod_uac(self): computername = self.computernames[0] self.add_computer_ldap(computername, samdb=self.admin_samdb) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) self.assertEqual( int(res[0]["userAccountControl"][0]), (UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT | UF_TRUSTED_FOR_DELEGATION), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.admin_samdb.modify(m) self.fail( "Unexpectedly able to set userAccountControl to UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT|UF_TRUSTED_FOR_DELEGATION on %s" % m.dn) except LdbError as e12: (enum, estr) = e12.args self.assertEqual(ldb.ERR_OTHER, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.admin_samdb.modify(m) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) self.assertEqual( int(res[0]["userAccountControl"][0]), (UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(UF_ACCOUNTDISABLE), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.admin_samdb.modify(m) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE) def test_uac_bits_set(self): user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str( user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) computername = self.computernames[0] self.add_computer_ldap(computername) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=[]) m = ldb.Message() m.dn = res[0].dn m["description"] = ldb.MessageElement( ("A description"), ldb.FLAG_MOD_REPLACE, "description") self.samdb.modify(m) # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test priv_to_auth_users_bits = set([ UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED, UF_DONT_EXPIRE_PASSWD ]) # These bits really are privileged, or can't be changed from UF_NORMAL as a non-admin priv_bits = set([ UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_WORKSTATION_TRUST_ACCOUNT ]) invalid_bits = set( [UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT]) for bit in bits: m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(bit | UF_PASSWD_NOTREQD), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) if (bit in priv_bits): self.fail( "Unexpectedly able to set userAccountControl bit 0x%08X on %s" % (bit, m.dn)) except LdbError as e: (enum, estr) = e.args if bit in invalid_bits: self.assertEqual( enum, ldb.ERR_OTHER, "was not able to set 0x%08X on %s" % (bit, m.dn)) # No point going on, try the next bit continue elif (bit in priv_bits): self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) else: self.fail( "Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr)) def uac_bits_unrelated_modify_helper(self, account_type): user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str( user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) computername = self.computernames[0] self.add_computer_ldap( computername, others={"userAccountControl": [str(account_type)]}) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) self.assertEqual(int(res[0]["userAccountControl"][0]), account_type) m = ldb.Message() m.dn = res[0].dn m["description"] = ldb.MessageElement( ("A description"), ldb.FLAG_MOD_REPLACE, "description") self.samdb.modify(m) invalid_bits = set( [UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT]) # UF_LOCKOUT isn't actually ignored, it changes other # attributes but does not stick here. See MS-SAMR 2.2.1.13 # UF_FLAG Codes clarification that UF_SCRIPT and # UF_PASSWD_CANT_CHANGE are simply ignored by both clients and # servers. Other bits are ignored as they are undefined, or # are not set into the attribute (instead triggering other # events). ignored_bits = set([ UF_SCRIPT, UF_00000004, UF_LOCKOUT, UF_PASSWD_CANT_CHANGE, UF_00000400, UF_00004000, UF_00008000, UF_PASSWORD_EXPIRED, int("0x10000000", 16), int("0x20000000", 16), int("0x40000000", 16), int("0x80000000", 16) ]) super_priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT]) priv_to_remove_bits = set([ UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_WORKSTATION_TRUST_ACCOUNT ]) for bit in bits: # Reset this to the initial position, just to be sure m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(account_type), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.admin_samdb.modify(m) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) self.assertEqual(int(res[0]["userAccountControl"][0]), account_type) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(bit | UF_PASSWD_NOTREQD), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.admin_samdb.modify(m) if bit in invalid_bits: self.fail( "Should have been unable to set userAccountControl bit 0x%08X on %s" % (bit, m.dn)) except LdbError as e1: (enum, estr) = e1.args if bit in invalid_bits: self.assertEqual(enum, ldb.ERR_OTHER) # No point going on, try the next bit continue elif bit in super_priv_bits: self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS) # No point going on, try the next bit continue else: self.fail( "Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr)) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) if bit in ignored_bits: self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD, "Bit 0x%08x shouldn't stick" % bit) else: if bit in account_types: self.assertEqual(int(res[0]["userAccountControl"][0]), bit | UF_PASSWD_NOTREQD, "Bit 0x%08x didn't stick" % bit) else: self.assertEqual( int(res[0]["userAccountControl"][0]), bit | UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD, "Bit 0x%08x didn't stick" % bit) try: m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(bit | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.samdb.modify(m) except LdbError as e2: (enum, estr) = e2.args self.fail( "Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr)) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) if bit in account_types: self.assertEqual( int(res[0]["userAccountControl"][0]), bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD, "bit 0X%08x should have been added (0X%08x vs 0X%08x)" % (bit, int(res[0]["userAccountControl"][0]), bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)) elif bit in ignored_bits: self.assertEqual( int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD, "bit 0X%08x should have been added (0X%08x vs 0X%08x)" % (bit, int( res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)) else: self.assertEqual( int(res[0]["userAccountControl"][0]), bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD, "bit 0X%08x should have been added (0X%08x vs 0X%08x)" % (bit, int(res[0]["userAccountControl"][0]), bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)) try: m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement( str(UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.samdb.modify(m) if bit in priv_to_remove_bits: self.fail( "Should have been unable to remove userAccountControl bit 0x%08X on %s" % (bit, m.dn)) except LdbError as e3: (enum, estr) = e3.args if bit in priv_to_remove_bits: self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS) else: self.fail( "Unexpectedly unable to remove userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr)) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) if bit in priv_to_remove_bits: if bit in account_types: self.assertEqual( int(res[0]["userAccountControl"][0]), bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD, "bit 0X%08x should not have been removed" % bit) else: self.assertEqual( int(res[0]["userAccountControl"][0]), bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD, "bit 0X%08x should not have been removed" % bit) else: self.assertEqual( int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD, "bit 0X%08x should have been removed" % bit) def test_uac_bits_unrelated_modify_normal(self): self.uac_bits_unrelated_modify_helper(UF_NORMAL_ACCOUNT) def test_uac_bits_unrelated_modify_workstation(self): self.uac_bits_unrelated_modify_helper(UF_WORKSTATION_TRUST_ACCOUNT) def test_uac_bits_add(self): computername = self.computernames[0] user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str( user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) invalid_bits = set( [UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT]) # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test priv_to_auth_users_bits = set([ UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED, UF_DONT_EXPIRE_PASSWD ]) # These bits really are privileged priv_bits = set([ UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION ]) for bit in bits: try: self.add_computer_ldap( computername, others={"userAccountControl": [str(bit)]}) delete_force( self.admin_samdb, "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn)) if bit in priv_bits: self.fail( "Unexpectdly able to set userAccountControl bit 0x%08X on %s" % (bit, computername)) except LdbError as e4: (enum, estr) = e4.args if bit in invalid_bits: self.assertEqual( enum, ldb.ERR_OTHER, "Invalid bit 0x%08X was able to be set on %s" % (bit, computername)) # No point going on, try the next bit continue elif bit in priv_bits: self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS) continue else: self.fail( "Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, computername, estr)) def test_primarygroupID_cc_add(self): computername = self.computernames[0] user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str( user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) try: # When creating a new object, you can not ever set the primaryGroupID self.add_computer_ldap( computername, others={"primaryGroupID": [str(security.DOMAIN_RID_ADMINS)]}) self.fail( "Unexpectedly able to set primaryGruopID to be an admin on %s" % computername) except LdbError as e13: (enum, estr) = e13.args self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM) def test_primarygroupID_priv_DC_modify(self): computername = self.computernames[0] self.add_computer_ldap( computername, others={"userAccountControl": [str(UF_SERVER_TRUST_ACCOUNT)]}, samdb=self.admin_samdb) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=[""]) m = ldb.Message() m.dn = ldb.Dn( self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid), security.DOMAIN_RID_USERS)) m["member"] = ldb.MessageElement([str(res[0].dn)], ldb.FLAG_MOD_ADD, "member") self.admin_samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["primaryGroupID"] = ldb.MessageElement( [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE, "primaryGroupID") try: self.admin_samdb.modify(m) # When creating a new object, you can not ever set the primaryGroupID self.fail( "Unexpectedly able to set primaryGroupID to be other than DCS on %s" % computername) except LdbError as e14: (enum, estr) = e14.args self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM) def test_primarygroupID_priv_member_modify(self): computername = self.computernames[0] self.add_computer_ldap(computername, others={ "userAccountControl": [ str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT) ] }, samdb=self.admin_samdb) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=[""]) m = ldb.Message() m.dn = ldb.Dn( self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid), security.DOMAIN_RID_USERS)) m["member"] = ldb.MessageElement([str(res[0].dn)], ldb.FLAG_MOD_ADD, "member") self.admin_samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["primaryGroupID"] = ldb.MessageElement( [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE, "primaryGroupID") try: self.admin_samdb.modify(m) # When creating a new object, you can not ever set the primaryGroupID self.fail( "Unexpectedly able to set primaryGroupID to be other than DCS on %s" % computername) except LdbError as e15: (enum, estr) = e15.args self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM) def test_primarygroupID_priv_user_modify(self): computername = self.computernames[0] self.add_computer_ldap( computername, others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT)]}, samdb=self.admin_samdb) res = self.admin_samdb.search( "%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=[""]) m = ldb.Message() m.dn = ldb.Dn( self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid), security.DOMAIN_RID_ADMINS)) m["member"] = ldb.MessageElement([str(res[0].dn)], ldb.FLAG_MOD_ADD, "member") self.admin_samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["primaryGroupID"] = ldb.MessageElement( [str(security.DOMAIN_RID_ADMINS)], ldb.FLAG_MOD_REPLACE, "primaryGroupID") self.admin_samdb.modify(m)
class UserAccountControlTests(samba.tests.TestCase): def add_computer_ldap(self, computername, others=None, samdb=None): if samdb is None: samdb = self.samdb dn = "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn) domainname = ldb.Dn(self.samdb, self.samdb.domain_dn()).canonical_str().replace("/", "") samaccountname = "%s$" % computername dnshostname = "%s.%s" % (computername, domainname) msg_dict = { "dn": dn, "objectclass": "computer"} if others is not None: msg_dict = dict(msg_dict.items() + others.items()) msg = ldb.Message.from_dict(self.samdb, msg_dict ) msg["sAMAccountName"] = samaccountname print("Adding computer account %s" % computername) samdb.add(msg) def get_creds(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()) creds_tmp.set_gensec_features(creds_tmp.get_gensec_features() | gensec.FEATURE_SEAL) creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop return creds_tmp def setUp(self): super(UserAccountControlTests, self).setUp() self.admin_creds = creds self.admin_samdb = SamDB(url=ldaphost, session_info=system_session(), credentials=self.admin_creds, lp=lp) self.domain_sid = security.dom_sid(self.admin_samdb.get_domain_sid()) self.base_dn = self.admin_samdb.domain_dn() self.unpriv_user = "******" self.unpriv_user_pw = "samba123@" self.unpriv_creds = self.get_creds(self.unpriv_user, self.unpriv_user_pw) delete_force(self.admin_samdb, "CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) delete_force(self.admin_samdb, "OU=test_computer_ou1,%s" % (self.base_dn)) delete_force(self.admin_samdb, "CN=%s,CN=Users,%s" % (self.unpriv_user, self.base_dn)) self.admin_samdb.newuser(self.unpriv_user, self.unpriv_user_pw) res = self.admin_samdb.search("CN=%s,CN=Users,%s" % (self.unpriv_user, self.admin_samdb.domain_dn()), scope=SCOPE_BASE, attrs=["objectSid"]) self.assertEqual(1, len(res)) self.unpriv_user_sid = ndr_unpack(security.dom_sid, res[0]["objectSid"][0]) self.unpriv_user_dn = res[0].dn self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp) self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % host, lp, self.unpriv_creds) self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED) self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid) self.sd_utils = sd_utils.SDUtils(self.admin_samdb) self.admin_samdb.create_ou("OU=test_computer_ou1," + self.base_dn) self.unpriv_user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.unpriv_user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) self.add_computer_ldap("testcomputer-t") self.sd_utils.modify_sd_on_dn("OU=test_computer_ou1," + self.base_dn, old_sd) self.computernames = ["testcomputer-0"] # Get the SD of the template account, then force it to match # what we expect for SeMachineAccountPrivilege accounts, so we # can confirm we created the accounts correctly self.sd_reference_cc = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) self.sd_reference_modify = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) for ace in self.sd_reference_modify.dacl.aces: if ace.type == security.SEC_ACE_TYPE_ACCESS_ALLOWED and ace.trustee == self.unpriv_user_sid: ace.access_mask = ace.access_mask | security.SEC_ADS_SELF_WRITE | security.SEC_ADS_WRITE_PROP # Now reconnect without domain admin rights self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp) def tearDown(self): super(UserAccountControlTests, self).tearDown() for computername in self.computernames: delete_force(self.admin_samdb, "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn)) delete_force(self.admin_samdb, "CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn)) delete_force(self.admin_samdb, "OU=test_computer_ou1,%s" % (self.base_dn)) delete_force(self.admin_samdb, "CN=%s,CN=Users,%s" % (self.unpriv_user, self.base_dn)) def test_add_computer_sd_cc(self): user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) computername=self.computernames[0] sd = ldb.MessageElement((ndr_pack(self.sd_reference_modify)), ldb.FLAG_MOD_ADD, "nTSecurityDescriptor") self.add_computer_ldap(computername, others={"nTSecurityDescriptor": sd}) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["ntSecurityDescriptor"]) desc = res[0]["nTSecurityDescriptor"][0] desc = ndr_unpack(security.descriptor, desc, allow_remaining=True) sddl = desc.as_sddl(self.domain_sid) self.assertEqual(self.sd_reference_modify.as_sddl(self.domain_sid), sddl) m = ldb.Message() m.dn = res[0].dn m["description"]= ldb.MessageElement( ("A description"), ldb.FLAG_MOD_REPLACE, "description") self.samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail("Unexpectedly able to set userAccountControl to be a DC on %s" % m.dn) except LdbError as e5: (enum, estr) = e5.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT|samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail("Unexpectedly able to set userAccountControl to be an RODC on %s" % m.dn) except LdbError as e6: (enum, estr) = e6.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail("Unexpectedly able to set userAccountControl to be an Workstation on %s" % m.dn) except LdbError as e7: (enum, estr) = e7.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["primaryGroupID"] = ldb.MessageElement(str(security.DOMAIN_RID_ADMINS), ldb.FLAG_MOD_REPLACE, "primaryGroupID") try: self.samdb.modify(m) except LdbError as e8: (enum, estr) = e8.args self.assertEqual(ldb.ERR_UNWILLING_TO_PERFORM, enum) return self.fail() def test_mod_computer_cc(self): user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) computername=self.computernames[0] self.add_computer_ldap(computername) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=[]) m = ldb.Message() m.dn = res[0].dn m["description"]= ldb.MessageElement( ("A description"), ldb.FLAG_MOD_REPLACE, "description") self.samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT|samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail("Unexpectedly able to set userAccountControl on %s" % m.dn) except LdbError as e9: (enum, estr) = e9.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail() except LdbError as e10: (enum, estr) = e10.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) self.fail("Unexpectedly able to set userAccountControl to be an Workstation on %s" % m.dn) except LdbError as e11: (enum, estr) = e11.args self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) def test_admin_mod_uac(self): computername=self.computernames[0] self.add_computer_ldap(computername, samdb=self.admin_samdb) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT|UF_TRUSTED_FOR_DELEGATION), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.admin_samdb.modify(m) self.fail("Unexpectedly able to set userAccountControl to UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT|UF_TRUSTED_FOR_DELEGATION on %s" % m.dn) except LdbError as e12: (enum, estr) = e12.args self.assertEqual(ldb.ERR_OTHER, enum) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.admin_samdb.modify(m) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) self.assertEqual(int(res[0]["userAccountControl"][0]), UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(UF_ACCOUNTDISABLE), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.admin_samdb.modify(m) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT| UF_ACCOUNTDISABLE) def test_uac_bits_set(self): user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) computername=self.computernames[0] self.add_computer_ldap(computername) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=[]) m = ldb.Message() m.dn = res[0].dn m["description"]= ldb.MessageElement( ("A description"), ldb.FLAG_MOD_REPLACE, "description") self.samdb.modify(m) # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test priv_to_auth_users_bits = set([UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED, UF_DONT_EXPIRE_PASSWD]) # These bits really are privileged, or can't be changed from UF_NORMAL as a non-admin priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_WORKSTATION_TRUST_ACCOUNT]) invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT]) for bit in bits: m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(bit|UF_PASSWD_NOTREQD), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.samdb.modify(m) if (bit in priv_bits): self.fail("Unexpectedly able to set userAccountControl bit 0x%08X on %s" % (bit, m.dn)) except LdbError as e: (enum, estr) = e.args if bit in invalid_bits: self.assertEqual(enum, ldb.ERR_OTHER, "was not able to set 0x%08X on %s" % (bit, m.dn)) # No point going on, try the next bit continue elif (bit in priv_bits): self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum) else: self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr)) def uac_bits_unrelated_modify_helper(self, account_type): user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) computername=self.computernames[0] self.add_computer_ldap(computername, others={"userAccountControl": [str(account_type)]}) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) self.assertEqual(int(res[0]["userAccountControl"][0]), account_type) m = ldb.Message() m.dn = res[0].dn m["description"]= ldb.MessageElement( ("A description"), ldb.FLAG_MOD_REPLACE, "description") self.samdb.modify(m) invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT]) # UF_LOCKOUT isn't actually ignored, it changes other # attributes but does not stick here. See MS-SAMR 2.2.1.13 # UF_FLAG Codes clarification that UF_SCRIPT and # UF_PASSWD_CANT_CHANGE are simply ignored by both clients and # servers. Other bits are ignored as they are undefined, or # are not set into the attribute (instead triggering other # events). ignored_bits = set([UF_SCRIPT, UF_00000004, UF_LOCKOUT, UF_PASSWD_CANT_CHANGE, UF_00000400, UF_00004000, UF_00008000, UF_PASSWORD_EXPIRED, int("0x10000000", 16), int("0x20000000", 16), int("0x40000000", 16), int("0x80000000", 16)]) super_priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT]) priv_to_remove_bits = set([UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_WORKSTATION_TRUST_ACCOUNT]) for bit in bits: # Reset this to the initial position, just to be sure m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(account_type), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.admin_samdb.modify(m) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) self.assertEqual(int(res[0]["userAccountControl"][0]), account_type) m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(bit|UF_PASSWD_NOTREQD), ldb.FLAG_MOD_REPLACE, "userAccountControl") try: self.admin_samdb.modify(m) if bit in invalid_bits: self.fail("Should have been unable to set userAccountControl bit 0x%08X on %s" % (bit, m.dn)) except LdbError as e1: (enum, estr) = e1.args if bit in invalid_bits: self.assertEqual(enum, ldb.ERR_OTHER) # No point going on, try the next bit continue elif bit in super_priv_bits: self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS) # No point going on, try the next bit continue else: self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr)) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) if bit in ignored_bits: self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD, "Bit 0x%08x shouldn't stick" % bit) else: if bit in account_types: self.assertEqual(int(res[0]["userAccountControl"][0]), bit|UF_PASSWD_NOTREQD, "Bit 0x%08x didn't stick" % bit) else: self.assertEqual(int(res[0]["userAccountControl"][0]), bit|UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD, "Bit 0x%08x didn't stick" % bit) try: m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(bit|UF_PASSWD_NOTREQD|UF_ACCOUNTDISABLE), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.samdb.modify(m) except LdbError as e2: (enum, estr) = e2.args self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr)) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) if bit in account_types: self.assertEqual(int(res[0]["userAccountControl"][0]), bit|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD, "bit 0X%08x should have been added (0X%08x vs 0X%08x)" % (bit, int(res[0]["userAccountControl"][0]), bit|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD)) elif bit in ignored_bits: self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD, "bit 0X%08x should have been added (0X%08x vs 0X%08x)" % (bit, int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD)) else: self.assertEqual(int(res[0]["userAccountControl"][0]), bit|UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD, "bit 0X%08x should have been added (0X%08x vs 0X%08x)" % (bit, int(res[0]["userAccountControl"][0]), bit|UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD)) try: m = ldb.Message() m.dn = res[0].dn m["userAccountControl"] = ldb.MessageElement(str(UF_PASSWD_NOTREQD|UF_ACCOUNTDISABLE), ldb.FLAG_MOD_REPLACE, "userAccountControl") self.samdb.modify(m) if bit in priv_to_remove_bits: self.fail("Should have been unable to remove userAccountControl bit 0x%08X on %s" % (bit, m.dn)) except LdbError as e3: (enum, estr) = e3.args if bit in priv_to_remove_bits: self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS) else: self.fail("Unexpectedly unable to remove userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr)) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=["userAccountControl"]) if bit in priv_to_remove_bits: if bit in account_types: self.assertEqual(int(res[0]["userAccountControl"][0]), bit|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD, "bit 0X%08x should not have been removed" % bit) else: self.assertEqual(int(res[0]["userAccountControl"][0]), bit|UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD, "bit 0X%08x should not have been removed" % bit) else: self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD, "bit 0X%08x should have been removed" % bit) def test_uac_bits_unrelated_modify_normal(self): self.uac_bits_unrelated_modify_helper(UF_NORMAL_ACCOUNT) def test_uac_bits_unrelated_modify_workstation(self): self.uac_bits_unrelated_modify_helper(UF_WORKSTATION_TRUST_ACCOUNT) def test_uac_bits_add(self): computername=self.computernames[0] user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT]) # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test priv_to_auth_users_bits = set([UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED, UF_DONT_EXPIRE_PASSWD]) # These bits really are privileged priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION]) for bit in bits: try: self.add_computer_ldap(computername, others={"userAccountControl": [str(bit)]}) delete_force(self.admin_samdb, "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn)) if bit in priv_bits: self.fail("Unexpectdly able to set userAccountControl bit 0x%08X on %s" % (bit, computername)) except LdbError as e4: (enum, estr) = e4.args if bit in invalid_bits: self.assertEqual(enum, ldb.ERR_OTHER, "Invalid bit 0x%08X was able to be set on %s" % (bit, computername)) # No point going on, try the next bit continue elif bit in priv_bits: self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS) continue else: self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, computername, estr)) def test_primarygroupID_cc_add(self): computername=self.computernames[0] user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn) mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid) old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn) self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod) try: # When creating a new object, you can not ever set the primaryGroupID self.add_computer_ldap(computername, others={"primaryGroupID": [str(security.DOMAIN_RID_ADMINS)]}) self.fail("Unexpectedly able to set primaryGruopID to be an admin on %s" % computername) except LdbError as e13: (enum, estr) = e13.args self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM) def test_primarygroupID_priv_DC_modify(self): computername=self.computernames[0] self.add_computer_ldap(computername, others={"userAccountControl": [str(UF_SERVER_TRUST_ACCOUNT)]}, samdb=self.admin_samdb) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=[""]) m = ldb.Message() m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid), security.DOMAIN_RID_USERS)) m["member"]= ldb.MessageElement( [str(res[0].dn)], ldb.FLAG_MOD_ADD, "member") self.admin_samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["primaryGroupID"]= ldb.MessageElement( [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE, "primaryGroupID") try: self.admin_samdb.modify(m) # When creating a new object, you can not ever set the primaryGroupID self.fail("Unexpectedly able to set primaryGroupID to be other than DCS on %s" % computername) except LdbError as e14: (enum, estr) = e14.args self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM) def test_primarygroupID_priv_member_modify(self): computername=self.computernames[0] self.add_computer_ldap(computername, others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT)]}, samdb=self.admin_samdb) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=[""]) m = ldb.Message() m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid), security.DOMAIN_RID_USERS)) m["member"]= ldb.MessageElement( [str(res[0].dn)], ldb.FLAG_MOD_ADD, "member") self.admin_samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["primaryGroupID"]= ldb.MessageElement( [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE, "primaryGroupID") try: self.admin_samdb.modify(m) # When creating a new object, you can not ever set the primaryGroupID self.fail("Unexpectedly able to set primaryGroupID to be other than DCS on %s" % computername) except LdbError as e15: (enum, estr) = e15.args self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM) def test_primarygroupID_priv_user_modify(self): computername=self.computernames[0] self.add_computer_ldap(computername, others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT)]}, samdb=self.admin_samdb) res = self.admin_samdb.search("%s" % self.base_dn, expression="(&(objectClass=computer)(samAccountName=%s$))" % computername, scope=SCOPE_SUBTREE, attrs=[""]) m = ldb.Message() m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid), security.DOMAIN_RID_ADMINS)) m["member"]= ldb.MessageElement( [str(res[0].dn)], ldb.FLAG_MOD_ADD, "member") self.admin_samdb.modify(m) m = ldb.Message() m.dn = res[0].dn m["primaryGroupID"]= ldb.MessageElement( [str(security.DOMAIN_RID_ADMINS)], ldb.FLAG_MOD_REPLACE, "primaryGroupID") self.admin_samdb.modify(m)
class PrivAttrsTests(samba.tests.TestCase): def get_creds(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()) creds_tmp.set_gensec_features(creds_tmp.get_gensec_features() | gensec.FEATURE_SEAL) creds_tmp.set_kerberos_state( DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop return creds_tmp def assertGotLdbError(self, wanted, got): if not self.strict_checking: self.assertNotEqual(got, ldb.SUCCESS) else: self.assertEqual(got, wanted) def setUp(self): super().setUp() strict_checking = samba.tests.env_get_var_value('STRICT_CHECKING', allow_missing=True) if strict_checking is None: strict_checking = '1' self.strict_checking = bool(int(strict_checking)) self.admin_creds = creds self.admin_samdb = SamDB(url=ldaphost, credentials=self.admin_creds, lp=lp) self.domain_sid = security.dom_sid(self.admin_samdb.get_domain_sid()) self.base_dn = self.admin_samdb.domain_dn() self.unpriv_user = "******" self.unpriv_user_pw = "samba123@" self.unpriv_creds = self.get_creds(self.unpriv_user, self.unpriv_user_pw) self.admin_sd_utils = sd_utils.SDUtils(self.admin_samdb) self.test_ou_name = "OU=test_priv_attrs" self.test_ou = self.test_ou_name + "," + self.base_dn delete_force(self.admin_samdb, self.test_ou, controls=["tree_delete:0"]) self.admin_samdb.create_ou(self.test_ou) expected_user_dn = f"CN={self.unpriv_user},{self.test_ou_name},{self.base_dn}" self.admin_samdb.newuser(self.unpriv_user, self.unpriv_user_pw, userou=self.test_ou_name) res = self.admin_samdb.search(expected_user_dn, scope=SCOPE_BASE, attrs=["objectSid"]) self.assertEqual(1, len(res)) self.unpriv_user_dn = res[0].dn self.addCleanup(delete_force, self.admin_samdb, self.unpriv_user_dn, controls=["tree_delete:0"]) self.unpriv_user_sid = self.admin_sd_utils.get_object_sid( self.unpriv_user_dn) self.unpriv_samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp) @classmethod def setUpDynamicTestCases(cls): for test_name in attrs.keys(): for add_or_mod in ["add", "mod-del-add", "mod-replace"]: for permission in ["admin-add", "CC"]: for sd in ["default", "WP"]: for objectclass in ["computer", "user"]: tname = f"{test_name}_{add_or_mod}_{permission}_{sd}_{objectclass}" targs = (test_name, add_or_mod, permission, sd, objectclass) cls.generate_dynamic_test("test_priv_attr", tname, *targs) def add_computer_ldap(self, computername, others=None, samdb=None): dn = "CN=%s,%s" % (computername, self.test_ou) domainname = ldb.Dn(samdb, samdb.domain_dn()).canonical_str().replace( "/", "") samaccountname = "%s$" % computername dnshostname = "%s.%s" % (computername, domainname) msg_dict = {"dn": dn, "objectclass": "computer"} if others is not None: msg_dict = dict(list(msg_dict.items()) + list(others.items())) msg = ldb.Message.from_dict(samdb, msg_dict) msg["sAMAccountName"] = samaccountname print("Adding computer account %s" % computername) try: samdb.add(msg) except ldb.LdbError: print(msg) raise return msg.dn def add_user_ldap(self, username, others=None, samdb=None): dn = "CN=%s,%s" % (username, self.test_ou) domainname = ldb.Dn(samdb, samdb.domain_dn()).canonical_str().replace( "/", "") samaccountname = "%s$" % username msg_dict = {"dn": dn, "objectclass": "user"} if others is not None: msg_dict = dict(list(msg_dict.items()) + list(others.items())) msg = ldb.Message.from_dict(samdb, msg_dict) msg["sAMAccountName"] = samaccountname print("Adding user account %s" % username) try: samdb.add(msg) except ldb.LdbError: print(msg) raise return msg.dn def add_thing_ldap(self, user, others, samdb, objectclass): if objectclass == "user": dn = self.add_user_ldap(user, others, samdb=samdb) elif objectclass == "computer": dn = self.add_computer_ldap(user, others, samdb=samdb) return dn def _test_priv_attr_with_args(self, test_name, add_or_mod, permission, sd, objectclass): user = "******" if "attr" in attrs[test_name]: attr = attrs[test_name]["attr"] else: attr = test_name if add_or_mod == "add": others = {attr: attrs[test_name]["value"]} else: others = {} if permission == "CC": samdb = self.unpriv_samdb # Set CC on container to allow user add mod = "(OA;CI;CC;bf967aba-0de6-11d0-a285-00aa003049e2;;%s)" % str( self.unpriv_user_sid) self.admin_sd_utils.dacl_add_ace(self.test_ou, mod) mod = "(OA;CI;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str( self.unpriv_user_sid) self.admin_sd_utils.dacl_add_ace(self.test_ou, mod) else: samdb = self.admin_samdb if sd == "WP": # Set SD to WP to the target user as part of add sd = "O:%sG:DUD:(OA;CIID;RPWP;;;%s)(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;%s)" % ( self.unpriv_user_sid, self.unpriv_user_sid, self.unpriv_user_sid) tmp_desc = security.descriptor.from_sddl(sd, self.domain_sid) others["ntSecurityDescriptor"] = ndr_pack(tmp_desc) if add_or_mod == "add": # only-1 and only-2 are due to windows behaviour if "only-1" in attrs[test_name] and \ attrs[test_name]["only-1"] != objectclass: try: dn = self.add_thing_ldap(user, others, samdb, objectclass) self.fail( f"{test_name}: Unexpectedly able to set {attr} on new {objectclass} as ADMIN (should fail LDAP_OBJECT_CLASS_VIOLATION)" ) except LdbError as e5: (enum, estr) = e5.args self.assertGotLdbError(ldb.ERR_OBJECT_CLASS_VIOLATION, enum) elif permission == "CC": try: dn = self.add_thing_ldap(user, others, samdb, objectclass) self.fail( f"{test_name}: Unexpectedly able to set {attr} on new {objectclass}" ) except LdbError as e5: (enum, estr) = e5.args if "unpriv-add-error" in attrs[test_name]: self.assertGotLdbError(attrs[test_name]["unpriv-add-error"], \ enum) else: self.assertGotLdbError(attrs[test_name]["unpriv-error"], \ enum) elif "only-2" in attrs[test_name] and \ attrs[test_name]["only-2"] != objectclass: try: dn = self.add_thing_ldap(user, others, samdb, objectclass) self.fail( f"{test_name}: Unexpectedly able to set {attr} on new {objectclass} as ADMIN (should fail LDAP_OBJECT_CLASS_VIOLATION)" ) except LdbError as e5: (enum, estr) = e5.args self.assertGotLdbError(ldb.ERR_OBJECT_CLASS_VIOLATION, enum) elif "priv-error" in attrs[test_name]: try: dn = self.add_thing_ldap(user, others, samdb, objectclass) self.fail( f"{test_name}: Unexpectedly able to set {attr} on new {objectclass} as ADMIN" ) except LdbError as e5: (enum, estr) = e5.args self.assertGotLdbError(attrs[test_name]["priv-error"], enum) else: try: dn = self.add_thing_ldap(user, others, samdb, objectclass) except LdbError as e5: (enum, estr) = e5.args self.fail( f"Failed to add account {user} as objectclass {objectclass}" ) else: try: dn = self.add_thing_ldap(user, others, samdb, objectclass) except LdbError as e5: (enum, estr) = e5.args self.fail( f"Failed to add account {user} as objectclass {objectclass}" ) if add_or_mod == "add": return m = ldb.Message() m.dn = dn # Do modify if add_or_mod == "mod-del-add": m["0"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr) m["1"] = ldb.MessageElement(attrs[test_name]["value"], ldb.FLAG_MOD_ADD, attr) else: m["0"] = ldb.MessageElement(attrs[test_name]["value"], ldb.FLAG_MOD_REPLACE, attr) try: self.unpriv_samdb.modify(m) self.fail( f"{test_name}: Unexpectedly able to set {attr} on {m.dn}") except LdbError as e5: (enum, estr) = e5.args self.assertGotLdbError(attrs[test_name]["unpriv-error"], enum)