diff --git a/dissect/target/plugins/os/windows/credential/credhist.py b/dissect/target/plugins/os/windows/credential/credhist.py index d81bb822c0..b8db580911 100644 --- a/dissect/target/plugins/os/windows/credential/credhist.py +++ b/dissect/target/plugins/os/windows/credential/credhist.py @@ -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: @@ -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: diff --git a/dissect/target/plugins/os/windows/credential/lsa.py b/dissect/target/plugins/os/windows/credential/lsa.py index 474b7c621b..3048cb69fd 100644 --- a/dissect/target/plugins/os/windows/credential/lsa.py +++ b/dissect/target/plugins/os/windows/credential/lsa.py @@ -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" @@ -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 diff --git a/dissect/target/plugins/os/windows/dpapi/dpapi.py b/dissect/target/plugins/os/windows/dpapi/dpapi.py index f835bff1d6..4a1237ced1 100644 --- a/dissect/target/plugins/os/windows/dpapi/dpapi.py +++ b/dissect/target/plugins/os/windows/dpapi/dpapi.py @@ -144,12 +144,36 @@ def _users(self) -> dict[str, str]: """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 ` 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 ` 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") @@ -168,13 +192,24 @@ def decrypt_user_blob(self, data: bytes, username: str | None = None, sid: str | 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 ` 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: @@ -182,7 +217,7 @@ def decrypt_blob(self, data: bytes) -> bytes: 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): return blob.clear_text raise ValueError("Failed to decrypt blob using any available master key") diff --git a/dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py b/dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py index 65fe81f458..5e285174c7 100644 --- a/dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +++ b/dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py @@ -31,11 +31,20 @@ def check_compatible(self) -> None: @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 + yield self.__namespace__, value