CVE-2015-8467: samdb: Match MS15-096 behaviour for userAccountControl
[samba.git] / source4 / dsdb / tests / python / user_account_control.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # This tests the restrictions on userAccountControl that apply even if write access is permitted
4 #
5 # Copyright Samuel Cabrero 2014 <samuelcabrero@kernevil.me>
6 # Copyright Andrew Bartlett 2014 <abartlet@samba.org>
7 #
8 # Licenced under the GPLv3
9 #
10
11 import optparse
12 import sys
13 import unittest
14 import samba
15 import samba.getopt as options
16 import samba.tests
17 import ldb
18 import base64
19
20 sys.path.insert(0, "bin/python")
21 from samba.tests.subunitrun import TestProgram, SubunitOptions
22
23 from samba.subunit.run import SubunitTestRunner
24 from samba.auth import system_session
25 from samba.samdb import SamDB
26 from samba.dcerpc import samr, security, lsa
27 from samba.credentials import Credentials
28 from samba.ndr import ndr_unpack, ndr_pack
29 from samba.tests import delete_force
30 from samba import gensec, sd_utils
31 from samba.credentials import DONT_USE_KERBEROS
32 from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
33 from ldb import Message, MessageElement, Dn
34 from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
35 from samba.dsdb import UF_SCRIPT, UF_ACCOUNTDISABLE, UF_00000004, UF_HOMEDIR_REQUIRED, \
36     UF_LOCKOUT,UF_PASSWD_NOTREQD, UF_PASSWD_CANT_CHANGE, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,\
37     UF_TEMP_DUPLICATE_ACCOUNT, UF_NORMAL_ACCOUNT, UF_00000400, UF_INTERDOMAIN_TRUST_ACCOUNT, \
38     UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_00004000, \
39     UF_00008000, UF_DONT_EXPIRE_PASSWD, UF_MNS_LOGON_ACCOUNT, UF_SMARTCARD_REQUIRED, \
40     UF_TRUSTED_FOR_DELEGATION, UF_NOT_DELEGATED, UF_USE_DES_KEY_ONLY, UF_DONT_REQUIRE_PREAUTH, \
41     UF_PASSWORD_EXPIRED, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_NO_AUTH_DATA_REQUIRED, \
42     UF_PARTIAL_SECRETS_ACCOUNT, UF_USE_AES_KEYS
43
44
45 parser = optparse.OptionParser("machine_account_privilege.py [options] <host>")
46 sambaopts = options.SambaOptions(parser)
47 parser.add_option_group(sambaopts)
48 parser.add_option_group(options.VersionOptions(parser))
49
50 # use command line creds if available
51 credopts = options.CredentialsOptions(parser)
52 parser.add_option_group(credopts)
53 opts, args = parser.parse_args()
54
55 if len(args) < 1:
56     parser.print_usage()
57     sys.exit(1)
58 host = args[0]
59
60 if not "://" in host:
61     ldaphost = "ldap://%s" % host
62 else:
63     ldaphost = host
64     start = host.rindex("://")
65     host = host.lstrip(start+3)
66
67 lp = sambaopts.get_loadparm()
68 creds = credopts.get_credentials(lp)
69 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
70
71 bits = [UF_SCRIPT, UF_ACCOUNTDISABLE, UF_00000004, UF_HOMEDIR_REQUIRED,
72         UF_LOCKOUT,UF_PASSWD_NOTREQD, UF_PASSWD_CANT_CHANGE,
73         UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
74         UF_TEMP_DUPLICATE_ACCOUNT, UF_NORMAL_ACCOUNT, UF_00000400,
75         UF_INTERDOMAIN_TRUST_ACCOUNT,
76         UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_00004000,
77         UF_00008000, UF_DONT_EXPIRE_PASSWD, UF_MNS_LOGON_ACCOUNT, UF_SMARTCARD_REQUIRED,
78         UF_TRUSTED_FOR_DELEGATION, UF_NOT_DELEGATED, UF_USE_DES_KEY_ONLY,
79         UF_DONT_REQUIRE_PREAUTH,
80         UF_PASSWORD_EXPIRED, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
81         UF_NO_AUTH_DATA_REQUIRED,
82         UF_PARTIAL_SECRETS_ACCOUNT, UF_USE_AES_KEYS,
83         int("0x10000000", 16), int("0x20000000", 16), int("0x40000000", 16), int("0x80000000", 16)]
84
85 account_types = set([UF_NORMAL_ACCOUNT, UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT])
86
87
88 class UserAccountControlTests(samba.tests.TestCase):
89     def add_computer_ldap(self, computername, others=None, samdb=None):
90         if samdb is None:
91             samdb = self.samdb
92         dn = "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn)
93         domainname = ldb.Dn(self.samdb, self.samdb.domain_dn()).canonical_str().replace("/", "")
94         samaccountname = "%s$" % computername
95         dnshostname = "%s.%s" % (computername, domainname)
96         msg_dict = {
97             "dn": dn,
98             "objectclass": "computer"}
99         if others is not None:
100             msg_dict = dict(msg_dict.items() + others.items())
101
102         msg = ldb.Message.from_dict(self.samdb, msg_dict )
103         msg["sAMAccountName"] = samaccountname
104
105         print "Adding computer account %s" % computername
106         samdb.add(msg)
107
108     def get_creds(self, target_username, target_password):
109         creds_tmp = Credentials()
110         creds_tmp.set_username(target_username)
111         creds_tmp.set_password(target_password)
112         creds_tmp.set_domain(creds.get_domain())
113         creds_tmp.set_realm(creds.get_realm())
114         creds_tmp.set_workstation(creds.get_workstation())
115         creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
116                                       | gensec.FEATURE_SEAL)
117         creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
118         return creds_tmp
119
120     def setUp(self):
121         super(UserAccountControlTests, self).setUp()
122         self.admin_creds = creds
123         self.admin_samdb = SamDB(url=ldaphost,
124                                  session_info=system_session(),
125                                  credentials=self.admin_creds, lp=lp)
126
127         self.unpriv_user = "testuser1"
128         self.unpriv_user_pw = "samba123@"
129         self.unpriv_creds = self.get_creds(self.unpriv_user, self.unpriv_user_pw)
130
131         self.admin_samdb.newuser(self.unpriv_user, self.unpriv_user_pw)
132         res = self.admin_samdb.search("CN=%s,CN=Users,%s" % (self.unpriv_user, self.admin_samdb.domain_dn()),
133                                       scope=SCOPE_BASE,
134                                       attrs=["objectSid"])
135         self.assertEqual(1, len(res))
136
137         self.unpriv_user_sid = ndr_unpack(security.dom_sid, res[0]["objectSid"][0])
138         self.unpriv_user_dn = res[0].dn
139
140         self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp)
141         self.domain_sid = security.dom_sid(self.samdb.get_domain_sid())
142         self.base_dn = self.samdb.domain_dn()
143
144         self.samr = samr.samr("ncacn_ip_tcp:%s[sign]" % host, lp, self.unpriv_creds)
145         self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
146         self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid)
147
148         self.sd_utils = sd_utils.SDUtils(self.admin_samdb)
149
150         self.admin_samdb.create_ou("OU=test_computer_ou1," + self.base_dn)
151         self.unpriv_user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
152         mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.unpriv_user_sid)
153
154         old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
155
156         self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
157
158         self.add_computer_ldap("testcomputer-t")
159
160         self.sd_utils.modify_sd_on_dn("OU=test_computer_ou1," + self.base_dn, old_sd)
161
162         self.computernames = ["testcomputer-0"]
163
164         # Get the SD of the template account, then force it to match
165         # what we expect for SeMachineAccountPrivilege accounts, so we
166         # can confirm we created the accounts correctly
167         self.sd_reference_cc = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn))
168
169         self.sd_reference_modify = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn))
170         for ace in self.sd_reference_modify.dacl.aces:
171             if ace.type == security.SEC_ACE_TYPE_ACCESS_ALLOWED and ace.trustee == self.unpriv_user_sid:
172                 ace.access_mask = ace.access_mask | security.SEC_ADS_SELF_WRITE | security.SEC_ADS_WRITE_PROP
173
174         # Now reconnect without domain admin rights
175         self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp)
176
177
178     def tearDown(self):
179         super(UserAccountControlTests, self).tearDown()
180         for computername in self.computernames:
181             delete_force(self.admin_samdb, "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn))
182         delete_force(self.admin_samdb, "CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn))
183         delete_force(self.admin_samdb, "OU=test_computer_ou1,%s" % (self.base_dn))
184         delete_force(self.admin_samdb, "CN=%s,CN=Users,%s" % (self.unpriv_user, self.base_dn))
185
186     def test_add_computer_sd_cc(self):
187         user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
188         mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
189
190         old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
191
192         self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
193
194         computername=self.computernames[0]
195         sd = ldb.MessageElement((ndr_pack(self.sd_reference_modify)),
196                                 ldb.FLAG_MOD_ADD,
197                                 "nTSecurityDescriptor")
198         self.add_computer_ldap(computername,
199                                others={"nTSecurityDescriptor": sd})
200
201         res = self.admin_samdb.search("%s" % self.base_dn,
202                                       expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
203                                       scope=SCOPE_SUBTREE,
204                                       attrs=["ntSecurityDescriptor"])
205
206         desc = res[0]["nTSecurityDescriptor"][0]
207         desc = ndr_unpack(security.descriptor, desc, allow_remaining=True)
208
209         sddl = desc.as_sddl(self.domain_sid)
210         self.assertEqual(self.sd_reference_modify.as_sddl(self.domain_sid), sddl)
211
212         m = ldb.Message()
213         m.dn = res[0].dn
214         m["description"]= ldb.MessageElement(
215             ("A description"), ldb.FLAG_MOD_REPLACE,
216             "description")
217         self.samdb.modify(m)
218
219         m = ldb.Message()
220         m.dn = res[0].dn
221         m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT),
222                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
223         try:
224             self.samdb.modify(m)
225             self.fail("Unexpectedly able to set userAccountControl to be a DC on %s" % m.dn)
226         except LdbError, (enum, estr):
227             self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
228
229         m = ldb.Message()
230         m.dn = res[0].dn
231         m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT|samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT),
232                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
233         try:
234             self.samdb.modify(m)
235             self.fail("Unexpectedly able to set userAccountControl to be an RODC on %s" % m.dn)
236         except LdbError, (enum, estr):
237             self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
238
239         m = ldb.Message()
240         m.dn = res[0].dn
241         m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
242                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
243         try:
244             self.samdb.modify(m)
245             self.fail("Unexpectedly able to set userAccountControl to be an Workstation on %s" % m.dn)
246         except LdbError, (enum, estr):
247             self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
248
249         m = ldb.Message()
250         m.dn = res[0].dn
251         m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT),
252                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
253         self.samdb.modify(m)
254
255         m = ldb.Message()
256         m.dn = res[0].dn
257         m["primaryGroupID"] = ldb.MessageElement(str(security.DOMAIN_RID_ADMINS),
258                                                  ldb.FLAG_MOD_REPLACE, "primaryGroupID")
259         try:
260             self.samdb.modify(m)
261         except LdbError, (enum, estr):
262             self.assertEqual(ldb.ERR_UNWILLING_TO_PERFORM, enum)
263             return
264         self.fail()
265
266     def test_mod_computer_cc(self):
267         user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
268         mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
269
270         old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
271
272         self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
273
274         computername=self.computernames[0]
275         self.add_computer_ldap(computername)
276
277         res = self.admin_samdb.search("%s" % self.base_dn,
278                                       expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
279                                       scope=SCOPE_SUBTREE,
280                                       attrs=[])
281
282         m = ldb.Message()
283         m.dn = res[0].dn
284         m["description"]= ldb.MessageElement(
285             ("A description"), ldb.FLAG_MOD_REPLACE,
286             "description")
287         self.samdb.modify(m)
288
289         m = ldb.Message()
290         m.dn = res[0].dn
291         m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT|samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT),
292                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
293         try:
294             self.samdb.modify(m)
295             self.fail("Unexpectedly able to set userAccountControl on %s" % m.dn)
296         except LdbError, (enum, estr):
297             self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
298
299         m = ldb.Message()
300         m.dn = res[0].dn
301         m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT),
302                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
303         try:
304              self.samdb.modify(m)
305              self.fail()
306         except LdbError, (enum, estr):
307              self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
308
309         m = ldb.Message()
310         m.dn = res[0].dn
311         m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT),
312                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
313         self.samdb.modify(m)
314
315         m = ldb.Message()
316         m.dn = res[0].dn
317         m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
318                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
319         try:
320             self.samdb.modify(m)
321             self.fail("Unexpectedly able to set userAccountControl to be an Workstation on %s" % m.dn)
322         except LdbError, (enum, estr):
323             self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
324
325
326     def test_admin_mod_uac(self):
327         computername=self.computernames[0]
328         self.add_computer_ldap(computername, samdb=self.admin_samdb)
329
330         res = self.admin_samdb.search("%s" % self.base_dn,
331                                       expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
332                                       scope=SCOPE_SUBTREE,
333                                       attrs=["userAccountControl"])
334
335         self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD)
336
337         m = ldb.Message()
338         m.dn = res[0].dn
339         m["userAccountControl"] = ldb.MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT|UF_TRUSTED_FOR_DELEGATION),
340                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
341         try:
342             self.admin_samdb.modify(m)
343             self.fail("Unexpectedly able to set userAccountControl to UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT|UF_TRUSTED_FOR_DELEGATION on %s" % m.dn)
344         except LdbError, (enum, estr):
345             self.assertEqual(ldb.ERR_OTHER, enum)
346
347         m = ldb.Message()
348         m.dn = res[0].dn
349         m["userAccountControl"] = ldb.MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT),
350                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
351         self.admin_samdb.modify(m)
352
353         res = self.admin_samdb.search("%s" % self.base_dn,
354                                       expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
355                                       scope=SCOPE_SUBTREE,
356                                       attrs=["userAccountControl"])
357
358         self.assertEqual(int(res[0]["userAccountControl"][0]), UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT)
359         m = ldb.Message()
360         m.dn = res[0].dn
361         m["userAccountControl"] = ldb.MessageElement(str(UF_ACCOUNTDISABLE),
362                                                      ldb.FLAG_MOD_REPLACE, "userAccountControl")
363         self.admin_samdb.modify(m)
364
365         res = self.admin_samdb.search("%s" % self.base_dn,
366                                       expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
367                                       scope=SCOPE_SUBTREE,
368                                       attrs=["userAccountControl"])
369
370         self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT| UF_ACCOUNTDISABLE)
371
372
373     def test_uac_bits_set(self):
374         user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
375         mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
376
377         old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
378
379         self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
380
381         computername=self.computernames[0]
382         self.add_computer_ldap(computername)
383
384         res = self.admin_samdb.search("%s" % self.base_dn,
385                                       expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
386                                       scope=SCOPE_SUBTREE,
387                                       attrs=[])
388
389         m = ldb.Message()
390         m.dn = res[0].dn
391         m["description"]= ldb.MessageElement(
392             ("A description"), ldb.FLAG_MOD_REPLACE,
393             "description")
394         self.samdb.modify(m)
395
396         # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test
397         priv_to_auth_users_bits = set([UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
398                                        UF_DONT_EXPIRE_PASSWD])
399
400         # These bits really are privileged, or can't be changed from UF_NORMAL as a non-admin
401         priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT,
402                          UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
403                          UF_WORKSTATION_TRUST_ACCOUNT])
404
405         invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT])
406
407         for bit in bits:
408             m = ldb.Message()
409             m.dn = res[0].dn
410             m["userAccountControl"] = ldb.MessageElement(str(bit|UF_PASSWD_NOTREQD),
411                                                          ldb.FLAG_MOD_REPLACE, "userAccountControl")
412             try:
413                 self.samdb.modify(m)
414                 if (bit in priv_bits):
415                     self.fail("Unexpectedly able to set userAccountControl bit 0x%08X on %s" % (bit, m.dn))
416             except LdbError, (enum, estr):
417                 if bit in invalid_bits:
418                     self.assertEqual(enum, ldb.ERR_OTHER, "was not able to set 0x%08X on %s" % (bit, m.dn))
419                     # No point going on, try the next bit
420                     continue
421                 elif (bit in priv_bits):
422                     self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
423                 else:
424                     self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
425
426
427     def uac_bits_unrelated_modify_helper(self, account_type):
428         user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
429         mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
430
431         old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
432
433         self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
434
435         computername=self.computernames[0]
436         self.add_computer_ldap(computername, others={"userAccountControl": [str(account_type)]})
437
438         res = self.admin_samdb.search("%s" % self.base_dn,
439                                       expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
440                                       scope=SCOPE_SUBTREE,
441                                       attrs=["userAccountControl"])
442         self.assertEqual(int(res[0]["userAccountControl"][0]), account_type)
443
444         m = ldb.Message()
445         m.dn = res[0].dn
446         m["description"]= ldb.MessageElement(
447             ("A description"), ldb.FLAG_MOD_REPLACE,
448             "description")
449         self.samdb.modify(m)
450
451         invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT])
452
453         # UF_LOCKOUT isn't actually ignored, it changes other
454         # attributes but does not stick here.  See MS-SAMR 2.2.1.13
455         # UF_FLAG Codes clarification that UF_SCRIPT and
456         # UF_PASSWD_CANT_CHANGE are simply ignored by both clients and
457         # servers.  Other bits are ignored as they are undefined, or
458         # are not set into the attribute (instead triggering other
459         # events).
460         ignored_bits = set([UF_SCRIPT, UF_00000004, UF_LOCKOUT, UF_PASSWD_CANT_CHANGE,
461                             UF_00000400, UF_00004000, UF_00008000, UF_PASSWORD_EXPIRED,
462                             int("0x10000000", 16), int("0x20000000", 16), int("0x40000000", 16), int("0x80000000", 16)])
463         super_priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT])
464
465         priv_to_remove_bits = set([UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_WORKSTATION_TRUST_ACCOUNT])
466
467         for bit in bits:
468             # Reset this to the initial position, just to be sure
469             m = ldb.Message()
470             m.dn = res[0].dn
471             m["userAccountControl"] = ldb.MessageElement(str(account_type),
472                                                          ldb.FLAG_MOD_REPLACE, "userAccountControl")
473             self.admin_samdb.modify(m)
474
475             res = self.admin_samdb.search("%s" % self.base_dn,
476                                           expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
477                                           scope=SCOPE_SUBTREE,
478                                           attrs=["userAccountControl"])
479
480             self.assertEqual(int(res[0]["userAccountControl"][0]), account_type)
481
482             m = ldb.Message()
483             m.dn = res[0].dn
484             m["userAccountControl"] = ldb.MessageElement(str(bit|UF_PASSWD_NOTREQD),
485                                                          ldb.FLAG_MOD_REPLACE, "userAccountControl")
486             try:
487                 self.admin_samdb.modify(m)
488                 if bit in invalid_bits:
489                     self.fail("Should have been unable to set userAccountControl bit 0x%08X on %s" % (bit, m.dn))
490
491             except LdbError, (enum, estr):
492                 if bit in invalid_bits:
493                     self.assertEqual(enum, ldb.ERR_OTHER)
494                     # No point going on, try the next bit
495                     continue
496                 elif bit in super_priv_bits:
497                     self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
498                     # No point going on, try the next bit
499                     continue
500                 else:
501                     self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
502
503             res = self.admin_samdb.search("%s" % self.base_dn,
504                                           expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
505                                           scope=SCOPE_SUBTREE,
506                                           attrs=["userAccountControl"])
507
508             if bit in ignored_bits:
509                 self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD, "Bit 0x%08x shouldn't stick" % bit)
510             else:
511                 if bit in account_types:
512                     self.assertEqual(int(res[0]["userAccountControl"][0]), bit|UF_PASSWD_NOTREQD, "Bit 0x%08x didn't stick" % bit)
513                 else:
514                     self.assertEqual(int(res[0]["userAccountControl"][0]), bit|UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD, "Bit 0x%08x didn't stick" % bit)
515
516             try:
517                 m = ldb.Message()
518                 m.dn = res[0].dn
519                 m["userAccountControl"] = ldb.MessageElement(str(bit|UF_PASSWD_NOTREQD|UF_ACCOUNTDISABLE),
520                                                              ldb.FLAG_MOD_REPLACE, "userAccountControl")
521                 self.samdb.modify(m)
522
523             except LdbError, (enum, estr):
524                 self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
525
526             res = self.admin_samdb.search("%s" % self.base_dn,
527                                           expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
528                                           scope=SCOPE_SUBTREE,
529                                           attrs=["userAccountControl"])
530
531             if bit in account_types:
532                 self.assertEqual(int(res[0]["userAccountControl"][0]),
533                                  bit|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD,
534                                  "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
535                                  % (bit, int(res[0]["userAccountControl"][0]),
536                                     bit|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD))
537             elif bit in ignored_bits:
538                 self.assertEqual(int(res[0]["userAccountControl"][0]),
539                                  UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD,
540                                  "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
541                                  % (bit, int(res[0]["userAccountControl"][0]),
542                                     UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD))
543
544             else:
545                 self.assertEqual(int(res[0]["userAccountControl"][0]),
546                                  bit|UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD,
547                                  "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
548                                  % (bit, int(res[0]["userAccountControl"][0]),
549                                     bit|UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD))
550
551             try:
552                 m = ldb.Message()
553                 m.dn = res[0].dn
554                 m["userAccountControl"] = ldb.MessageElement(str(UF_PASSWD_NOTREQD|UF_ACCOUNTDISABLE),
555                                                              ldb.FLAG_MOD_REPLACE, "userAccountControl")
556                 self.samdb.modify(m)
557                 if bit in priv_to_remove_bits:
558                     self.fail("Should have been unable to remove userAccountControl bit 0x%08X on %s" % (bit, m.dn))
559
560             except LdbError, (enum, estr):
561                 if bit in priv_to_remove_bits:
562                     self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
563                 else:
564                     self.fail("Unexpectedly unable to remove userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
565
566             res = self.admin_samdb.search("%s" % self.base_dn,
567                                           expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
568                                           scope=SCOPE_SUBTREE,
569                                           attrs=["userAccountControl"])
570
571             if bit in priv_to_remove_bits:
572                 if bit in account_types:
573                     self.assertEqual(int(res[0]["userAccountControl"][0]),
574                                      bit|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD,
575                                      "bit 0X%08x should not have been removed" % bit)
576                 else:
577                     self.assertEqual(int(res[0]["userAccountControl"][0]),
578                                      bit|UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD,
579                                      "bit 0X%08x should not have been removed" % bit)
580             else:
581                 self.assertEqual(int(res[0]["userAccountControl"][0]),
582                                  UF_NORMAL_ACCOUNT|UF_ACCOUNTDISABLE|UF_PASSWD_NOTREQD,
583                                  "bit 0X%08x should have been removed" % bit)
584
585     def test_uac_bits_unrelated_modify_normal(self):
586         self.uac_bits_unrelated_modify_helper(UF_NORMAL_ACCOUNT)
587
588     def test_uac_bits_unrelated_modify_workstation(self):
589         self.uac_bits_unrelated_modify_helper(UF_WORKSTATION_TRUST_ACCOUNT)
590
591     def test_uac_bits_add(self):
592         computername=self.computernames[0]
593
594         user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
595         mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
596
597         old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
598
599         self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
600
601         invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT])
602         # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test
603         priv_to_auth_users_bits = set([UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
604                                        UF_DONT_EXPIRE_PASSWD])
605
606         # These bits really are privileged
607         priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT,
608                          UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION])
609
610         for bit in bits:
611             try:
612                 self.add_computer_ldap(computername, others={"userAccountControl": [str(bit)]})
613                 delete_force(self.admin_samdb, "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn))
614                 if bit in priv_bits:
615                     self.fail("Unexpectdly able to set userAccountControl bit 0x%08X on %s" % (bit, computername))
616
617             except LdbError, (enum, estr):
618                 if bit in invalid_bits:
619                     self.assertEqual(enum, ldb.ERR_OTHER, "Invalid bit 0x%08X was able to be set on %s" % (bit, computername))
620                     # No point going on, try the next bit
621                     continue
622                 elif bit in priv_bits:
623                     self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
624                     continue
625                 else:
626                     self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, computername, estr))
627
628     def test_primarygroupID_cc_add(self):
629         computername=self.computernames[0]
630
631         user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
632         mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
633
634         old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
635
636         self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
637         try:
638             # When creating a new object, you can not ever set the primaryGroupID
639             self.add_computer_ldap(computername, others={"primaryGroupID": [str(security.DOMAIN_RID_ADMINS)]})
640             self.fail("Unexpectedly able to set primaryGruopID to be an admin on %s" % computername)
641         except LdbError, (enum, estr):
642             self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
643
644
645     def test_primarygroupID_priv_DC_modify(self):
646         computername=self.computernames[0]
647
648         self.add_computer_ldap(computername,
649                                others={"userAccountControl": [str(UF_SERVER_TRUST_ACCOUNT)]},
650                                samdb=self.admin_samdb)
651         res = self.admin_samdb.search("%s" % self.base_dn,
652                                       expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
653                                       scope=SCOPE_SUBTREE,
654                                       attrs=[""])
655
656
657         m = ldb.Message()
658         m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
659                                                          security.DOMAIN_RID_USERS))
660         m["member"]= ldb.MessageElement(
661             [str(res[0].dn)], ldb.FLAG_MOD_ADD,
662             "member")
663         self.admin_samdb.modify(m)
664
665         m = ldb.Message()
666         m.dn = res[0].dn
667         m["primaryGroupID"]= ldb.MessageElement(
668             [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE,
669             "primaryGroupID")
670         try:
671             self.admin_samdb.modify(m)
672
673             # When creating a new object, you can not ever set the primaryGroupID
674             self.fail("Unexpectedly able to set primaryGroupID to be other than DCS on %s" % computername)
675         except LdbError, (enum, estr):
676             self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
677
678     def test_primarygroupID_priv_member_modify(self):
679         computername=self.computernames[0]
680
681         self.add_computer_ldap(computername,
682                                others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT)]},
683                                samdb=self.admin_samdb)
684         res = self.admin_samdb.search("%s" % self.base_dn,
685                                       expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
686                                       scope=SCOPE_SUBTREE,
687                                       attrs=[""])
688
689
690         m = ldb.Message()
691         m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
692                                                          security.DOMAIN_RID_USERS))
693         m["member"]= ldb.MessageElement(
694             [str(res[0].dn)], ldb.FLAG_MOD_ADD,
695             "member")
696         self.admin_samdb.modify(m)
697
698         m = ldb.Message()
699         m.dn = res[0].dn
700         m["primaryGroupID"]= ldb.MessageElement(
701             [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE,
702             "primaryGroupID")
703         try:
704             self.admin_samdb.modify(m)
705
706             # When creating a new object, you can not ever set the primaryGroupID
707             self.fail("Unexpectedly able to set primaryGroupID to be other than DCS on %s" % computername)
708         except LdbError, (enum, estr):
709             self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
710
711
712     def test_primarygroupID_priv_user_modify(self):
713         computername=self.computernames[0]
714
715         self.add_computer_ldap(computername,
716                                others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT)]},
717                                samdb=self.admin_samdb)
718         res = self.admin_samdb.search("%s" % self.base_dn,
719                                       expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
720                                       scope=SCOPE_SUBTREE,
721                                       attrs=[""])
722
723
724         m = ldb.Message()
725         m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
726                                                          security.DOMAIN_RID_ADMINS))
727         m["member"]= ldb.MessageElement(
728             [str(res[0].dn)], ldb.FLAG_MOD_ADD,
729             "member")
730         self.admin_samdb.modify(m)
731
732         m = ldb.Message()
733         m.dn = res[0].dn
734         m["primaryGroupID"]= ldb.MessageElement(
735             [str(security.DOMAIN_RID_ADMINS)], ldb.FLAG_MOD_REPLACE,
736             "primaryGroupID")
737         self.admin_samdb.modify(m)
738
739
740 runner = SubunitTestRunner()
741 rc = 0
742 if not runner.run(unittest.makeSuite(UserAccountControlTests)).wasSuccessful():
743     rc = 1
744 sys.exit(rc)