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

Added user-status functionality to the SAMHashes Class of the secrestdump.py #1847

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
50 changes: 25 additions & 25 deletions examples/secretsdump.py
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -72,6 +72,7 @@
except NameError:
pass


class DumpSecrets:
def __init__(self, remoteName, username='', password='', domain='', options=None):
self.__useVSSMethod = options.use_vss
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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')
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]@]<targetName or address> or LOCAL'
' (if you want to parse local files)')
Expand All @@ -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',
Expand Down Expand Up @@ -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)

Expand Down
155 changes: 150 additions & 5 deletions impacket/examples/secretsdump.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,40 @@ class DOMAIN_ACCOUNT_F(Structure):
# ('Unknown4','<L=0'),
)

class DOMAIN_ACCOUNT_V(Structure):
structure = (
('Randomstuffforfun','<L=0'),
('SystemSid', ':', lambda data: data[-12:]),
('Data',':'),
)

class USER_ACCOUNT_C(Structure):
structure = (
('GroupNumber','<L=0'),
('Unknown','436s=b""'),
('GroupMembers',':'),
)

class USER_ACCOUNT_F(Structure):
structure = (
('Unknown','8s=b""'),
('LastLogonTimestamp','8s=b""'),
('Unknown2','8s=b""'),
('PasswordLastSetTimeStamp','8s=b""'),
('AccountExpiresTimeStamp','8s=b""'),
('LastIncorrectPasswordTimestamp','8s=b""'),
('UserNumber','<L=0'),
('Unknown3','<L=0'),
('GroupedData','H=0'),
('Unknown4','<H=0'),
('CountryCode','<H=0'),
('Unknown5','<H=0'),
('InvalidPWDCount','<H=0'),
('NumberOfLogons','<H=0'),
('Unknown6','<L=0'),
('Unknown7','8s=b""')
)

# Great help from here https://web.archive.org/web/20190717124313/http://www.beginningtoseethelight.org/ntsecurity/index.htm
class USER_ACCOUNT_V(Structure):
structure = (
Expand Down Expand Up @@ -227,6 +261,21 @@ class USER_ACCOUNT_V(Structure):
('Data',':=b""'),
)

class BUILTIN_GROUP_C(Structure):
structure = (
('Unknown1', '<16s=b""'), # First 16 bytes of unknown data
('NameOffset', '<L=0'), # Offset for the group name
('NameLength', '<L=0'), # Length of the group name
('Unknown2', '<L=0'), # Padding or unused data
('CommentOffset', '<L=0'), # Offset for the group comment
('CommentLength', '<L=0'), # Length of the group comment
('Unknown3', '<L=0'), # Padding or unused data
('UsersOffset', '<L=0'), # Offset for users
('Unknown4', '<L=0'), # Padding or unused data
('UserCount', '<L=0'), # Number of users
('Data', ':'), # Remaining data
)

class NL_RECORD(Structure):
structure = (
('UserLength','<H=0'),
Expand Down Expand Up @@ -1340,15 +1389,40 @@ def finish(self):
self.__registryHive.close()

class SAMHashes(OfflineRegistry):
def __init__(self, samFile, bootKey, isRemote = False, perSecretCallback = lambda secret: _print_helper(secret)):
def __init__(self, samFile, bootKey, isRemote = False, printUserStatus=False, perSecretCallback = lambda secret: _print_helper(secret)):
OfflineRegistry.__init__(self, samFile, isRemote)
self.__samFile = samFile
self.__hashedBootKey = b''
self.__bootKey = bootKey
self.__printUserStatus = printUserStatus
self.__cryptoCommon = CryptoCommon()
self.__itemsFound = {}
self.__perSecretCallback = perSecretCallback

def binary_to_sid(self, binary_data, without_prefix=False):
if len(binary_data) < 12:
return ""

if len(binary_data) == 12:
if not without_prefix:
rev = binary_data[0]
authid = hexlify(binary_data[2:8]).decode().lstrip("0")
sub = unpack("<L", binary_data[8:12])[0]
return f"S-{rev}-{authid}-{sub}"
else:
sections = [binary_data[i:i + 4][::-1] for i in range(0, 12, 4)]
decimals = [int.from_bytes(section, byteorder='big') for section in sections]
return f"S-1-5-21-{decimals[0]}-{decimals[1]}-{decimals[2]}"

if len(binary_data) > 12:
rev = binary_data[0]
authid = hexlify(binary_data[2:8]).decode().lstrip("0")
sub = "-".join(map(str, unpack("<LLLL", binary_data[8:24])))
rid = unpack("<L", binary_data[24:28])[0]
return f"S-{rev}-{authid}-{sub}-{rid}"

return ""

def MD5(self, data):
md5 = hashlib.new('md5')
md5.update(data)
Expand Down Expand Up @@ -1423,13 +1497,80 @@ def dump(self):
except:
pass

F = self.getValue(ntpath.join(r'SAM\Domains\Account','F'))[1]
domainData = DOMAIN_ACCOUNT_F(F)
LockoutThreshold = domainData['LockoutThreshold']

V = self.getValue(ntpath.join(r'SAM\Domains\Account','V'))[1]
domainDataV = DOMAIN_ACCOUNT_V(V)
system_sid = self.binary_to_sid(domainDataV['SystemSid'], without_prefix=True)

groups_root = r'SAM\Domains\Builtin\Aliases'
groups = OrderedDict()

for entry in self.enumKey(groups_root):
if not entry.startswith("00000"):
continue

data = self.getValue(ntpath.join(groups_root, entry, 'C'))[1]
group_data = BUILTIN_GROUP_C(data)

name_offset = group_data['NameOffset']
name_length = group_data['NameLength']
groupname = group_data['Data'][name_offset:name_offset + name_length].decode('utf-16')
user_count = group_data['UserCount']

groups[groupname] = {
'Group Name': groupname,
'User Count': user_count,
'Members': []
}

try:
new_offset = 0
for _ in range(500): # Check a maximum of 500 members
offset = group_data['UsersOffset'] + 52 + new_offset
entry_type = unpack("<L", data[offset:offset + 4])[0]

if entry_type in (257, 1281):
sid_length = 12 if entry_type == 257 else 28
sid = self.binary_to_sid(data[offset:offset + sid_length])
groups[groupname]['Members'].append(sid)
new_offset += sid_length

except Exception:
if not groups[groupname]['Members']:
groups[groupname]['Members'] = ['No users in this group']

local_admins = [
member.strip()
for group in groups.values()
if group['Group Name'] == 'Administrators'
for member in group['Members']
if member.strip()
]

for rid in rids:
userAccount = USER_ACCOUNT_V(self.getValue(ntpath.join(usersKey,rid,'V'))[1])
rid = int(rid,16)
disabled = locked_out = auto_locked = is_admin = False

V = userAccount['Data']
userAccountF = USER_ACCOUNT_F(self.getValue(ntpath.join(usersKey, rid, 'F'))[1])
InvalidPWDCount = userAccountF['InvalidPWDCount']
UserNumber = userAccountF['UserNumber']
user_sid = f"{system_sid}-{UserNumber}"

is_admin = user_sid in local_admins
locked = InvalidPWDCount >= 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)
Expand Down Expand Up @@ -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)

Expand Down
Loading