Skip to content
Merged
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
19 changes: 19 additions & 0 deletions dploot/lib/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,22 @@
"Default User",
"All Users",
]

# simple class to check for false positives, case insensitive
class FalsePositives(list):
def __init__(self,
false_positives: list[str] = None
) -> None:
if false_positives is None:
false_positives = FALSE_POSITIVES

super().__init__(map (lambda x: x.lower(), false_positives))

def __contains__(self, name):
return super().__contains__(str(name).lower())

def __setitem__(self, key, value):
return super().__setitem__(key,str(value).lower())

def append(self, element):
return super().append(str(element).lower())
4 changes: 2 additions & 2 deletions dploot/lib/masterkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@


class Masterkey:
def __init__(self, guid, blob = None, sid = None, key = None, sha1 = None, user: str = "None") -> None:
def __init__(self, guid, blob = None, sid:str = None, key = None, sha1 = None, user: str = "None") -> None:
self.guid = guid
self.blob = blob
self.sid = sid
self.sid = str(sid).upper()
self.user = user

self.key = key
Expand Down
52 changes: 41 additions & 11 deletions dploot/lib/smb.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
import os
import logging
import time

from pathlib import Path
from typing import Any, Dict, List, Optional

from dploot.lib.target import Target
from dploot.lib.consts import FalsePositives

from impacket.smbconnection import SMBConnection
from impacket.winregistry import Registry
Expand All @@ -22,8 +25,6 @@
)

from dploot.lib.wmi import DPLootWmiExec
from dploot.lib.consts import FALSE_POSITIVES


