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/>.
21 from base64
import b64encode
22 from samba
import sd_utils
23 from samba
.ndr
import ndr_unpack
, ndr_pack
24 from samba
.dcerpc
import security
25 from samba
.dcerpc
.security
import SECINFO_DACL
26 from samba
.descriptor
import (
27 get_managed_service_accounts_descriptor
,
29 from samba
.dsdb
import (
30 DS_DOMAIN_FUNCTION_2008
,
31 DS_DOMAIN_FUNCTION_2008_R2
,
32 DS_DOMAIN_FUNCTION_2012
,
33 DS_DOMAIN_FUNCTION_2012_R2
,
34 DS_DOMAIN_FUNCTION_2016
,
41 # Missing updates from 2008 R2 - version 5
42 75: "5e1574f6-55df-493e-a671-aaeffca6a100",
43 76: "d262aae8-41f7-48ed-9f35-56bbb677573d",
44 77: "82112ba0-7e4c-4a44-89d9-d46c9612bf91",
45 # Windows Server 2012 - version 9
46 78: "c3c927a6-cc1d-47c0-966b-be8f9b63d991",
47 79: "54afcfb9-637a-4251-9f47-4d50e7021211",
48 80: "f4728883-84dd-483c-9897-274f2ebcf11e",
49 81: "ff4f9d27-7157-4cb0-80a9-5d6f2b14c8ff",
50 # Windows Server 2012 R2 - version 10
54 functional_level_to_max_update
= {
55 DS_DOMAIN_FUNCTION_2008
: 74,
56 DS_DOMAIN_FUNCTION_2008_R2
: 77,
57 DS_DOMAIN_FUNCTION_2012
: 81,
58 DS_DOMAIN_FUNCTION_2012_R2
: 81,
59 DS_DOMAIN_FUNCTION_2016
: 88,
62 functional_level_to_version
= {
63 DS_DOMAIN_FUNCTION_2008
: 3,
64 DS_DOMAIN_FUNCTION_2008_R2
: 5,
65 DS_DOMAIN_FUNCTION_2012
: 9,
66 DS_DOMAIN_FUNCTION_2012_R2
: 10,
67 DS_DOMAIN_FUNCTION_2016
: 15,
70 # No update numbers have been skipped over
74 class DomainUpdateException(Exception):
78 class DomainUpdate(object):
79 """Check and update a SAM database for domain updates"""
81 def __init__(self
, samdb
, fix
=False,
82 add_update_container
=True):
84 :param samdb: LDB database
85 :param fix: Apply the update if the container is missing
86 :param add_update_container: Add the container at the end of the change
87 :raise DomainUpdateException:
91 self
.add_update_container
= add_update_container
92 # TODO: In future we should check for inconsistencies when it claims it has been done
93 self
.check_update_applied
= False
95 self
.config_dn
= self
.samdb
.get_config_basedn()
96 self
.domain_dn
= self
.samdb
.domain_dn()
97 self
.schema_dn
= self
.samdb
.get_schema_basedn()
99 self
.sd_utils
= sd_utils
.SDUtils(samdb
)
100 self
.domain_sid
= security
.dom_sid(samdb
.get_domain_sid())
102 self
.domainupdate_container
= self
.samdb
.get_root_basedn()
103 if not self
.domainupdate_container
.add_child("CN=Operations,CN=DomainUpdates,CN=System"):
104 raise DomainUpdateException("Failed to add domain update container child")
106 self
.revision_object
= self
.samdb
.get_root_basedn()
107 if not self
.revision_object
.add_child("CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System"):
108 raise DomainUpdateException("Failed to add revision object child")
110 def check_updates_functional_level(self
, functional_level
,
111 old_functional_level
=None,
112 update_revision
=False):
114 Apply all updates for a given old and new functional level
115 :param functional_level: constant
116 :param old_functional_level: constant
117 :param update_revision: modify the stored version
118 :raise DomainUpdateException:
120 res
= self
.samdb
.search(base
=self
.revision_object
,
121 attrs
=["revision"], scope
=ldb
.SCOPE_BASE
)
123 expected_update
= functional_level_to_max_update
[functional_level
]
125 if old_functional_level
:
126 min_update
= functional_level_to_max_update
[old_functional_level
]
129 min_update
= MIN_UPDATE
131 self
.check_updates_range(min_update
, expected_update
)
133 expected_version
= functional_level_to_version
[functional_level
]
134 found_version
= int(res
[0]['revision'][0])
135 if update_revision
and found_version
< expected_version
:
137 raise DomainUpdateException("Revision is not high enough. Fix is set to False."
138 "\nExpected: %dGot: %d" % (expected_version
,
140 self
.samdb
.modify_ldif("""dn: %s
144 """ % (str(self
.revision_object
), expected_version
))
146 def check_updates_iterator(self
, iterator
):
148 Apply a list of updates which must be within the valid range of updates
149 :param iterator: Iterable specifying integer update numbers to apply
150 :raise DomainUpdateException:
153 if op
< MIN_UPDATE
or op
> MAX_UPDATE
:
154 raise DomainUpdateException("Update number invalid.")
156 # No LDIF file exists for the change
157 getattr(self
, "operation_%d" % op
)(op
)
159 def check_updates_range(self
, start
=0, end
=0):
161 Apply a range of updates which must be within the valid range of updates
162 :param start: integer update to begin
163 :param end: integer update to end (inclusive)
164 :raise DomainUpdateException:
167 if start
< MIN_UPDATE
or start
> end
or end
> MAX_UPDATE
:
168 raise DomainUpdateException("Update number invalid.")
170 if op
not in missing_updates
:
171 # No LDIF file exists for the change
172 getattr(self
, "operation_%d" % op
)(op
)
176 def update_exists(self
, op
):
178 :param op: Integer update number
179 :return: True if update exists else False
182 res
= self
.samdb
.search(base
=self
.domainupdate_container
,
183 expression
="(CN=%s)" % update_map
[op
])
189 def update_add(self
, op
):
191 Add the corresponding container object for the given update
192 :param op: Integer update
194 self
.samdb
.add_ldif("""dn: CN=%s,%s
195 objectClass: container
196 """ % (update_map
[op
], str(self
.domainupdate_container
)))
198 def insert_ace_into_dacl(self
, dn
, existing_sddl
, ace
):
200 Add an ACE to a DACL, checking if it already exists with a simple string search.
202 :param dn: DN to modify
203 :param existing_sddl: existing sddl as string
204 :param ace: string ace to insert
205 :return: True if modified else False
207 index
= existing_sddl
.rfind("S:")
209 new_sddl
= existing_sddl
[:index
] + ace
+ existing_sddl
[index
:]
211 # Insert it at the end if no S: section
212 new_sddl
= existing_sddl
+ ace
214 if ace
in existing_sddl
:
217 self
.sd_utils
.modify_sd_on_dn(dn
, new_sddl
,
218 controls
=["sd_flags:1:%d" % SECINFO_DACL
])
222 def insert_ace_into_string(self
, dn
, ace
, attr
):
224 Insert an ACE into a string attribute like defaultSecurityDescriptor.
225 This also checks if it already exists using a simple string search.
227 :param dn: DN to modify
228 :param ace: string ace to insert
229 :param attr: attribute to modify
230 :return: True if modified else False
232 msg
= self
.samdb
.search(base
=dn
,
234 controls
=["search_options:1:2"])
237 existing_sddl
= msg
[0][attr
][0]
238 index
= existing_sddl
.rfind("S:")
240 new_sddl
= existing_sddl
[:index
] + ace
+ existing_sddl
[index
:]
242 # Insert it at the end if no S: section
243 new_sddl
= existing_sddl
+ ace
245 if ace
in existing_sddl
:
250 m
[attr
] = ldb
.MessageElement(new_sddl
, ldb
.FLAG_MOD_REPLACE
,
253 self
.samdb
.modify(m
, controls
=["relax:0"])
257 def raise_if_not_fix(self
, op
):
259 Raises an exception if not set to fix.
260 :param op: Integer operation
261 :raise DomainUpdateException:
264 raise DomainUpdateException("Missing operation %d. Fix is currently set to False" % op
)
266 # Create a new object CN=TPM Devices in the Domain partition.
267 def operation_78(self
, op
):
268 if self
.update_exists(op
):
270 self
.raise_if_not_fix(op
)
272 self
.samdb
.add_ldif("""dn: CN=TPM Devices,%s
274 objectClass: msTPM-InformationObjectsContainer
275 """ % self
.domain_dn
,
276 controls
=["relax:0", "provision:0"])
278 if self
.add_update_container
:
281 # Created an access control entry for the TPM service.
282 def operation_79(self
, op
):
283 if self
.update_exists(op
):
285 self
.raise_if_not_fix(op
)
287 ace
= "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
289 res
= self
.samdb
.search(expression
="(objectClass=samDomain)",
290 attrs
=["nTSecurityDescriptor"],
291 controls
=["search_options:1:2"])
293 existing_sd
= ndr_unpack(security
.descriptor
,
294 msg
["nTSecurityDescriptor"][0])
295 existing_sddl
= existing_sd
.as_sddl(self
.domain_sid
)
297 self
.insert_ace_into_dacl(msg
.dn
, existing_sddl
, ace
)
299 res
= self
.samdb
.search(expression
="(objectClass=domainDNS)",
300 attrs
=["nTSecurityDescriptor"],
301 controls
=["search_options:1:2"])
303 existing_sd
= ndr_unpack(security
.descriptor
,
304 msg
["nTSecurityDescriptor"][0])
305 existing_sddl
= existing_sd
.as_sddl(self
.domain_sid
)
307 self
.insert_ace_into_dacl(msg
.dn
, existing_sddl
, ace
)
309 if self
.add_update_container
:
312 # Grant "Clone DC" extended right to Cloneable Domain Controllers group
313 def operation_80(self
, op
):
314 if self
.update_exists(op
):
316 self
.raise_if_not_fix(op
)
318 ace
= "(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;%s-522)" % str(self
.domain_sid
)
320 res
= self
.samdb
.search(base
=self
.domain_dn
,
321 scope
=ldb
.SCOPE_BASE
,
322 attrs
=["nTSecurityDescriptor"],
323 controls
=["search_options:1:2",
324 "sd_flags:1:%d" % SECINFO_DACL
])
327 existing_sd
= ndr_unpack(security
.descriptor
,
328 msg
["nTSecurityDescriptor"][0])
329 existing_sddl
= existing_sd
.as_sddl(self
.domain_sid
)
331 self
.insert_ace_into_dacl(msg
.dn
, existing_sddl
, ace
)
333 if self
.add_update_container
:
336 # Grant ms-DS-Allowed-To-Act-On-Behalf-Of-Other-Identity to Principal Self
338 def operation_81(self
, op
):
339 if self
.update_exists(op
):
341 self
.raise_if_not_fix(op
)
343 ace
= "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
345 res
= self
.samdb
.search(expression
="(objectClass=samDomain)",
346 attrs
=["nTSecurityDescriptor"],
347 controls
=["search_options:1:2"])
349 existing_sd
= ndr_unpack(security
.descriptor
,
350 msg
["nTSecurityDescriptor"][0])
351 existing_sddl
= existing_sd
.as_sddl(self
.domain_sid
)
353 self
.insert_ace_into_dacl(msg
.dn
, existing_sddl
, ace
)
355 res
= self
.samdb
.search(expression
="(objectClass=domainDNS)",
356 attrs
=["nTSecurityDescriptor"],
357 controls
=["search_options:1:2"])
360 existing_sd
= ndr_unpack(security
.descriptor
,
361 msg
["nTSecurityDescriptor"][0])
362 existing_sddl
= existing_sd
.as_sddl(self
.domain_sid
)
364 self
.insert_ace_into_dacl(msg
.dn
, existing_sddl
, ace
)
366 if self
.add_update_container
:
370 # THE FOLLOWING ARE MISSING UPDATES FROM 2008 R2
373 # Add Managed Service Accounts container
374 def operation_75(self
, op
):
375 if self
.update_exists(op
):
377 self
.raise_if_not_fix(op
)
379 descriptor
= get_managed_service_accounts_descriptor(self
.domain_sid
)
380 managedservice_descr
= b64encode(descriptor
).decode('utf8')
381 managed_service_dn
= "CN=Managed Service Accounts,%s" % \
384 self
.samdb
.modify_ldif("""dn: %s
386 objectClass: container
387 description: Default container for managed service accounts
388 showInAdvancedViewOnly: FALSE
389 nTSecurityDescriptor:: %s""" % (managed_service_dn
, managedservice_descr
),
390 controls
=["relax:0", "provision:0"])
392 if self
.add_update_container
:
395 # Add the otherWellKnownObjects reference to MSA
396 def operation_76(self
, op
):
397 if self
.update_exists(op
):
399 self
.raise_if_not_fix(op
)
401 managed_service_dn
= "CN=Managed Service Accounts,%s" % \
404 self
.samdb
.modify_ldif("""dn: %s
406 add: otherWellKnownObjects
407 otherWellKnownObjects: B:32:1EB93889E40C45DF9F0C64D23BBB6237:%s
408 """ % (str(self
.domain_dn
), managed_service_dn
), controls
=["relax:0",
411 if self
.add_update_container
:
414 # Add the PSPs object in the System container
415 def operation_77(self
, op
):
416 if self
.update_exists(op
):
418 self
.raise_if_not_fix(op
)
420 self
.samdb
.add_ldif("""dn: CN=PSPs,CN=System,%s
422 objectClass: msImaging-PSPs
423 """ % str(self
.domain_dn
), controls
=["relax:0", "provision:0"])
425 if self
.add_update_container
: