Skip to content
2 changes: 2 additions & 0 deletions dissect/target/plugins/os/windows/credential/credhist.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def _parse(self) -> Iterator[CredHistEntry]:
raw=entry,
)
except EOFError:
# An empty CREDHIST file will be 24 bytes long and has dwNextLinkSize set to 0.
pass

def decrypt(self, password_hash: bytes) -> None:
Expand Down Expand Up @@ -178,6 +179,7 @@ def check_compatible(self) -> None:
@export(record=CredHistRecord)
def credhist(self) -> Iterator[CredHistRecord]:
"""Yield and decrypt all Windows CREDHIST entries on the target."""

passwords = keychain_passwords()

if not passwords:
Expand Down
27 changes: 18 additions & 9 deletions dissect/target/plugins/os/windows/credential/lsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class LSAPlugin(Plugin):
- https://learn.microsoft.com/en-us/windows/win32/secauthn/lsa-authentication
- https://moyix.blogspot.com/2008/02/decrypting-lsa-secrets.html (Windows XP)
- https://github.com/fortra/impacket/blob/master/impacket/examples/secretsdump.py
- ReVaulting decryption and opportunities SANS Summit Prague 2015
"""

__namespace__ = "lsa"
Expand Down Expand Up @@ -83,23 +84,31 @@ def lsakey(self) -> bytes:

@cached_property
def _secrets(self) -> dict[str, bytes] | None:
"""Return dict of Windows system decrypted LSA secrets."""
"""Return dict of Windows system decrypted LSA secrets.

Includes current values (``CurrVal``) and the previous value (``OldVal``).
Key names are suffixed with ``_OldVal`` if an old value is found in the registry.
"""
if not self.target.ntversion:
raise ValueError("Unable to determine Windows NT version")

result = {}
for subkey in self.target.registry.key(self.SECURITY_POLICY_KEY).subkey("Secrets").subkeys():
enc_data = subkey.subkey("CurrVal").value("(Default)").value
for val in ["CurrVal", "OldVal"]:
try:
enc_data = subkey.subkey(val).value("(Default)").value
except RegistryKeyNotFoundError:
continue

# Windows Vista or newer
if float(self.target.ntversion) >= 6.0:
secret = _decrypt_aes(enc_data, self.lsakey)
# Windows Vista or newer
if float(self.target.ntversion) >= 6.0:
secret = _decrypt_aes(enc_data, self.lsakey)

# Windows XP
else:
secret = _decrypt_des(enc_data, self.lsakey)
# Windows XP
else:
secret = _decrypt_des(enc_data, self.lsakey)

result[subkey.name] = secret
result[f"{subkey.name}{'_OldVal' if val == 'OldVal' else ''}"] = secret

return result

Expand Down
53 changes: 44 additions & 9 deletions dissect/target/plugins/os/windows/dpapi/dpapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,36 @@
"""Cached map of username to SID."""
return {user.name: user.sid for user in self.target.users()}

def decrypt_system_blob(self, data: bytes) -> bytes:
"""Decrypt the given bytes using the SYSTEM master key."""
return self.decrypt_user_blob(data, sid=self.SYSTEM_SID)
def decrypt_system_blob(self, data: bytes, **kwargs) -> bytes:
"""Decrypt the given bytes using the SYSTEM master key.

def decrypt_user_blob(self, data: bytes, username: str | None = None, sid: str | None = None) -> bytes:
"""Decrypt the given bytes using the master key of the given SID or username."""
Args:
data: Bytes of DPAPI system blob to decrypt.
**kwargs: Arbitrary named arguments to pass to :meth:`DPAPIBlob.decrypt <dissect.target.plugins.os.windows.dpapi.blob.Blob.decrypt>` function.

Raises:
ValueError: When conditions to decrypt are not met or if decrypting failed.

Returns:
Decrypted bytes.
""" # noqa: E501
return self.decrypt_user_blob(data, sid=self.SYSTEM_SID, **kwargs)

def decrypt_user_blob(self, data: bytes, username: str | None = None, sid: str | None = None, **kwargs) -> bytes:
"""Decrypt the given bytes using the master key of the given SID or username.

Args:
data: Bytes of DPAPI blob to decrypt.
username: Username of the owner of the DPAPI blob.
sid: SID of the owner of the DPAPI blob.
**kwargs: Arbitrary named arguments to pass to :meth:`DPAPIBlob.decrypt <dissect.target.plugins.os.windows.dpapi.blob.Blob.decrypt>` function.

Raises:
ValueError: When conditions to decrypt are not met or if decrypting failed.

Returns:
Decrypted bytes.
""" # noqa: E501

if not sid and not username:
raise ValueError("Either sid or username argument is required")
Expand All @@ -168,21 +192,32 @@
if not (mk := self.master_keys.get(sid, {}).get(blob.guid)):
raise ValueError(f"Blob is encrypted using master key {blob.guid} that we do not have for SID {sid}")

if not blob.decrypt(mk.key):
if not blob.decrypt(mk.key, **kwargs):
raise ValueError(f"Failed to decrypt blob for SID {sid}")

return blob.clear_text

def decrypt_blob(self, data: bytes) -> bytes:
"""Attempt to decrypt the given bytes using any of the available master keys."""
def decrypt_blob(self, data: bytes, **kwargs) -> bytes:
"""Attempt to decrypt the given bytes using any of the available master keys.

Args:
data: Bytes of DPAPI blob to decrypt.
**kwargs: Arbitrary named arguments to pass to :meth:`DPAPIBlob.decrypt <dissect.target.plugins.os.windows.dpapi.blob.Blob.decrypt>` function.

Raises:
ValueError: When conditions to decrypt are not met or if decrypting failed.

Returns:
Decrypted bytes.
""" # noqa: E501
try:
blob = DPAPIBlob(data)
except EOFError as e:
raise ValueError(f"Failed to parse DPAPI blob: {e}")

for user in self.master_keys:
for mk in self.master_keys[user].values():
if blob.decrypt(mk.key):
if blob.decrypt(mk.key, **kwargs):

Check warning on line 220 in dissect/target/plugins/os/windows/dpapi/dpapi.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/windows/dpapi/dpapi.py#L220

Added line #L220 was not covered by tests
return blob.clear_text

raise ValueError("Failed to decrypt blob using any available master key")
25 changes: 17 additions & 8 deletions dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,20 @@

@export(output="yield")
def keys(self) -> Iterator[tuple[str, str]]:
"""Yield Windows LSA DefaultPassword strings."""
if default_pass := self.target.lsa._secrets.get("DefaultPassword"):
try:
value = c_defaultpassword.DefaultPassword(default_pass).data
except Exception:
self.target.log.warning("Failed to parse LSA DefaultPassword value")
return
yield self.__namespace__, value
"""Yield Windows LSA DefaultPassword strings.

Currently extracts decrypted ``DefaultPassword`` values from LSA.
Does not yet parse ``HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\DefaultPassword``.

Resources:
- https://learn.microsoft.com/en-us/troubleshoot/windows-server/user-profiles-and-logon/turn-on-automatic-logon
""" # noqa: E501

for secret in ["DefaultPassword", "DefaultPassword_OldVal"]:
if default_pass := self.target.lsa._secrets.get(secret):
try:
value = c_defaultpassword.DefaultPassword(default_pass).data
except Exception:
self.target.log.warning("Failed to parse LSA %s value", secret)
continue

Check warning on line 49 in dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py#L47-L49

Added lines #L47 - L49 were not covered by tests
yield self.__namespace__, value
Loading