--- /dev/null
+# Samba4 Domain update checker
+#
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import re
+import ldb
+import samba
+import time
+from base64 import b64encode
+from samba import dsdb
+from samba import common
+from samba import sd_utils
+from samba.dcerpc import misc
+from samba.dcerpc import drsuapi
+from samba.ndr import ndr_unpack, ndr_pack
+from samba.dcerpc import drsblobs
+from samba.common import dsdb_Dn
+from samba.dcerpc import security
+from samba.dcerpc.security import SECINFO_DACL
+from samba.descriptor import (
+ get_wellknown_sds,
+ get_diff_sds,
+ get_managed_service_accounts_descriptor,
+)
+from samba.auth import system_session, admin_session
+from samba.netcmd import CommandError
+from samba.netcmd.fsmo import get_fsmo_roleowner
+from samba.dsdb import (
+ DS_DOMAIN_FUNCTION_2008,
+ DS_DOMAIN_FUNCTION_2008_R2,
+ DS_DOMAIN_FUNCTION_2012,
+ DS_DOMAIN_FUNCTION_2012_R2,
+ DS_DOMAIN_FUNCTION_2016,
+)
+
+MIN_UPDATE = 75
+MAX_UPDATE = 81
+
+update_map = {
+ # Missing updates from 2008 R2 - version 5
+ 75: "5e1574f6-55df-493e-a671-aaeffca6a100",
+ 76: "d262aae8-41f7-48ed-9f35-56bbb677573d",
+ 77: "82112ba0-7e4c-4a44-89d9-d46c9612bf91",
+ # Windows Server 2012 - version 9
+ 78: "c3c927a6-cc1d-47c0-966b-be8f9b63d991",
+ 79: "54afcfb9-637a-4251-9f47-4d50e7021211",
+ 80: "f4728883-84dd-483c-9897-274f2ebcf11e",
+ 81: "ff4f9d27-7157-4cb0-80a9-5d6f2b14c8ff",
+ # Windows Server 2012 R2 - version 10
+ # No updates
+}
+
+functional_level_to_max_update = {
+ DS_DOMAIN_FUNCTION_2008: 74,
+ DS_DOMAIN_FUNCTION_2008_R2: 77,
+ DS_DOMAIN_FUNCTION_2012: 81,
+ DS_DOMAIN_FUNCTION_2012_R2: 81,
+ DS_DOMAIN_FUNCTION_2016: 88,
+}
+
+functional_level_to_version = {
+ DS_DOMAIN_FUNCTION_2008: 3,
+ DS_DOMAIN_FUNCTION_2008_R2: 5,
+ DS_DOMAIN_FUNCTION_2012: 9,
+ DS_DOMAIN_FUNCTION_2012_R2: 10,
+ DS_DOMAIN_FUNCTION_2016: 15,
+}
+
+# No update numbers have been skipped over
+missing_updates = []
+
+
+class DomainUpdateException(Exception):
+ pass
+
+
+class DomainUpdate(object):
+ """Check and update a SAM database for domain updates"""
+
+ def __init__(self, samdb, fix=False,
+ add_update_container=True):
+ """
+ :param samdb: LDB database
+ :param fix: Apply the update if the container is missing
+ :param add_update_container: Add the container at the end of the change
+ :raise DomainUpdateException:
+ """
+ self.samdb = samdb
+ self.fix = fix
+ self.add_update_container = add_update_container
+ # TODO: In future we should check for inconsistencies when it claims it has been done
+ self.check_update_applied = False
+
+ self.config_dn = self.samdb.get_config_basedn()
+ self.domain_dn = self.samdb.domain_dn()
+ self.schema_dn = self.samdb.get_schema_basedn()
+
+ self.sd_utils = sd_utils.SDUtils(samdb)
+ self.domain_sid = security.dom_sid(samdb.get_domain_sid())
+
+ self.domainupdate_container = self.samdb.get_root_basedn()
+ if not self.domainupdate_container.add_child("CN=Operations,CN=DomainUpdates,CN=System"):
+ raise DomainUpdateException("Failed to add domain update container child")
+
+ self.revision_object = self.samdb.get_root_basedn()
+ if not self.revision_object.add_child("CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System"):
+ raise DomainUpdateException("Failed to add revision object child")
+
+ def check_updates_functional_level(self, functional_level,
+ old_functional_level=None):
+ res = self.samdb.search(base=self.revision_object,
+ attrs=["revision"], scope=ldb.SCOPE_BASE)
+
+ expected_update = functional_level_to_max_update[functional_level]
+
+ if old_functional_level:
+ min_update = functional_level_to_max_update[old_functional_level]
+ min_update += 1
+ else:
+ min_update = MIN_UPDATE
+
+ self.check_updates_range(min_update, expected_update)
+
+ expected_version = functional_level_to_version[functional_level]
+ if int(res[0]['revision'][0]) < expected_version:
+ pass
+
+ def check_updates_iterator(self, iterator):
+ """
+ Apply a list of updates which must be within the valid range of updates
+ :param iterator: Iterable specifying integer update numbers to apply
+ :raise DomainUpdateException:
+ """
+ for op in iterator:
+ if op < MIN_UPDATE or op > MAX_UPDATE:
+ raise DomainUpdateException("Update number invalid.")
+
+ # No LDIF file exists for the change
+ getattr(self, "operation_%d" % op)(op)
+
+ def check_updates_range(self, start=0, end=0):
+ """
+ Apply a range of updates which must be within the valid range of updates
+ :param start: integer update to begin
+ :param end: integer update to end (inclusive)
+ :raise DomainUpdateException:
+ """
+ op = start
+ if start < MIN_UPDATE or start > end or end > MAX_UPDATE:
+ raise DomainUpdateException("Update number invalid.")
+ while op <= end:
+ if op not in missing_updates:
+ # No LDIF file exists for the change
+ getattr(self, "operation_%d" % op)(op)
+
+ op += 1
+
+ def update_exists(self, op):
+ """
+ :param op: Integer update number
+ :return: True if update exists else False
+ """
+ try:
+ res = self.samdb.search(base=self.domainupdate_container,
+ expression="(CN=%s)" % update_map[op])
+ except ldb.LdbError:
+ return False
+
+ return len(res) == 1
+
+ def update_add(self, op):
+ """
+ Add the corresponding container object for the given update
+ :param op: Integer update
+ """
+ self.samdb.add_ldif("""dn: CN=%s,%s
+objectClass: container
+""" % (update_map[op], str(self.domainupdate_container)))
+
+ def insert_ace_into_dacl(self, dn, existing_sddl, ace):
+ """
+ Add an ACE to a DACL, checking if it already exists with a simple string search.
+
+ :param dn: DN to modify
+ :param existing_sddl: existing sddl as string
+ :param ace: string ace to insert
+ :return: True if modified else False
+ """
+ index = existing_sddl.rfind("S:")
+ if index != -1:
+ new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
+ else:
+ # Insert it at the end if no S: section
+ new_sddl = existing_sddl + ace
+
+ if ace in existing_sddl:
+ return False
+
+ self.sd_utils.modify_sd_on_dn(dn, new_sddl,
+ controls=["sd_flags:1:%d" % SECINFO_DACL])
+
+ return True
+
+ def insert_ace_into_string(self, dn, ace, attr):
+ """
+ Insert an ACE into a string attribute like defaultSecurityDescriptor.
+ This also checks if it already exists using a simple string search.
+
+ :param dn: DN to modify
+ :param ace: string ace to insert
+ :param attr: attribute to modify
+ :return: True if modified else False
+ """
+ msg = self.samdb.search(base=dn,
+ attrs=[attr],
+ controls=["search_options:1:2"])
+
+ assert len(msg) == 1
+ existing_sddl = msg[0][attr][0]
+ index = existing_sddl.rfind("S:")
+ if index != -1:
+ new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
+ else:
+ # Insert it at the end if no S: section
+ new_sddl = existing_sddl + ace
+
+ if ace in existing_sddl:
+ return False
+
+ m = ldb.Message()
+ m.dn = dn
+ m[attr] = ldb.MessageElement(new_sddl, ldb.FLAG_MOD_REPLACE,
+ attr)
+
+ self.samdb.modify(m, controls=["relax:0"])
+
+ return True
+
+ # Create a new object CN=TPM Devices in the Domain partition.
+ def operation_78(self, op):
+ if self.update_exists(op):
+ return
+
+ self.samdb.add_ldif("""dn: CN=TPM Devices,%s
+objectClass: top
+objectClass: msTPM-InformationObjectsContainer
+""" % self.domain_dn,
+ controls=["relax:0", "provision:0"])
+
+ if self.add_update_container:
+ self.update_add(op)
+
+ # Created an access control entry for the TPM service.
+ def operation_79(self, op):
+ if self.update_exists(op):
+ return
+
+ ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
+
+ res = self.samdb.search(expression="(objectClass=samDomain)",
+ attrs=["nTSecurityDescriptor"],
+ controls=["search_options:1:2"])
+ for msg in res:
+ existing_sd = ndr_unpack(security.descriptor,
+ msg["nTSecurityDescriptor"][0])
+ existing_sddl = existing_sd.as_sddl(self.domain_sid)
+
+ self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
+
+ res = self.samdb.search(expression="(objectClass=domainDNS)",
+ attrs=["nTSecurityDescriptor"],
+ controls=["search_options:1:2"])
+ for msg in res:
+ existing_sd = ndr_unpack(security.descriptor,
+ msg["nTSecurityDescriptor"][0])
+ existing_sddl = existing_sd.as_sddl(self.domain_sid)
+
+ self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
+
+ if self.add_update_container:
+ self.update_add(op)
+
+ # Grant "Clone DC" extended right to Cloneable Domain Controllers group
+ def operation_80(self, op):
+ if self.update_exists(op):
+ return
+
+ ace = "(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;%s-522)" % str(self.domain_sid)
+
+ res = self.samdb.search(base=self.domain_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=["nTSecurityDescriptor"],
+ controls=["search_options:1:2",
+ "sd_flags:1:%d" % SECINFO_DACL])
+ msg = res[0]
+
+ existing_sd = ndr_unpack(security.descriptor,
+ msg["nTSecurityDescriptor"][0])
+ existing_sddl = existing_sd.as_sddl(self.domain_sid)
+
+ self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
+
+ if self.add_update_container:
+ self.update_add(op)
+
+ # Grant ms-DS-Allowed-To-Act-On-Behalf-Of-Other-Identity to Principal Self
+ # on all objects
+ def operation_81(self, op):
+ if self.update_exists(op):
+ return
+
+ ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
+
+ res = self.samdb.search(expression="(objectClass=samDomain)",
+ attrs=["nTSecurityDescriptor"],
+ controls=["search_options:1:2"])
+ for msg in res:
+ existing_sd = ndr_unpack(security.descriptor,
+ msg["nTSecurityDescriptor"][0])
+ existing_sddl = existing_sd.as_sddl(self.domain_sid)
+
+ self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
+
+ res = self.samdb.search(expression="(objectClass=domainDNS)",
+ attrs=["nTSecurityDescriptor"],
+ controls=["search_options:1:2"])
+
+ for msg in res:
+ existing_sd = ndr_unpack(security.descriptor,
+ msg["nTSecurityDescriptor"][0])
+ existing_sddl = existing_sd.as_sddl(self.domain_sid)
+
+ self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
+
+ if self.add_update_container:
+ self.update_add(op)
+
+ #
+ # THE FOLLOWING ARE MISSING UPDATES FROM 2008 R2
+ #
+
+ # Add Managed Service Accounts container
+ def operation_75(self, op):
+ if self.update_exists(op):
+ return
+
+ descriptor = get_managed_service_accounts_descriptor(self.domain_sid)
+ managedservice_descr = b64encode(descriptor)
+ managed_service_dn = "CN=Managed Service Accounts,%s" % \
+ str(self.domain_dn)
+
+ self.samdb.modify_ldif("""dn: %s
+changetype: add
+objectClass: container
+description: Default container for managed service accounts
+showInAdvancedViewOnly: FALSE
+nTSecurityDescriptor:: %s""" % (managed_service_dn, managedservice_descr),
+ controls=["relax:0", "provision:0"])
+
+ if self.add_update_container:
+ self.update_add(op)
+
+ # Add the otherWellKnownObjects reference to MSA
+ def operation_76(self, op):
+ if self.update_exists(op):
+ return
+
+ managed_service_dn = "CN=Managed Service Accounts,%s" % \
+ str(self.domain_dn)
+
+ self.samdb.modify_ldif("""dn: %s
+changetype: modify
+add: otherWellKnownObjects
+otherWellKnownObjects: B:32:1EB93889E40C45DF9F0C64D23BBB6237:%s
+""" % (str(self.domain_dn), managed_service_dn), controls=["relax:0",
+ "provision:0"])
+
+ if self.add_update_container:
+ self.update_add(op)
+
+ # Add the PSPs object in the System container
+ def operation_77(self, op):
+ if self.update_exists(op):
+ return
+
+ self.samdb.add_ldif("""dn: CN=PSPs,CN=System,%s
+objectClass: top
+objectClass: msImaging-PSPs
+""" % str(self.domain_dn), controls=["relax:0", "provision:0"])
+
+ if self.add_update_container:
+ self.update_add(op)