Skip to content

Commit 7eb1170

Browse files
authored
Update secretsdump.py
1 parent 2cc4496 commit 7eb1170

File tree

1 file changed

+171
-29
lines changed

1 file changed

+171
-29
lines changed

impacket/examples/secretsdump.py

Lines changed: 171 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
from binascii import unhexlify, hexlify
6363
from collections import OrderedDict
6464
from datetime import datetime, timedelta, timezone
65-
from struct import unpack, pack
65+
from struct import unpack, pack, unpack_from
6666
from six import b, PY2
6767

6868
from impacket import LOG
@@ -153,6 +153,13 @@ class SAM_HASH_AES(Structure):
153153
('Hash',':'),
154154
)
155155

156+
SAM_V_VALUE_BASE = 0xCC
157+
AAD3 = unhexlify('aad3b435b51404eeaad3b435b51404ee')
158+
CONST_LM = b"LMPASSWORD\x00"
159+
CONST_NT = b"NTPASSWORD\x00"
160+
CONST_LMH = b"LMPASSWORDHISTORY\x00"
161+
CONST_NTH = b"NTPASSWORDHISTORY\x00"
162+
156163
class DOMAIN_ACCOUNT_F(Structure):
157164
structure = (
158165
('Revision','<L=0'),
@@ -1415,7 +1422,7 @@ def finish(self):
14151422
self.__registryHive.close()
14161423

14171424
class SAMHashes(OfflineRegistry):
1418-
def __init__(self, samFile, bootKey, isRemote = False, printUserStatus=False, perSecretCallback = lambda secret: _print_helper(secret)):
1425+
def __init__(self, samFile, bootKey, isRemote = False, printUserStatus=False, history=False, perSecretCallback = lambda secret: _print_helper(secret)):
14191426
OfflineRegistry.__init__(self, samFile, isRemote)
14201427
self.__samFile = samFile
14211428
self.__hashedBootKey = b''
@@ -1424,6 +1431,7 @@ def __init__(self, samFile, bootKey, isRemote = False, printUserStatus=False, pe
14241431
self.__cryptoCommon = CryptoCommon()
14251432
self.__itemsFound = {}
14261433
self.__perSecretCallback = perSecretCallback
1434+
self.__history = history
14271435

14281436
def binary_to_sid(self, binary_data, without_prefix=False):
14291437
if len(binary_data) < 12:
@@ -1534,6 +1542,94 @@ def __replaceValue(self, obj, offset, value):
15341542
obj[offset + i] = v
15351543
return bytes(obj)
15361544

1545+
@staticmethod
1546+
def _read_v_entry(v, index):
1547+
try:
1548+
entry_offset = unpack_from('<L', v, 0x0C * index)[0] + SAM_V_VALUE_BASE
1549+
entry_length = unpack_from('<L', v, 0x0C * index + 4)[0]
1550+
except Exception:
1551+
return None
1552+
if entry_offset == SAM_V_VALUE_BASE or entry_length == 0:
1553+
return None
1554+
if entry_offset + entry_length > len(v):
1555+
return None
1556+
record = v[entry_offset:entry_offset + entry_length]
1557+
if len(record) < 4:
1558+
return None
1559+
pek_id = unpack_from('<H', record, 0)[0]
1560+
revision = unpack_from('<H', record, 2)[0]
1561+
return entry_offset, entry_length, pek_id, revision, record
1562+
1563+
def _parse_record_hashes(self, record, record_length, rid, constant, revision):
1564+
if record_length < 8 or len(self.__hashedBootKey) < 16:
1565+
return []
1566+
1567+
if revision == 2:
1568+
data_length = unpack_from('<L', record, 0x04)[0] if record_length >= 0x08 else 0
1569+
1570+
if record_length <= 0x18 and data_length == 0:
1571+
return []
1572+
1573+
if record_length <= 0x18:
1574+
# Some offline hives mark history entries as revision 2 but still use RC4 layout
1575+
revision = 1
1576+
else:
1577+
if record_length < 0x18 + 16:
1578+
return []
1579+
iv = record[0x08:0x18]
1580+
if data_length == 0 or data_length % 16 != 0:
1581+
return []
1582+
if 0x18 + data_length > record_length:
1583+
return []
1584+
encrypted = record[0x18:0x18 + data_length]
1585+
decrypted = self.__cryptoCommon.decryptAES(self.__hashedBootKey[:16], encrypted, iv)
1586+
1587+
if revision == 1:
1588+
encrypted = record[8:record_length]
1589+
if not encrypted:
1590+
return []
1591+
md5 = hashlib.new('md5')
1592+
md5.update(self.__hashedBootKey[:16])
1593+
md5.update(pack('<L', rid))
1594+
md5.update(constant)
1595+
decrypted = ARC4.new(md5.digest()).encrypt(encrypted)
1596+
else:
1597+
return []
1598+
1599+
key1, key2 = self.__cryptoCommon.deriveKey(rid)
1600+
des1 = DES.new(key1, DES.MODE_ECB)
1601+
des2 = DES.new(key2, DES.MODE_ECB)
1602+
1603+
hashes = []
1604+
for idx in range(0, len(decrypted), 16):
1605+
block = decrypted[idx:idx + 16]
1606+
if len(block) != 16:
1607+
break
1608+
plain = des1.decrypt(block[:8]) + des2.decrypt(block[8:])
1609+
if plain in (b'\x00' * 16, b'\xff' * 16):
1610+
continue
1611+
hashes.append(plain)
1612+
return hashes
1613+
1614+
def _extract_history_hashes(self, v_data, rid):
1615+
nt_hist_entry = self._read_v_entry(v_data, 15)
1616+
lm_hist_entry = self._read_v_entry(v_data, 16)
1617+
1618+
lm_history = []
1619+
nt_history = []
1620+
1621+
if lm_hist_entry:
1622+
_, length, _, revision, record = lm_hist_entry
1623+
constant = CONST_LMH if revision == 1 else b''
1624+
lm_history = list(self._parse_record_hashes(record, length, rid, constant, revision))
1625+
1626+
if nt_hist_entry:
1627+
_, length, _, revision, record = nt_hist_entry
1628+
constant = CONST_NTH if revision == 1 else b''
1629+
nt_history = list(self._parse_record_hashes(record, length, rid, constant, revision))
1630+
1631+
return lm_history, nt_history
1632+
15371633
def dump(self):
15381634
NTPASSWORD = b"NTPASSWORD\0"
15391635
LMPASSWORD = b"LMPASSWORD\0"
@@ -1633,7 +1729,8 @@ def dump(self):
16331729
auto_locked = bool(grouped_data & 0x0400)
16341730
locked_out = locked
16351731

1636-
userAccount = USER_ACCOUNT_V(self.getValue(ntpath.join(usersKey, rid, 'V'))[1])
1732+
v_raw = self.getValue(ntpath.join(usersKey, rid, 'V'))[1]
1733+
userAccount = USER_ACCOUNT_V(v_raw)
16371734
rid = int(rid, 16)
16381735

16391736
V = userAccount['Data']
@@ -1643,44 +1740,89 @@ def dump(self):
16431740
logging.debug('The account %s doesn\'t have hash information.' % userName)
16441741
continue
16451742

1646-
encNTHash = b''
1647-
if V[userAccount['NTHashOffset']:][2:3] == b'\x01':
1648-
# Old Style hashes
1649-
newStyle = False
1650-
if userAccount['LMHashLength'] == 20:
1651-
encLMHash = SAM_HASH(V[userAccount['LMHashOffset']:][:userAccount['LMHashLength']])
1652-
if userAccount['NTHashLength'] == 20:
1653-
encNTHash = SAM_HASH(V[userAccount['NTHashOffset']:][:userAccount['NTHashLength']])
1654-
else:
1655-
# New Style hashes
1656-
newStyle = True
1657-
if userAccount['LMHashLength'] == 24:
1658-
encLMHash = SAM_HASH_AES(V[userAccount['LMHashOffset']:][:userAccount['LMHashLength']])
1659-
encNTHash = SAM_HASH_AES(V[userAccount['NTHashOffset']:][:userAccount['NTHashLength']])
1743+
newStyle = V[userAccount['NTHashOffset']:][2:3] != b'\x01'
16601744

1661-
LOG.debug('NewStyle hashes is: %s' % newStyle)
1745+
encLMHash = None
16621746
if userAccount['LMHashLength'] >= 20:
1663-
lmHash = self.__decryptHash(rid, encLMHash, LMPASSWORD, newStyle)
1664-
else:
1665-
lmHash = b''
1747+
raw_lm = V[userAccount['LMHashOffset']:][:userAccount['LMHashLength']]
1748+
encLMHash = SAM_HASH_AES(raw_lm) if newStyle else SAM_HASH(raw_lm)
16661749

1667-
if encNTHash != b'':
1668-
ntHash = self.__decryptHash(rid, encNTHash, NTPASSWORD, newStyle)
1750+
raw_nt = V[userAccount['NTHashOffset']:][:userAccount['NTHashLength']]
1751+
if newStyle:
1752+
encNTHash = SAM_HASH_AES(raw_nt)
16691753
else:
1670-
ntHash = b''
1754+
encNTHash = SAM_HASH(raw_nt)
1755+
1756+
lmHash = b''
1757+
if encLMHash is not None:
1758+
try:
1759+
lmHash = self.__decryptHash(rid, encLMHash, LMPASSWORD, newStyle)
1760+
except Exception:
1761+
LOG.debug('Failed to decrypt LM hash for %s', userName, exc_info=True)
1762+
lmHash = b''
1763+
1764+
ntHash = b''
1765+
if encNTHash is not None:
1766+
try:
1767+
ntHash = self.__decryptHash(rid, encNTHash, NTPASSWORD, newStyle)
1768+
except Exception:
1769+
LOG.debug('Failed to decrypt NT hash for %s', userName, exc_info=True)
1770+
ntHash = b''
16711771

16721772
if lmHash == b'':
1673-
lmHash = ntlm.LMOWFv1('','')
1773+
lmHash = ntlm.LMOWFv1('', '')
16741774
if ntHash == b'':
1675-
ntHash = ntlm.NTOWFv1('','')
1775+
ntHash = ntlm.NTOWFv1('', '')
16761776

1677-
answer = "%s:%d:%s:%s:::" % (userName, rid, hexlify(lmHash).decode('utf-8'), hexlify(ntHash).decode('utf-8'))
1777+
lm_hex = hexlify(lmHash).decode('utf-8')
1778+
nt_hex = hexlify(ntHash).decode('utf-8')
1779+
1780+
LOG.debug('NewStyle hashes is: %s' % newStyle)
1781+
1782+
answer = "%s:%d:%s:%s:::" % (userName, rid, lm_hex, nt_hex)
16781783

16791784
if self.__printUserStatus is True:
16801785
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'})"
16811786

16821787
self.__itemsFound[rid] = answer
16831788
self.__perSecretCallback(answer)
1789+
1790+
if self.__history:
1791+
lm_history_raw, nt_history_raw = self._extract_history_hashes(v_raw, rid)
1792+
entry_count = max(len(lm_history_raw), len(nt_history_raw))
1793+
if entry_count == 0:
1794+
continue
1795+
1796+
blank_lm_hex = hexlify(ntlm.LMOWFv1('', '')).decode('utf-8')
1797+
1798+
history_entries = []
1799+
1800+
for idx in range(entry_count):
1801+
lm_bytes = lm_history_raw[idx] if idx < len(lm_history_raw) else b''
1802+
nt_bytes = nt_history_raw[idx] if idx < len(nt_history_raw) else b''
1803+
1804+
if lm_bytes in (b'', AAD3):
1805+
lm_val = blank_lm_hex
1806+
else:
1807+
lm_val = hexlify(lm_bytes).decode('utf-8')
1808+
1809+
if nt_bytes:
1810+
nt_val = hexlify(nt_bytes).decode('utf-8')
1811+
else:
1812+
nt_val = hexlify(ntlm.NTOWFv1('', '')).decode('utf-8')
1813+
1814+
# Some hives mirror the current password into the first history slot.
1815+
if lm_val == lm_hex and nt_val == nt_hex:
1816+
continue
1817+
1818+
history_line = f"{userName}_history{idx}:{rid}:{lm_val}:{nt_val}:::"
1819+
history_entries.append(history_line)
1820+
1821+
if not history_entries:
1822+
continue
1823+
1824+
for history_line in history_entries:
1825+
self.__perSecretCallback(history_line)
16841826

16851827
def edit(self, user, newNTHash, newLMHash=b''):
16861828
NTPASSWORD = b"NTPASSWORD\0"
@@ -2417,8 +2559,8 @@ def __init__(self, ntdsFile, bootKey, isRemote=False, history=False, noLMHash=Tr
24172559
self.__skipUser = skipUser
24182560
self.__perSecretCallback = perSecretCallback
24192561

2420-
# these are all the columns that we need to get the secrets.
2421-
# If in the future someone finds other columns containing interesting things please extend ths table.
2562+
# these are all the columns that we need to get the secrets.
2563+
# If in the future someone finds other columns containing interesting things please extend ths table.
24222564
self.__filter_tables_usersecret = {
24232565
self.NAME_TO_INTERNAL['objectSid'] : 1,
24242566
self.NAME_TO_INTERNAL['dBCSPwd'] : 1,

0 commit comments

Comments
 (0)