class DPLootSMBConnection:
# if called with target = LOCAL, return an instance of DPLootLocalSMConnection,
Expand All @@ -43,14 +44,14 @@ def __new__(
# we end up here when a child class is instantiated.
return super().__new__(cls)

def __init__(self, target: Target, false_positive: List[str] = FALSE_POSITIVES) -> None:
def __init__(self, target: Target, false_positive: List[str] | None = None) -> None:
self.target = target
self.remote_ops = None
self.local_session = None

self._usersProfiles = None

self.false_positive = false_positive
self.false_positive = FalsePositives(false_positive)

def listDirs(self, share: str, dirlist: List[str]) -> Dict[str, Any]:
result = {}
Expand Down Expand Up @@ -390,8 +391,8 @@ def _sharedfile_fromdirentry(d: os.DirEntry):

SharedFile.fromDirEntry = _sharedfile_fromdirentry

def remote_list_dir(self, share, path, wildcard=True) -> "Any | None":
path = os.path.join(self.target.local_root, path.replace("\\", os.sep))
def remote_list_dir(self, share, path, wildcard=True) -> list[SharedFile]:
path = self.get_real_path(path)
if not wildcard:
raise NotImplementedError("Not implemented for wildcard == False")
try:
Expand All @@ -418,6 +419,36 @@ def listPath(self, shareName: str = "C$", path: Optional[str] = None, password:
def getFile(self, *args, **kwargs) -> "Any | None":
raise NotImplementedError("getFile is not implemented in LOCAL mode")

def get_real_path(self, path:str) -> str:
"""Match path against file system (case insensitive if py>=3.12).
Only used when target is `LOCAL`.

Args:
path (str): pah representation (ie C:\\Windows\\...)

Returns:
str: real path on the filesystem
"""
# clean path (remove c:\, /, and current root if already present)
path=path.removeprefix(self.target.local_root)
if path[:3].lower() == "c:\\":
path = path[3:]
path=path.replace("\\", os.sep).lstrip(os.sep)

globok = False
# The pattern to match does not contain jokers, so Path.glob() should return 0 or 1 match
try:
path=next(Path(self.target.local_root).glob(path, case_sensitive=False))
globok=True
except (StopIteration, TypeError):
# StopIteration: path does not exist.
# TypeError: unexpexted keyword (case_sensitive added in python 3.12)
# Return a representation of path anyway
path=os.path.join(self.target.local_root, path)

#logging.debug(f"get_real_path: [{globok=}] returning {path}")
return str(path)

def readFile(
self,
shareName,
Expand All @@ -431,12 +462,10 @@ def readFile(
) -> bytes:
data = None
try:
with open(
os.path.join(self.target.local_root, path.replace("\\", os.sep)), "rb"
) as f:
with open(self.get_real_path(path), "rb") as f:
data = f.read()
except Exception as e:
logging.debug(f"Exception occurred while trying to read {path}: {e}")
logging.debug(f"Exception occurred while trying to read {path}: {repr(e)}")

return data

Expand All @@ -453,7 +482,7 @@ def getUsersProfiles(self) -> dict[str, str] | None:

result = {}
# open hive
reg_file_path = os.path.join(self.target.local_root, self.hklm_software_path)
reg_file_path = self.get_real_path(self.hklm_software_path)
reg = Registry(reg_file_path, isRemote=False)

# open key
Expand All @@ -474,6 +503,7 @@ def getUsersProfiles(self) -> dict[str, str] | None:
.replace(r"%systemroot%", self.systemroot)
)
path = ntpath.normpath(path)
path = self.get_real_path(path)
# store in result dict
result[user_sid] = path

Expand Down
2 changes: 1 addition & 1 deletion dploot/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def is_certificate_guid(value: str):


def is_credfile(value: str):
guid = re.compile(r"[A-F0-9]{32}")
guid = re.compile(r"[A-F0-9a-f]{32}")
return guid.match(value)


Expand Down
13 changes: 6 additions & 7 deletions dploot/triage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,27 @@
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.masterkey import Masterkey
from dploot.lib.consts import FALSE_POSITIVES
from dploot.lib.consts import FalsePositives

# Define base triage class.

class Triage(ABC):
class Triage:
"""
Abstract Class Definition for the DPLoot Triage Class.
Class Definition for the DPLoot Triage Class.
"""
@abstractmethod
def __init__(
self,
target: Target,
conn: DPLootSMBConnection,
masterkeys: List[Masterkey] = None,
per_loot_callback: Callable = None,
false_positive: List[str] = FALSE_POSITIVES,
false_positive: List[str] | None = None,
) -> None:

self.target = target
self.conn = conn
self.masterkeys = masterkeys
self.per_loot_callback = per_loot_callback
self.false_positive = false_positive
self.false_positive = FalsePositives(false_positive)

self.looted_files = {}
60 changes: 32 additions & 28 deletions dploot/triage/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from impacket.structure import Structure


from dploot.lib.consts import FALSE_POSITIVES
from dploot.lib.dpapi import decrypt_blob, find_masterkey_for_blob
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.target import Target
Expand Down Expand Up @@ -173,7 +172,7 @@ def __init__(
conn: DPLootSMBConnection,
masterkeys: List[Masterkey],
per_secret_callback: Callable = None,
false_positive: List[str] = FALSE_POSITIVES,
false_positive: List[str] | None = None,
) -> None:
super().__init__(
target,
Expand Down Expand Up @@ -243,35 +242,40 @@ def triage_chrome_browsers_for_user(
f"Found {browser.upper()} AppData files for user {user}"
)
aesStateKey_json = json.loads(aesStateKey_bytes)
profiles = aesStateKey_json['profile']['profiles_order']
blob = base64.b64decode(aesStateKey_json["os_crypt"]["encrypted_key"])
if blob[:5] == b"DPAPI":
dpapi_blob = blob[5:]
masterkey = find_masterkey_for_blob(
dpapi_blob, masterkeys=self.masterkeys
)
if masterkey is not None:
aeskey = decrypt_blob(
blob_bytes=dpapi_blob, masterkey=masterkey
)

if "app_bound_encrypted_key" in aesStateKey_json["os_crypt"]:
app_bound_blob = base64.b64decode(aesStateKey_json["os_crypt"]["app_bound_encrypted_key"])
dpapi_blob = app_bound_blob[4:] # Trim off APPB
masterkey = find_masterkey_for_blob(
try:
blob = base64.b64decode(aesStateKey_json["os_crypt"]["encrypted_key"])
if blob[:5] == b"DPAPI":
dpapi_blob = blob[5:]
masterkey = find_masterkey_for_blob(
dpapi_blob, masterkeys=self.masterkeys
)
if masterkey is not None:
intermediate_key = decrypt_blob(
blob_bytes=dpapi_blob, masterkey=masterkey
)
if masterkey is not None:
aeskey = decrypt_blob(
blob_bytes=dpapi_blob, masterkey=masterkey
)

if "app_bound_encrypted_key" in aesStateKey_json["os_crypt"]:
app_bound_blob = base64.b64decode(aesStateKey_json["os_crypt"]["app_bound_encrypted_key"])
dpapi_blob = app_bound_blob[4:] # Trim off APPB
masterkey = find_masterkey_for_blob(
intermediate_key, masterkeys=self.masterkeys
)
if masterkey:
app_bound_key = AppBoundKey(decrypt_blob(
blob_bytes=intermediate_key, masterkey=masterkey
)).key
dpapi_blob, masterkeys=self.masterkeys
)
if masterkey is not None:
intermediate_key = decrypt_blob(
blob_bytes=dpapi_blob, masterkey=masterkey
)
masterkey = find_masterkey_for_blob(
intermediate_key, masterkeys=self.masterkeys
)
if masterkey:
app_bound_key = AppBoundKey(decrypt_blob(
blob_bytes=intermediate_key, masterkey=masterkey
)).key
profiles = aesStateKey_json['profile']['profiles_order']
except KeyError as e:
logging.debug(f"Key not found! {repr(e)}")
# logging.debug(f"{aesStateKey_json=}")

for profile in profiles:
loginData_bytes = self.conn.readFile(
shareName=self.share,
Expand Down
35 changes: 24 additions & 11 deletions dploot/triage/certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from pyasn1.type.char import UTF8String

from dploot.triage import Triage
from dploot.lib.consts import FALSE_POSITIVES
from dploot.lib.crypto import CERTBLOB
from dploot.lib.dpapi import decrypt_privatekey, find_masterkey_for_privatekey_blob
from dploot.lib.smb import DPLootSMBConnection
Expand All @@ -49,8 +48,8 @@ class Certificate:
def dump(self) -> None:
print("Issuer:\t\t\t%s" % str(self.cert.issuer.rfc4514_string()))
print("Subject:\t\t%s" % str(self.cert.subject.rfc4514_string()))
print("Valid Date:\t\t%s" % self.cert.not_valid_before)
print("Expiry Date:\t\t%s" % self.cert.not_valid_after)
print("Valid Date (UTC):\t%s" % self.cert.not_valid_before_utc)
print("Expiry Date (UTC):\t%s" % self.cert.not_valid_after_utc)
print("Extended Key Usage:")
for i in self.cert.extensions.get_extension_for_oid(
oid=ExtensionOID.EXTENDED_KEY_USAGE
Expand Down Expand Up @@ -92,7 +91,7 @@ def __init__(
conn: DPLootSMBConnection,
masterkeys: List[Masterkey],
per_certificate_callback: Callable = None,
false_positive: List[str] = FALSE_POSITIVES,
false_positive: List[str] | None = None,
) -> None:
super().__init__(
target,
Expand All @@ -113,7 +112,12 @@ def triage_system_certificates(self) -> List[Certificate]:
self.conn.enable_remoteops()
certificates = []
pkeys = self.loot_privatekeys()
logging.debug(f'Got {len(pkeys)} private key(s).')
# stop here if no private key has been found.
if not pkeys:
return certificates
certs = self.loot_system_certificates()
logging.debug(f'Got {len(certs)} certificate(s).')
if len(pkeys) > 0 and len(certs) > 0:
certificates = self.correlate_certificates_and_privatekeys(
certs=certs, private_keys=pkeys, winuser="SYSTEM"
Expand Down Expand Up @@ -153,8 +157,12 @@ def loot_system_certificates(self) -> Dict[str, x509.Certificate]:
continue

# store in certificates dict
cert = self.der_to_cert(certblob.der)
certificates[certificate_key] = cert
try:
cert = self.der_to_cert(certblob.der)
certificates[certificate_key] = cert
except Exception as e:
logging.debug(f'Excetpion while converting certificate: {repr(e)}')
continue
reg.close()
else:
ans = rrp.hOpenLocalMachine(self.conn.remote_ops._RemoteOperations__rrp)
Expand Down Expand Up @@ -200,8 +208,11 @@ def loot_system_certificates(self) -> Dict[str, x509.Certificate]:
)
certblob = CERTBLOB(certblob_bytes)
if certblob.der is not None:
cert = self.der_to_cert(certblob.der)
certificates[certificate_key] = cert
try:
cert = self.der_to_cert(certblob.der)
certificates[certificate_key] = cert
except Exception as e:
logging.debug(f'Excetpion while converting certificate: {repr(e)}')
rrp.hBaseRegCloseKey(
self.conn.remote_ops._RemoteOperations__rrp, keyHandle
)
Expand Down Expand Up @@ -254,8 +265,8 @@ def loot_privatekeys(
d not in self.false_positive
and d.is_directory() > 0
and (
d.get_longname()[:2] == "S-"
or d.get_longname() == "MachineKeys"
d.get_longname()[:2].upper() == "S-"
or d.get_longname().upper() == "MachineKeys".upper()
)
):
sid = d.get_longname()
Expand Down Expand Up @@ -317,7 +328,9 @@ def loot_certificates(
if certblob.der is not None:
cert = self.der_to_cert(certblob.der)
certificates[certname] = cert
except Exception:
logging.debug(f'added cert {cert.subject}')
except Exception as e:
logging.debug(repr(e))
pass
return certificates

Expand Down
3 changes: 1 addition & 2 deletions dploot/triage/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@


from dploot.triage import Triage
from dploot.lib.consts import FALSE_POSITIVES
from dploot.lib.dpapi import decrypt_credential, find_masterkey_for_credential_blob
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.target import Target
Expand Down Expand Up @@ -54,7 +53,7 @@ def __init__(
conn: DPLootSMBConnection,
masterkeys: List[Masterkey],
per_credential_callback: Callable = None,
false_positive: List[str] = FALSE_POSITIVES,
false_positive: List[str] | None = None,
) -> None:
super().__init__(
target,
Expand Down
Loading