From 6542a4439fc0191ff4e48e74ad7d02233793c9d9 Mon Sep 17 00:00:00 2001 From: Mark Bregman Date: Fri, 15 Nov 2024 16:57:25 +0100 Subject: [PATCH] Added functionality to the SAMHashes Class of the secrestdump.py library to be able to print the user status for SAM dumps. There was already a user-status flag for the NTDS dumps, but not for the SAM dumps. Now, when directly calling secretsdump.py to make a SAM dump, the user can specify the -user-status flag, just like with the NTDS dump. Alternatively, when other tools are using the Secretsdump library, they can simply initiate the SAMHashes class with the printUserStatus flag set to True. The default is False, so if you don't specify anything when calling the Secretsdump Library it will do exactly as it did before. This should not break any existing tools. --- examples/secretsdump.py | 50 +++++----- impacket/examples/secretsdump.py | 155 ++++++++++++++++++++++++++++++- 2 files changed, 175 insertions(+), 30 deletions(-) diff --git a/examples/secretsdump.py b/examples/secretsdump.py index 1ab5616ed1..b43492f8a4 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Impacket - Collection of Python classes for working with network protocols. # -# Copyright Fortra, LLC and its affiliated companies +# Copyright Fortra, LLC and its affiliated companies # # All rights reserved. # @@ -72,6 +72,7 @@ except NameError: pass + class DumpSecrets: def __init__(self, remoteName, username='', password='', domain='', options=None): self.__useVSSMethod = options.use_vss @@ -111,7 +112,7 @@ def __init__(self, remoteName, username='', password='', domain='', options=None self.__ldapFilter = options.ldapfilter self.__skipUser = options.skip_user self.__pwdLastSet = options.pwd_last_set - self.__printUserStatus= options.user_status + self.__printUserStatus = options.user_status self.__resumeFileName = options.resumefile self.__canProcessSAMLSA = True self.__kdcHost = options.dc_ip @@ -215,7 +216,7 @@ def dump(self): localOperations = LocalOperations(self.__systemHive) bootKey = localOperations.getBootKey() if self.__ntdsFile is not None: - # Let's grab target's configuration about LM Hashes storage + # Let's grab target's configuration about LM Hashes storage self.__noLMHash = localOperations.checkNoLMHashPolicy() else: import binascii @@ -243,7 +244,7 @@ def dump(self): else: raise - self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost, self.__ldapConnection) + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost, self.__ldapConnection) self.__remoteOps.setExecMethod(self.__options.exec_method) if self.__justDC is False and self.__justDCNTLM is False and self.__useKeyListMethod is False or self.__useVSSMethod is True: self.__remoteOps.enableRegistry() @@ -253,7 +254,7 @@ def dump(self): except Exception as e: self.__canProcessSAMLSA = False if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ - and self.__doKerberos is True: + and self.__doKerberos is True: # Giving some hints here when SPN target name validation is set to something different to Off # This will prevent establishing SMB connections using TGS for SPNs different to cifs/ logging.error('Policy SPN target name validation might be restricting full DRSUAPI dump. Try -just-dc-user') @@ -276,8 +277,7 @@ def dump(self): SAMFileName = self.__remoteOps.saveSAM() else: SAMFileName = self.__samHive - - self.__SAMHashes = SAMHashes(SAMFileName, bootKey, isRemote = self.__isRemote) + self.__SAMHashes = SAMHashes(SAMFileName, bootKey, isRemote=self.__isRemote, printUserStatus=self.__printUserStatus) self.__SAMHashes.dump() if self.__outputFileName is not None: self.__SAMHashes.export(self.__outputFileName) @@ -292,7 +292,7 @@ def dump(self): SECURITYFileName = self.__securityHive self.__LSASecrets = LSASecrets(SECURITYFileName, bootKey, self.__remoteOps, - isRemote=self.__isRemote, history=self.__history) + isRemote=self.__isRemote, history=self.__history) self.__LSASecrets.dumpCachedHashes() if self.__outputFileName is not None: self.__LSASecrets.exportCached(self.__outputFileName) @@ -318,7 +318,7 @@ def dump(self): noLMHash=self.__noLMHash, remoteOps=self.__remoteOps, useVSSMethod=self.__useVSSMethod, justNTLM=self.__justDCNTLM, pwdLastSet=self.__pwdLastSet, resumeSession=self.__resumeFileName, - outputFileName=self.__outputFileName, justUser=self.__justUser, + outputFileName=self.__outputFileName, justUser=self.__justUser, skipUser=self.__skipUser, ldapFilter=self.__ldapFilter, printUserStatus=self.__printUserStatus) try: @@ -349,7 +349,7 @@ def dump(self): if self.__NTDSHashes is not None: if isinstance(e, KeyboardInterrupt): while True: - answer = input("Delete resume session file? [y/N] ") + answer = input("Delete resume session file? [y/N] ") if answer.upper() == '': answer = 'N' break @@ -391,8 +391,8 @@ def cleanup(self): print(version.BANNER) - parser = argparse.ArgumentParser(add_help = True, description = "Performs various techniques to dump secrets from " - "the remote machine without executing any agent there.") + parser = argparse.ArgumentParser(add_help=True, description="Performs various techniques to dump secrets from " + "the remote machine without executing any agent there.") parser.add_argument('target', action='store', help='[[domain/]username[:password]@] or LOCAL' ' (if you want to parse local files)') @@ -404,8 +404,8 @@ def cleanup(self): parser.add_argument('-sam', action='store', help='SAM hive to parse') parser.add_argument('-ntds', action='store', help='NTDS.DIT file to parse') parser.add_argument('-resumefile', action='store', help='resume file name to resume NTDS.DIT session dump (only ' - 'available to DRSUAPI approach). This file will also be used to keep updating the session\'s ' - 'state') + 'available to DRSUAPI approach). This file will also be used to keep updating the session\'s ' + 'state') parser.add_argument('-skip-sam', action='store_true', help='Do NOT parse the SAM hive on remote system') parser.add_argument('-skip-security', action='store_true', help='Do NOT parse the SECURITY hive on remote system') parser.add_argument('-outputfile', action='store', @@ -433,35 +433,35 @@ def cleanup(self): help='Extract only NTDS.DIT data for specific users based on an LDAP filter. ' 'Only available for DRSUAPI approach. Implies also -just-dc switch') group.add_argument('-just-dc', action='store_true', default=False, - help='Extract only NTDS.DIT data (NTLM hashes and Kerberos keys)') + help='Extract only NTDS.DIT data (NTLM hashes and Kerberos keys)') group.add_argument('-just-dc-ntlm', action='store_true', default=False, help='Extract only NTDS.DIT data (NTLM hashes only)') group.add_argument('-skip-user', action='store', help='Do NOT extract NTDS.DIT data for the user specified. ' - 'Can provide comma-separated list of users to skip, or text file with one user per line') + 'Can provide comma-separated list of users to skip, or text file with one user per line') group.add_argument('-pwd-last-set', action='store_true', default=False, help='Shows pwdLastSet attribute for each NTDS.DIT account. Doesn\'t apply to -outputfile data') group.add_argument('-user-status', action='store_true', default=False, - help='Display whether or not the user is disabled') + help='Display whether or not the user is disabled') group.add_argument('-history', action='store_true', help='Dump password history, and LSA secrets OldVal') group = parser.add_argument_group('authentication') - group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' - '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use' - ' the ones specified in the command line') - group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication' - ' (128 or 256 bits)') + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use' + ' the ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication' + ' (128 or 256 bits)') group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') group = parser.add_argument_group('connection') - group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' - 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') group.add_argument('-target-ip', action='store', metavar="ip address", help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' 'This is useful when target is the NetBIOS name and you cannot resolve it') - if len(sys.argv)==1: + if len(sys.argv) == 1: parser.print_help() sys.exit(1) diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index 43b776218c..9300287f08 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -181,6 +181,40 @@ class DOMAIN_ACCOUNT_F(Structure): # ('Unknown4',' 12: + rev = binary_data[0] + authid = hexlify(binary_data[2:8]).decode().lstrip("0") + sub = "-".join(map(str, unpack("= LockoutThreshold + + grouped_data = userAccountF['GroupedData'] + disabled = bool(grouped_data & 0x0001) + auto_locked = bool(grouped_data & 0x0400) + locked_out = locked - userName = V[userAccount['NameOffset']:userAccount['NameOffset']+userAccount['NameLength']].decode('utf-16le') + userAccount = USER_ACCOUNT_V(self.getValue(ntpath.join(usersKey, rid, 'V'))[1]) + rid = int(rid, 16) + + V = userAccount['Data'] + userName = V[userAccount['NameOffset']:userAccount['NameOffset'] + userAccount['NameLength']].decode('utf-16le') if userAccount['NTHashLength'] == 0: logging.error('SAM hashes extraction for user %s failed. The account doesn\'t have hash information.' % userName) @@ -1467,6 +1608,10 @@ def dump(self): ntHash = ntlm.NTOWFv1('','') answer = "%s:%d:%s:%s:::" % (userName, rid, hexlify(lmHash).decode('utf-8'), hexlify(ntHash).decode('utf-8')) + + if self.__printUserStatus is True: + answer = f"{answer} (Enabled={'False' if disabled else 'True'}) (Locked={'True' if locked_out or auto_locked else 'False'}) (Admin={'True' if is_admin else 'False'})" + self.__itemsFound[rid] = answer self.__perSecretCallback(answer)