#!/usr/bin/env python import optparse import sys # Find right directory when running from source tree sys.path.insert(0, "bin/python") import samba import ldb import urllib import os from samba import getopt as options from samba import sd_utils from samba.samdb import SamDB from samba.dcerpc import security, misc from samba.ndr import ndr_pack, ndr_unpack from samba.credentials import Credentials from samba.auth import system_session from samba.dcerpc.security import (SEC_ACE_TYPE_ACCESS_DENIED_OBJECT, SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT, SEC_ACE_OBJECT_TYPE_PRESENT) parser = optparse.OptionParser("samba_CVE-2018-1057_helper") sambaopts = options.SambaOptions(parser) parser.add_option_group(options.VersionOptions(parser)) credopts = options.CredentialsOptions(parser) parser.add_option_group(credopts) parser.add_option("-H", "--URL", help="LDB URL for database", type=str, metavar="URL", dest="url") parser.add_option("--lock-pwchange", help="Lock this database against CVE-2018-1057 password changes", action="store_true") parser.add_option("--unlock-pwchange", help="UnLock this database against CVE-2018-1057 password changes", action="store_true") parser.add_option("--base", dest="base", default="", help="Pass search base that will build DN list for the first DC.") parser.add_option("--scope", dest="scope", default="SUB", help="Pass search scope that builds DN list. Options: SUB, ONE, BASE") parser.add_option("--filter", dest="filter", default="(objectClass=user)", help="LDAP filter of objects to lock against password changes") parser.add_option("--no-schema", dest="no_schema", action="store_true", help="Also apply this change to default ACL in schema") parser.add_option("--dry-run", dest="dry_run", action="store_true", help="Do not modify the database") opts, args = parser.parse_args() if len(args) != 0: parser.print_usage() sys.exit(1) if opts.scope.upper() == "SUB": search_scope = ldb.SCOPE_SUBTREE elif opts.scope.upper() == "BASE": self.search_scope = ldb.SCOPE_BASE elif self.search_scope() == "ONE": self.search_scope = ldb.SCOPE_ONELEVEL else: raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE") if not opts.lock_pwchange and not opts.unlock_pwchange: raise StandardError("Neither --lock-pwchange nor --unlock-pwchange specified") lp_ctx = sambaopts.get_loadparm() if not opts.no_schema and \ lp_ctx.get("dsdb:schema update allowed") is None: lp_ctx.set("dsdb:schema update allowed", "yes") print("Temporarily overriding 'dsdb:schema update allowed' setting") creds = credopts.get_credentials(lp_ctx) sam_ldb = SamDB(opts.url, session_info=system_session(), credentials=creds, lp=lp_ctx) sd_helper = samba.sd_utils.SDUtils(sam_ldb) sam_ldb.transaction_start() if opts.base is "": base_dn = None else: base_dn = opts.base res = sam_ldb.search(base=base_dn, expression=opts.filter, scope=search_scope, attrs=["ntSecurityDescriptor"]) # This is the right to change the password (unlocked) pwchange_guid = misc.GUID("ab721a53-1e2f-11d0-9819-00aa0040529b"); # A different samba-only GUID to deny such changes (locked) pwchange_lock_guid = misc.GUID("ffffffff-CECE-2018-1057-000000b13272"); # We are only worried about when 'world' has this right sid_world = security.dom_sid(security.SID_WORLD) def change_desc(desc): changed = False for ace in desc.dacl.aces: if (ace.type == SEC_ACE_TYPE_ACCESS_DENIED_OBJECT or \ ace.type == SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT) \ and ace.object.flags & SEC_ACE_OBJECT_TYPE_PRESENT: if ace.object.type == pwchange_guid and ace.trustee == sid_world: # Cope with a previous verison of this script if ace.type == SEC_ACE_TYPE_ACCESS_DENIED_OBJECT and \ opts.unlock_pwchange: ace.type = SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT changed = True # Lock the object by changing the GUID to a different one if ace.type == SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT and \ opts.lock_pwchange: ace.object.type = pwchange_lock_guid changed = True elif ace.object.type == pwchange_lock_guid and \ ace.trustee == sid_world and \ ace.type == SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT and \ opts.unlock_pwchange: # We unlock by changing the guid back the the proper one ace.object.type = pwchange_guid changed = True return (changed, desc) for msg in res: desc = ndr_unpack(security.descriptor, msg["ntSecurityDescriptor"][0]) (changed, new_desc) = change_desc(desc) if not changed: continue operation = "Would modify" if not opts.dry_run: # We have to use str(msg.dn) rather than just msg.dn as on # older versions of samba where this is most needed # modify_sd_on_dn can't handle a ldb.Dn object sd_helper.modify_sd_on_dn(str(msg.dn), new_desc) operation = "Modified" print("%s change-password ACL right for world on: %s" % ( operation, msg.dn)) if not opts.no_schema and search_scope != ldb.SCOPE_BASE: res = sam_ldb.search(base=sam_ldb.get_schema_basedn(), expression="(&(objectClass=classSchema)" "(defaultSecurityDescriptor=*))", attrs=["defaultSecurityDescriptor"]) dom_sid = security.dom_sid(sam_ldb.get_domain_sid()) for msg in res: desc = security.descriptor.from_sddl(msg["defaultSecurityDescriptor"][0], dom_sid) (changed, new_desc) = change_desc(desc) if not changed: continue operation = "Would modify" if not opts.dry_run: desc_sddl = new_desc.as_sddl(dom_sid) new_msg = ldb.Message() new_msg.dn = msg.dn new_msg["old"] = ldb.MessageElement(msg["defaultSecurityDescriptor"][0], ldb.FLAG_MOD_DELETE, "defaultSecurityDescriptor") new_msg["new"] = ldb.MessageElement([desc_sddl], ldb.FLAG_MOD_ADD, "defaultSecurityDescriptor") sam_ldb.modify(new_msg) operation = "Modified" print("%s change-password ACL right for world for new objects of: %s" % ( operation, msg.dn)) sam_ldb.transaction_commit()