diff options
Diffstat (limited to 'python/samba/domain_update.py')
-rw-r--r-- | python/samba/domain_update.py | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/python/samba/domain_update.py b/python/samba/domain_update.py new file mode 100644 index 0000000..e91bdf4 --- /dev/null +++ b/python/samba/domain_update.py @@ -0,0 +1,573 @@ +# 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 ldb +from base64 import b64encode +from samba import sd_utils +from samba.dcerpc import security +from samba.descriptor import ( + get_managed_service_accounts_descriptor, +) +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 = 89 + +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 + # Windows Server 2016 - version 15 + 82: "83c53da7-427e-47a4-a07a-a324598b88f7", + # from the documentation and a fresh installation + # 83 is this: + # c81fc9cc-0130-4fd1-b272-634d74818133 + # adprep will use this on the wire: + # c81fc9cc-0130-f4d1-b272-634d74818133 + 83: "c81fc9cc-0130-4fd1-b272-634d74818133", + 84: "e5f9e791-d96d-4fc9-93c9-d53e1dc439ba", + 85: "e6d5fd00-385d-4e65-b02d-9da3493ed850", + 86: "3a6b3fbf-3168-4312-a10d-dd5b3393952d", + 87: "7f950403-0ab3-47f9-9730-5d7b0269f9bd", + 88: "434bb40d-dbc9-4fe7-81d4-d57229f7b080", + # Windows Server 2016 - version 16 + 89: "a0c238ba-9e30-4ee6-80a6-43f731e9a5cd", +} + + +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: 89, +} + +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: 16, +} + +# 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() + try: + self.domainupdate_container.add_child("CN=Operations,CN=DomainUpdates,CN=System") + except ldb.LdbError: + raise DomainUpdateException("Failed to add domain update container child") + + self.revision_object = self.samdb.get_root_basedn() + try: + self.revision_object.add_child("CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System") + except ldb.LdbError: + raise DomainUpdateException("Failed to add revision object child") + + def check_updates_functional_level(self, functional_level, + old_functional_level=None, + update_revision=False): + """ + Apply all updates for a given old and new functional level + :param functional_level: constant + :param old_functional_level: constant + :param update_revision: modify the stored version + :raise DomainUpdateException: + """ + 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] + found_version = int(res[0]['revision'][0]) + if update_revision and found_version < expected_version: + if not self.fix: + raise DomainUpdateException("Revision is not high enough. Fix is set to False." + "\nExpected: %dGot: %d" % (expected_version, + found_version)) + self.samdb.modify_ldif("""dn: %s +changetype: modify +replace: revision +revision: %d +""" % (str(self.revision_object), expected_version)) + + 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 + """ + update_dn = "CN=%s,%s" % (update_map[op], self.domainupdate_container) + try: + res = self.samdb.search(base=update_dn, + scope=ldb.SCOPE_BASE, + attrs=[]) + except ldb.LdbError as e: + (num, msg) = e.args + if num != ldb.ERR_NO_SUCH_OBJECT: + raise + return False + + assert len(res) == 1 + print("Skip Domain Update %u: %s" % (op, update_map[op])) + return True + + 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))) + print("Applied Domain Update %u: %s" % (op, update_map[op])) + + def raise_if_not_fix(self, op): + """ + Raises an exception if not set to fix. + :param op: Integer operation + :raise DomainUpdateException: + """ + if not self.fix: + raise DomainUpdateException("Missing operation %d. Fix is currently set to False" % op) + + # Create a new object CN=TPM Devices in the Domain partition. + def operation_78(self, op): + if self.update_exists(op): + return + self.raise_if_not_fix(op) + + 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 + self.raise_if_not_fix(op) + + ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)" + + self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=[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 + self.raise_if_not_fix(op) + + ace = "(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;CN)" + + self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=[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 + self.raise_if_not_fix(op) + + ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)" + + self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=[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 + self.raise_if_not_fix(op) + + descriptor = get_managed_service_accounts_descriptor(self.domain_sid) + managedservice_descr = b64encode(descriptor).decode('utf8') + 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 + self.raise_if_not_fix(op) + + 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.raise_if_not_fix(op) + + 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) + + ## ## Windows Server 2016: Domain-wide updates + ## + ## After the operations that are performed by domainprep in Windows + ## Server 2016 (operations 82-88) complete, the revision attribute for the + ## CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System,DC=ForestRootDomain + ## object is set to 15. + + ## Operation 82: {83c53da7-427e-47a4-a07a-a324598b88f7} + ## + ## Create CN=Keys container at root of domain + ## + ## - objectClass: container + ## - description: Default container for key credential objects + ## - ShowInAdvancedViewOnly: TRUE + ## + ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EA) + ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DA) + ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;SY) + ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DD) + ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;ED) + ## + def operation_82(self, op): + if self.update_exists(op): + return + self.raise_if_not_fix(op) + + keys_dn = "CN=Keys,%s" % str(self.domain_dn) + + sddl = "O:DA" + sddl += "D:" + sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EA)" + sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DA)" + sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;SY)" + sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DD)" + sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;ED)" + + ldif = """ +dn: %s +objectClass: container +description: Default container for key credential objects +ShowInAdvancedViewOnly: TRUE +nTSecurityDescriptor: %s +""" % (keys_dn, sddl) + + self.samdb.add_ldif(ldif) + + if self.add_update_container: + self.update_add(op) + + ## Operation 83: {c81fc9cc-0130-4fd1-b272-634d74818133} + ## + ## Add Full Control allow aces to CN=Keys container for "domain\Key Admins" + ## and "rootdomain\Enterprise Key Admins". + ## + ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Key Admins) + ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins) + ## + def operation_83(self, op): + if self.update_exists(op): + return + self.raise_if_not_fix(op) + + keys_dn = "CN=Keys,%s" % str(self.domain_dn) + + aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;KA)"] + aces += ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"] + + self.sd_utils.update_aces_in_dacl(keys_dn, add_aces=aces) + + if self.add_update_container: + self.update_add(op) + + + ## Operation 84: {e5f9e791-d96d-4fc9-93c9-d53e1dc439ba} + ## + ## Modify otherWellKnownObjects attribute to point to the CN=Keys container. + ## + ## - otherWellKnownObjects: B:32:683A24E2E8164BD3AF86AC3C2CF3F981:CN=Keys,%ws + def operation_84(self, op): + if self.update_exists(op): + return + self.raise_if_not_fix(op) + + keys_dn = "CN=Keys,%s" % str(self.domain_dn) + + ldif = """ +dn: %s +changetype: modify +add: otherWellKnownObjects +otherWellKnownObjects: B:32:683A24E2E8164BD3AF86AC3C2CF3F981:%s +""" % (str(self.domain_dn), keys_dn) + + self.samdb.modify_ldif(ldif) + + if self.add_update_container: + self.update_add(op) + + + ## Operation 85: {e6d5fd00-385d-4e65-b02d-9da3493ed850} + ## + ## Modify the domain NC to permit "domain\Key Admins" and + ## "rootdomain\Enterprise Key Admins" + ## to modify the msds-KeyCredentialLink attribute. + ## + ## (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Key Admins) + ## (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Enterprise Key Admins) + ## in root domain, but in non-root domains resulted in a bogus domain-relative + ## ACE with a non-resolvable -527 SID + ## + def operation_85(self, op): + if self.update_exists(op): + return + self.raise_if_not_fix(op) + + aces = ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;KA)"] + # we use an explicit sid in order to replay the windows mistake + aces += ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;%s-527)" % + str(self.domain_sid)] + + self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=aces) + + if self.add_update_container: + self.update_add(op) + + + ## Operation 86: {3a6b3fbf-3168-4312-a10d-dd5b3393952d} + ## + ## Grant the DS-Validated-Write-Computer CAR to creator owner and self + ## + ## (OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;PS) + ## (OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;CO) + ## + def operation_86(self, op): + if self.update_exists(op): + return + self.raise_if_not_fix(op) + + aces = ["(OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"] + aces += ["(OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;CO)"] + + self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=aces) + + if self.add_update_container: + self.update_add(op) + + ## Operation 87: {7f950403-0ab3-47f9-9730-5d7b0269f9bd} + ## + ## Delete the ACE granting Full Control to the incorrect + ## domain-relative Enterprise Key Admins group, and add + ## an ACE granting Full Control to Enterprise Key Admins group. + ## + ## Delete (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins) + ## Add (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins) + ## + def operation_87(self, op): + if self.update_exists(op): + return + self.raise_if_not_fix(op) + + # we use an explicit sid in order to replay the windows mistake + # note this is also strange for a 2nd reason because it doesn't + # delete: ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;%s-527)" + # which was added in operation_85, so the del is basically a noop + # and the result is one additional ace + del_aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;%s-527)" % + str(self.domain_sid)] + add_aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"] + + self.sd_utils.update_aces_in_dacl(self.domain_dn, + del_aces=del_aces, + add_aces=add_aces) + + if self.add_update_container: + self.update_add(op) + + ## Operation 88: {434bb40d-dbc9-4fe7-81d4-d57229f7b080} + ## + ## Add "msDS-ExpirePasswordsOnSmartCardOnlyAccounts" on the domain NC object + ## and set default value to FALSE + ## + def operation_88(self, op): + if self.update_exists(op): + return + self.raise_if_not_fix(op) + + ldif = """ +dn: %s +changetype: modify +add: msDS-ExpirePasswordsOnSmartCardOnlyAccounts +msDS-ExpirePasswordsOnSmartCardOnlyAccounts: FALSE +""" % str(self.domain_dn) + + self.samdb.modify_ldif(ldif) + + if self.add_update_container: + self.update_add(op) + + ## Windows Server 2016 (operation 89) complete, the **revision** attribute for the + ## CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System,DC=ForestRootDomain object + ## is set to **16**. + ## + + ## Operation 89: {a0c238ba-9e30-4ee6-80a6-43f731e9a5cd} + ## + ## Delete the ACE granting Full Control to Enterprise Key Admins and + ## add an ACE granting Enterprise Key Admins Full Control over just + ## the msdsKeyCredentialLink attribute. + ## + ## Delete (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins) + ## Add (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Enterprise Key Admins)| + ## + def operation_89(self, op): + if self.update_exists(op): + return + self.raise_if_not_fix(op) + + # Note this only fixes the mistake from operation_87 + # but leaves the mistake of operation_85 if we're + # not in the root domain... + del_aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"] + add_aces = ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;EK)"] + + self.sd_utils.update_aces_in_dacl(self.domain_dn, + del_aces=del_aces, + add_aces=add_aces) + + if self.add_update_container: + self.update_add(op) |