6262from binascii import unhexlify , hexlify
6363from collections import OrderedDict
6464from datetime import datetime , timedelta , timezone
65- from struct import unpack , pack
65+ from struct import unpack , pack , unpack_from
6666from six import b , PY2
6767
6868from 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+
156163class DOMAIN_ACCOUNT_F (Structure ):
157164 structure = (
158165 ('Revision' ,'<L=0' ),
@@ -1415,7 +1422,7 @@ def finish(self):
14151422 self .__registryHive .close ()
14161423
14171424class 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