1 # Samba4 Domain update checker
3 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 from base64
import b64encode
21 from samba
import sd_utils
22 from samba
.dcerpc
import security
23 from samba
.descriptor
import (
24 get_managed_service_accounts_descriptor
,
26 from samba
.dsdb
import (
27 DS_DOMAIN_FUNCTION_2008
,
28 DS_DOMAIN_FUNCTION_2008_R2
,
29 DS_DOMAIN_FUNCTION_2012
,
30 DS_DOMAIN_FUNCTION_2012_R2
,
31 DS_DOMAIN_FUNCTION_2016
,
38 # Missing updates from 2008 R2 - version 5
39 75: "5e1574f6-55df-493e-a671-aaeffca6a100",
40 76: "d262aae8-41f7-48ed-9f35-56bbb677573d",
41 77: "82112ba0-7e4c-4a44-89d9-d46c9612bf91",
42 # Windows Server 2012 - version 9
43 78: "c3c927a6-cc1d-47c0-966b-be8f9b63d991",
44 79: "54afcfb9-637a-4251-9f47-4d50e7021211",
45 80: "f4728883-84dd-483c-9897-274f2ebcf11e",
46 81: "ff4f9d27-7157-4cb0-80a9-5d6f2b14c8ff",
47 # Windows Server 2012 R2 - version 10
49 # Windows Server 2016 - version 15
50 82: "83c53da7-427e-47a4-a07a-a324598b88f7",
51 # from the documentation and a fresh installation
53 # c81fc9cc-0130-4fd1-b272-634d74818133
54 # adprep will use this on the wire:
55 # c81fc9cc-0130-f4d1-b272-634d74818133
56 83: "c81fc9cc-0130-4fd1-b272-634d74818133",
57 84: "e5f9e791-d96d-4fc9-93c9-d53e1dc439ba",
58 85: "e6d5fd00-385d-4e65-b02d-9da3493ed850",
59 86: "3a6b3fbf-3168-4312-a10d-dd5b3393952d",
60 87: "7f950403-0ab3-47f9-9730-5d7b0269f9bd",
61 88: "434bb40d-dbc9-4fe7-81d4-d57229f7b080",
62 # Windows Server 2016 - version 16
63 89: "a0c238ba-9e30-4ee6-80a6-43f731e9a5cd",
67 functional_level_to_max_update
= {
68 DS_DOMAIN_FUNCTION_2008
: 74,
69 DS_DOMAIN_FUNCTION_2008_R2
: 77,
70 DS_DOMAIN_FUNCTION_2012
: 81,
71 DS_DOMAIN_FUNCTION_2012_R2
: 81,
72 DS_DOMAIN_FUNCTION_2016
: 89,
75 functional_level_to_version
= {
76 DS_DOMAIN_FUNCTION_2008
: 3,
77 DS_DOMAIN_FUNCTION_2008_R2
: 5,
78 DS_DOMAIN_FUNCTION_2012
: 9,
79 DS_DOMAIN_FUNCTION_2012_R2
: 10,
80 DS_DOMAIN_FUNCTION_2016
: 16,
83 # No update numbers have been skipped over
87 class DomainUpdateException(Exception):
91 class DomainUpdate(object):
92 """Check and update a SAM database for domain updates"""
94 def __init__(self
, samdb
, fix
=False,
95 add_update_container
=True):
97 :param samdb: LDB database
98 :param fix: Apply the update if the container is missing
99 :param add_update_container: Add the container at the end of the change
100 :raise DomainUpdateException:
104 self
.add_update_container
= add_update_container
105 # TODO: In future we should check for inconsistencies when it claims it has been done
106 self
.check_update_applied
= False
108 self
.config_dn
= self
.samdb
.get_config_basedn()
109 self
.domain_dn
= self
.samdb
.domain_dn()
110 self
.schema_dn
= self
.samdb
.get_schema_basedn()
112 self
.sd_utils
= sd_utils
.SDUtils(samdb
)
113 self
.domain_sid
= security
.dom_sid(samdb
.get_domain_sid())
115 self
.domainupdate_container
= self
.samdb
.get_root_basedn()
117 self
.domainupdate_container
.add_child("CN=Operations,CN=DomainUpdates,CN=System")
119 raise DomainUpdateException("Failed to add domain update container child")
121 self
.revision_object
= self
.samdb
.get_root_basedn()
123 self
.revision_object
.add_child("CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System")
125 raise DomainUpdateException("Failed to add revision object child")
127 def check_updates_functional_level(self
, functional_level
,
128 old_functional_level
=None,
129 update_revision
=False):
131 Apply all updates for a given old and new functional level
132 :param functional_level: constant
133 :param old_functional_level: constant
134 :param update_revision: modify the stored version
135 :raise DomainUpdateException:
137 res
= self
.samdb
.search(base
=self
.revision_object
,
138 attrs
=["revision"], scope
=ldb
.SCOPE_BASE
)
140 expected_update
= functional_level_to_max_update
[functional_level
]
142 if old_functional_level
:
143 min_update
= functional_level_to_max_update
[old_functional_level
]
146 min_update
= MIN_UPDATE
148 self
.check_updates_range(min_update
, expected_update
)
150 expected_version
= functional_level_to_version
[functional_level
]
151 found_version
= int(res
[0]['revision'][0])
152 if update_revision
and found_version
< expected_version
:
154 raise DomainUpdateException("Revision is not high enough. Fix is set to False."
155 "\nExpected: %dGot: %d" % (expected_version
,
157 self
.samdb
.modify_ldif("""dn: %s
161 """ % (str(self
.revision_object
), expected_version
))
163 def check_updates_iterator(self
, iterator
):
165 Apply a list of updates which must be within the valid range of updates
166 :param iterator: Iterable specifying integer update numbers to apply
167 :raise DomainUpdateException:
170 if op
< MIN_UPDATE
or op
> MAX_UPDATE
:
171 raise DomainUpdateException("Update number invalid.")
173 # No LDIF file exists for the change
174 getattr(self
, "operation_%d" % op
)(op
)
176 def check_updates_range(self
, start
=0, end
=0):
178 Apply a range of updates which must be within the valid range of updates
179 :param start: integer update to begin
180 :param end: integer update to end (inclusive)
181 :raise DomainUpdateException:
184 if start
< MIN_UPDATE
or start
> end
or end
> MAX_UPDATE
:
185 raise DomainUpdateException("Update number invalid.")
187 if op
not in missing_updates
:
188 # No LDIF file exists for the change
189 getattr(self
, "operation_%d" % op
)(op
)
193 def update_exists(self
, op
):
195 :param op: Integer update number
196 :return: True if update exists else False
198 update_dn
= "CN=%s,%s" % (update_map
[op
], self
.domainupdate_container
)
200 res
= self
.samdb
.search(base
=update_dn
,
201 scope
=ldb
.SCOPE_BASE
,
203 except ldb
.LdbError
as e
:
205 if num
!= ldb
.ERR_NO_SUCH_OBJECT
:
210 print("Skip Domain Update %u: %s" % (op
, update_map
[op
]))
213 def update_add(self
, op
):
215 Add the corresponding container object for the given update
216 :param op: Integer update
218 self
.samdb
.add_ldif("""dn: CN=%s,%s
219 objectClass: container
220 """ % (update_map
[op
], str(self
.domainupdate_container
)))
221 print("Applied Domain Update %u: %s" % (op
, update_map
[op
]))
223 def raise_if_not_fix(self
, op
):
225 Raises an exception if not set to fix.
226 :param op: Integer operation
227 :raise DomainUpdateException:
230 raise DomainUpdateException("Missing operation %d. Fix is currently set to False" % op
)
232 # Create a new object CN=TPM Devices in the Domain partition.
233 def operation_78(self
, op
):
234 if self
.update_exists(op
):
236 self
.raise_if_not_fix(op
)
238 self
.samdb
.add_ldif("""dn: CN=TPM Devices,%s
240 objectClass: msTPM-InformationObjectsContainer
241 """ % self
.domain_dn
,
242 controls
=["relax:0", "provision:0"])
244 if self
.add_update_container
:
247 # Created an access control entry for the TPM service.
248 def operation_79(self
, op
):
249 if self
.update_exists(op
):
251 self
.raise_if_not_fix(op
)
253 ace
= "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
255 self
.sd_utils
.update_aces_in_dacl(self
.domain_dn
, add_aces
=[ace
])
257 if self
.add_update_container
:
260 # Grant "Clone DC" extended right to Cloneable Domain Controllers group
261 def operation_80(self
, op
):
262 if self
.update_exists(op
):
264 self
.raise_if_not_fix(op
)
266 ace
= "(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;CN)"
268 self
.sd_utils
.update_aces_in_dacl(self
.domain_dn
, add_aces
=[ace
])
270 if self
.add_update_container
:
273 # Grant ms-DS-Allowed-To-Act-On-Behalf-Of-Other-Identity to Principal Self
275 def operation_81(self
, op
):
276 if self
.update_exists(op
):
278 self
.raise_if_not_fix(op
)
280 ace
= "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
282 self
.sd_utils
.update_aces_in_dacl(self
.domain_dn
, add_aces
=[ace
])
284 if self
.add_update_container
:
288 # THE FOLLOWING ARE MISSING UPDATES FROM 2008 R2
291 # Add Managed Service Accounts container
292 def operation_75(self
, op
):
293 if self
.update_exists(op
):
295 self
.raise_if_not_fix(op
)
297 descriptor
= get_managed_service_accounts_descriptor(self
.domain_sid
)
298 managedservice_descr
= b64encode(descriptor
).decode('utf8')
299 managed_service_dn
= "CN=Managed Service Accounts,%s" % \
302 self
.samdb
.modify_ldif("""dn: %s
304 objectClass: container
305 description: Default container for managed service accounts
306 showInAdvancedViewOnly: FALSE
307 nTSecurityDescriptor:: %s""" % (managed_service_dn
, managedservice_descr
),
308 controls
=["relax:0", "provision:0"])
310 if self
.add_update_container
:
313 # Add the otherWellKnownObjects reference to MSA
314 def operation_76(self
, op
):
315 if self
.update_exists(op
):
317 self
.raise_if_not_fix(op
)
319 managed_service_dn
= "CN=Managed Service Accounts,%s" % \
322 self
.samdb
.modify_ldif("""dn: %s
324 add: otherWellKnownObjects
325 otherWellKnownObjects: B:32:1EB93889E40C45DF9F0C64D23BBB6237:%s
326 """ % (str(self
.domain_dn
), managed_service_dn
), controls
=["relax:0",
329 if self
.add_update_container
:
332 # Add the PSPs object in the System container
333 def operation_77(self
, op
):
334 if self
.update_exists(op
):
336 self
.raise_if_not_fix(op
)
338 self
.samdb
.add_ldif("""dn: CN=PSPs,CN=System,%s
340 objectClass: msImaging-PSPs
341 """ % str(self
.domain_dn
), controls
=["relax:0", "provision:0"])
343 if self
.add_update_container
:
346 ## ## Windows Server 2016: Domain-wide updates
348 ## After the operations that are performed by domainprep in Windows
349 ## Server 2016 (operations 82-88) complete, the revision attribute for the
350 ## CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System,DC=ForestRootDomain
351 ## object is set to 15.
353 ## Operation 82: {83c53da7-427e-47a4-a07a-a324598b88f7}
355 ## Create CN=Keys container at root of domain
357 ## - objectClass: container
358 ## - description: Default container for key credential objects
359 ## - ShowInAdvancedViewOnly: TRUE
361 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EA)
362 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DA)
363 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;SY)
364 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DD)
365 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;ED)
367 def operation_82(self
, op
):
368 if self
.update_exists(op
):
370 self
.raise_if_not_fix(op
)
372 keys_dn
= "CN=Keys,%s" % str(self
.domain_dn
)
376 sddl
+= "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EA)"
377 sddl
+= "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DA)"
378 sddl
+= "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;SY)"
379 sddl
+= "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DD)"
380 sddl
+= "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;ED)"
384 objectClass: container
385 description: Default container for key credential objects
386 ShowInAdvancedViewOnly: TRUE
387 nTSecurityDescriptor: %s
388 """ % (keys_dn
, sddl
)
390 self
.samdb
.add_ldif(ldif
)
392 if self
.add_update_container
:
395 ## Operation 83: {c81fc9cc-0130-4fd1-b272-634d74818133}
397 ## Add Full Control allow aces to CN=Keys container for "domain\Key Admins"
398 ## and "rootdomain\Enterprise Key Admins".
400 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Key Admins)
401 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
403 def operation_83(self
, op
):
404 if self
.update_exists(op
):
406 self
.raise_if_not_fix(op
)
408 keys_dn
= "CN=Keys,%s" % str(self
.domain_dn
)
410 aces
= ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;KA)"]
411 aces
+= ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"]
413 self
.sd_utils
.update_aces_in_dacl(keys_dn
, add_aces
=aces
)
415 if self
.add_update_container
:
419 ## Operation 84: {e5f9e791-d96d-4fc9-93c9-d53e1dc439ba}
421 ## Modify otherWellKnownObjects attribute to point to the CN=Keys container.
423 ## - otherWellKnownObjects: B:32:683A24E2E8164BD3AF86AC3C2CF3F981:CN=Keys,%ws
424 def operation_84(self
, op
):
425 if self
.update_exists(op
):
427 self
.raise_if_not_fix(op
)
429 keys_dn
= "CN=Keys,%s" % str(self
.domain_dn
)
434 add: otherWellKnownObjects
435 otherWellKnownObjects: B:32:683A24E2E8164BD3AF86AC3C2CF3F981:%s
436 """ % (str(self
.domain_dn
), keys_dn
)
438 self
.samdb
.modify_ldif(ldif
)
440 if self
.add_update_container
:
444 ## Operation 85: {e6d5fd00-385d-4e65-b02d-9da3493ed850}
446 ## Modify the domain NC to permit "domain\Key Admins" and
447 ## "rootdomain\Enterprise Key Admins"
448 ## to modify the msds-KeyCredentialLink attribute.
450 ## (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Key Admins)
451 ## (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Enterprise Key Admins)
452 ## in root domain, but in non-root domains resulted in a bogus domain-relative
453 ## ACE with a non-resolvable -527 SID
455 def operation_85(self
, op
):
456 if self
.update_exists(op
):
458 self
.raise_if_not_fix(op
)
460 aces
= ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;KA)"]
461 # we use an explicit sid in order to replay the windows mistake
462 aces
+= ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;%s-527)" %
463 str(self
.domain_sid
)]
465 self
.sd_utils
.update_aces_in_dacl(self
.domain_dn
, add_aces
=aces
)
467 if self
.add_update_container
:
471 ## Operation 86: {3a6b3fbf-3168-4312-a10d-dd5b3393952d}
473 ## Grant the DS-Validated-Write-Computer CAR to creator owner and self
475 ## (OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;PS)
476 ## (OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;CO)
478 def operation_86(self
, op
):
479 if self
.update_exists(op
):
481 self
.raise_if_not_fix(op
)
483 aces
= ["(OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"]
484 aces
+= ["(OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;CO)"]
486 self
.sd_utils
.update_aces_in_dacl(self
.domain_dn
, add_aces
=aces
)
488 if self
.add_update_container
:
491 ## Operation 87: {7f950403-0ab3-47f9-9730-5d7b0269f9bd}
493 ## Delete the ACE granting Full Control to the incorrect
494 ## domain-relative Enterprise Key Admins group, and add
495 ## an ACE granting Full Control to Enterprise Key Admins group.
497 ## Delete (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
498 ## Add (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
500 def operation_87(self
, op
):
501 if self
.update_exists(op
):
503 self
.raise_if_not_fix(op
)
505 # we use an explicit sid in order to replay the windows mistake
506 # note this is also strange for a 2nd reason because it doesn't
507 # delete: ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;%s-527)"
508 # which was added in operation_85, so the del is basically a noop
509 # and the result is one additional ace
510 del_aces
= ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;%s-527)" %
511 str(self
.domain_sid
)]
512 add_aces
= ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"]
514 self
.sd_utils
.update_aces_in_dacl(self
.domain_dn
,
518 if self
.add_update_container
:
521 ## Operation 88: {434bb40d-dbc9-4fe7-81d4-d57229f7b080}
523 ## Add "msDS-ExpirePasswordsOnSmartCardOnlyAccounts" on the domain NC object
524 ## and set default value to FALSE
526 def operation_88(self
, op
):
527 if self
.update_exists(op
):
529 self
.raise_if_not_fix(op
)
534 add: msDS-ExpirePasswordsOnSmartCardOnlyAccounts
535 msDS-ExpirePasswordsOnSmartCardOnlyAccounts: FALSE
536 """ % str(self
.domain_dn
)
538 self
.samdb
.modify_ldif(ldif
)
540 if self
.add_update_container
:
543 ## Windows Server 2016 (operation 89) complete, the **revision** attribute for the
544 ## CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System,DC=ForestRootDomain object
548 ## Operation 89: {a0c238ba-9e30-4ee6-80a6-43f731e9a5cd}
550 ## Delete the ACE granting Full Control to Enterprise Key Admins and
551 ## add an ACE granting Enterprise Key Admins Full Control over just
552 ## the msdsKeyCredentialLink attribute.
554 ## Delete (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
555 ## Add (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Enterprise Key Admins)|
557 def operation_89(self
, op
):
558 if self
.update_exists(op
):
560 self
.raise_if_not_fix(op
)
562 # Note this only fixes the mistake from operation_87
563 # but leaves the mistake of operation_85 if we're
564 # not in the root domain...
565 del_aces
= ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"]
566 add_aces
= ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;EK)"]
568 self
.sd_utils
.update_aces_in_dacl(self
.domain_dn
,
572 if self
.add_update_container
: