dissect.target 3.17.dev34__py3-none-any.whl → 3.17.dev36__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,6 +18,7 @@ from dissect.target.plugin import export
18
18
  from dissect.target.plugins.apps.browser.browser import (
19
19
  GENERIC_COOKIE_FIELDS,
20
20
  GENERIC_DOWNLOAD_RECORD_FIELDS,
21
+ GENERIC_EXTENSION_RECORD_FIELDS,
21
22
  GENERIC_HISTORY_RECORD_FIELDS,
22
23
  GENERIC_PASSWORD_RECORD_FIELDS,
23
24
  BrowserPlugin,
@@ -43,6 +44,7 @@ try:
43
44
  except ImportError:
44
45
  HAS_CRYPTO = False
45
46
 
47
+ FIREFOX_EXTENSION_RECORD_FIELDS = [("uri", "source_uri"), ("string[]", "optional_permissions")]
46
48
 
47
49
  log = logging.getLogger(__name__)
48
50
 
@@ -76,6 +78,10 @@ class FirefoxPlugin(BrowserPlugin):
76
78
  "browser/firefox/download", GENERIC_DOWNLOAD_RECORD_FIELDS
77
79
  )
78
80
 
81
+ BrowserExtensionRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
82
+ "browser/firefox/extension", GENERIC_EXTENSION_RECORD_FIELDS + FIREFOX_EXTENSION_RECORD_FIELDS
83
+ )
84
+
79
85
  BrowserPasswordRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
80
86
  "browser/firefox/password", GENERIC_PASSWORD_RECORD_FIELDS
81
87
  )
@@ -305,6 +311,72 @@ class FirefoxPlugin(BrowserPlugin):
305
311
  except SQLError as e:
306
312
  self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)
307
313
 
314
+ @export(record=BrowserExtensionRecord)
315
+ def extensions(self) -> Iterator[BrowserExtensionRecord]:
316
+ """Return browser extension records for Firefox.
317
+
318
+ Yields BrowserExtensionRecord with the following fields::
319
+ ts_install (datetime): Extension install timestamp.
320
+ ts_update (datetime): Extension update timestamp.
321
+ browser (string): The browser from which the records are generated.
322
+ id (string): Extension unique identifier.
323
+ name (string): Name of the extension.
324
+ short_name (string): Short name of the extension.
325
+ default_title (string): Default title of the extension.
326
+ description (string): Description of the extension.
327
+ version (string): Version of the extension.
328
+ ext_path (path): Relative path of the extension.
329
+ from_webstore (boolean): Extension from webstore.
330
+ permissions (string[]): Permissions of the extension.
331
+ manifest (varint): Version of the extensions' manifest.
332
+ optional_permissions (string[]): Optional permissions of the extension.
333
+ source_uri (path): Source path from which the extension was downloaded.
334
+ source (path): The source file of the download record.
335
+ """
336
+ for user, _, profile_dir in self._iter_profiles():
337
+ extension_file = profile_dir.joinpath("extensions.json")
338
+
339
+ if not extension_file.exists():
340
+ self.target.log.warning(
341
+ "No 'extensions.json' addon file found for user %s in directory %s", user, profile_dir
342
+ )
343
+ continue
344
+
345
+ try:
346
+ extensions = json.load(extension_file.open())
347
+
348
+ for extension in extensions.get("addons", []):
349
+ yield self.BrowserExtensionRecord(
350
+ ts_install=extension.get("installDate", 0) // 1000,
351
+ ts_update=extension.get("updateDate", 0) // 1000,
352
+ browser="firefox",
353
+ id=extension.get("id"),
354
+ name=extension.get("defaultLocale", {}).get("name"),
355
+ short_name=None,
356
+ default_title=None,
357
+ description=extension.get("defaultLocale", {}).get("description"),
358
+ version=extension.get("version"),
359
+ ext_path=extension.get("path"),
360
+ from_webstore=None,
361
+ permissions=extension.get("userPermissions", {}).get("permissions"),
362
+ manifest_version=extension.get("manifestVersion"),
363
+ source_uri=extension.get("sourceURI"),
364
+ optional_permissions=extension.get("optionalPermissions", {}).get("permissions"),
365
+ source=extension_file,
366
+ _target=self.target,
367
+ _user=user.user,
368
+ )
369
+
370
+ except FileNotFoundError:
371
+ self.target.log.info(
372
+ "No 'extensions.json' addon file found for user %s in directory %s", user, profile_dir
373
+ )
374
+ except json.JSONDecodeError:
375
+ self.target.log.warning(
376
+ "extensions.json file in directory %s is malformed, consider inspecting the file manually",
377
+ profile_dir,
378
+ )
379
+
308
380
  @export(record=BrowserPasswordRecord)
309
381
  def passwords(self) -> Iterator[BrowserPasswordRecord]:
310
382
  """Return Firefox browser password records.
@@ -0,0 +1,210 @@
1
+ import hashlib
2
+ import logging
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import BinaryIO, Iterator, Optional
6
+ from uuid import UUID
7
+
8
+ from dissect.cstruct import cstruct
9
+ from dissect.util.sid import read_sid
10
+
11
+ from dissect.target.exceptions import UnsupportedPluginError
12
+ from dissect.target.helpers import keychain
13
+ from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
14
+ from dissect.target.helpers.record import create_extended_descriptor
15
+ from dissect.target.plugin import Plugin, export
16
+ from dissect.target.plugins.general.users import UserDetails
17
+ from dissect.target.plugins.os.windows.dpapi.crypto import (
18
+ CipherAlgorithm,
19
+ HashAlgorithm,
20
+ derive_password_hash,
21
+ )
22
+ from dissect.target.target import Target
23
+
24
+ log = logging.getLogger(__name__)
25
+
26
+
27
+ CredHistRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
28
+ "windows/credential/history",
29
+ [
30
+ ("string", "guid"),
31
+ ("boolean", "decrypted"),
32
+ ("string", "sha1"),
33
+ ("string", "nt"),
34
+ ],
35
+ )
36
+
37
+
38
+ credhist_def = """
39
+ struct entry {
40
+ DWORD dwVersion;
41
+ CHAR guidLink[16];
42
+ DWORD dwNextLinkSize;
43
+ DWORD dwCredLinkType;
44
+ DWORD algHash; // ALG_ID
45
+ DWORD dwPbkdf2IterationCount;
46
+ DWORD dwSidSize;
47
+ DWORD algCrypt; // ALG_ID
48
+ DWORD dwShaHashSize;
49
+ DWORD dwNtHashSize;
50
+ CHAR pSalt[16];
51
+ CHAR pSid[dwSidSize];
52
+ CHAR encrypted[0];
53
+ };
54
+ """
55
+
56
+ c_credhist = cstruct()
57
+ c_credhist.load(credhist_def)
58
+
59
+
60
+ @dataclass
61
+ class CredHistEntry:
62
+ version: int
63
+ guid: str
64
+ user_sid: str
65
+ sha1: Optional[bytes]
66
+ nt: Optional[bytes]
67
+ hash_alg: HashAlgorithm = field(repr=False)
68
+ cipher_alg: CipherAlgorithm = field(repr=False)
69
+ raw: c_credhist.entry = field(repr=False)
70
+ decrypted: bool = False
71
+
72
+ def decrypt(self, password_hash: bytes) -> None:
73
+ """Decrypt this CREDHIST entry using the provided password hash. Modifies ``CredHistEntry.sha1``
74
+ and ``CredHistEntry.nt`` values.
75
+
76
+ If the decrypted ``nt`` value is 16 bytes we assume the decryption was successful.
77
+
78
+ Args:
79
+ password_hash: Bytes of SHA1 password hash digest.
80
+
81
+ Raises:
82
+ ValueError: If the decryption seems to have failed.
83
+ """
84
+ data = self.cipher_alg.decrypt_with_hmac(
85
+ data=self.raw.encrypted,
86
+ key=derive_password_hash(password_hash, self.user_sid),
87
+ iv=self.raw.pSalt,
88
+ hash_algorithm=self.hash_alg,
89
+ rounds=self.raw.dwPbkdf2IterationCount,
90
+ )
91
+
92
+ sha_size = self.raw.dwShaHashSize
93
+ nt_size = self.raw.dwNtHashSize
94
+
95
+ sha1 = data[:sha_size]
96
+ nt = data[sha_size : sha_size + nt_size].rstrip(b"\x00")
97
+
98
+ if len(nt) != 16:
99
+ raise ValueError("Decrypting failed, invalid password hash?")
100
+
101
+ self.decrypted = True
102
+ self.sha1 = sha1
103
+ self.nt = nt
104
+
105
+
106
+ class CredHistFile:
107
+ def __init__(self, fh: BinaryIO) -> None:
108
+ self.fh = fh
109
+ self.entries = list(self._parse())
110
+
111
+ def __repr__(self) -> str:
112
+ return f"<CredHistFile fh='{self.fh}' entries={len(self.entries)}>"
113
+
114
+ def _parse(self) -> Iterator[CredHistEntry]:
115
+ self.fh.seek(0)
116
+ try:
117
+ while True:
118
+ entry = c_credhist.entry(self.fh)
119
+
120
+ # determine size of encrypted data and add to entry
121
+ cipher_alg = CipherAlgorithm.from_id(entry.algCrypt)
122
+ enc_size = entry.dwShaHashSize + entry.dwNtHashSize
123
+ enc_size += enc_size % cipher_alg.block_length
124
+ entry.encrypted = self.fh.read(enc_size)
125
+
126
+ yield CredHistEntry(
127
+ version=entry.dwVersion,
128
+ guid=UUID(bytes_le=entry.guidLink),
129
+ user_sid=read_sid(entry.pSid),
130
+ hash_alg=HashAlgorithm.from_id(entry.algHash),
131
+ cipher_alg=cipher_alg,
132
+ sha1=None,
133
+ nt=None,
134
+ raw=entry,
135
+ )
136
+ except EOFError:
137
+ pass
138
+
139
+ def decrypt(self, password_hash: bytes) -> None:
140
+ """Decrypt a CREDHIST chain using the provided password SHA1 hash."""
141
+
142
+ for entry in reversed(self.entries):
143
+ try:
144
+ entry.decrypt(password_hash)
145
+ except ValueError as e:
146
+ log.warning("Could not decrypt entry %s with password %s", entry.guid, password_hash.hex())
147
+ log.debug("", exc_info=e)
148
+ continue
149
+ password_hash = entry.sha1
150
+
151
+
152
+ class CredHistPlugin(Plugin):
153
+ """Windows CREDHIST file parser.
154
+
155
+ Windows XP: ``C:\\Documents and Settings\\username\\Application Data\\Microsoft\\Protect\\CREDHIST``
156
+ Windows 7 and up: ``C:\\Users\\username\\AppData\\Roaming\\Microsoft\\Protect\\CREDHIST``
157
+
158
+ Resources:
159
+ - https://www.passcape.com/index.php?section=docsys&cmd=details&id=28#41
160
+ """
161
+
162
+ def __init__(self, target: Target):
163
+ super().__init__(target)
164
+ self.files = list(self._find_files())
165
+
166
+ def _find_files(self) -> Iterator[tuple[UserDetails, Path]]:
167
+ hashes = set()
168
+ for user_details in self.target.user_details.all_with_home():
169
+ for path in ["AppData/Roaming/Microsoft/Protect/CREDHIST", "Application Data/Microsoft/Protect/CREDHIST"]:
170
+ credhist_path = user_details.home_path.joinpath(path)
171
+ if credhist_path.exists() and (hash := credhist_path.get().hash()) not in hashes:
172
+ hashes.add(hash)
173
+ yield user_details.user, credhist_path
174
+
175
+ def check_compatible(self) -> None:
176
+ if not self.files:
177
+ raise UnsupportedPluginError("No CREDHIST files found on target.")
178
+
179
+ @export(record=CredHistRecord)
180
+ def credhist(self) -> Iterator[CredHistRecord]:
181
+ """Yield and decrypt all Windows CREDHIST entries on the target."""
182
+ passwords = keychain_passwords()
183
+
184
+ if not passwords:
185
+ self.target.log.warning("No passwords provided in keychain, cannot decrypt CREDHIST hashes")
186
+
187
+ for user, path in self.files:
188
+ credhist = CredHistFile(path.open("rb"))
189
+
190
+ for password in passwords:
191
+ credhist.decrypt(hashlib.sha1(password.encode("utf-16-le")).digest())
192
+
193
+ for entry in credhist.entries:
194
+ yield CredHistRecord(
195
+ guid=entry.guid,
196
+ decrypted=entry.decrypted,
197
+ sha1=entry.sha1.hex() if entry.sha1 else None,
198
+ nt=entry.nt.hex() if entry.nt else None,
199
+ _user=user,
200
+ _target=self.target,
201
+ )
202
+
203
+
204
+ def keychain_passwords() -> set:
205
+ passphrases = set()
206
+ for key in keychain.get_keys_for_provider("user") + keychain.get_keys_without_provider():
207
+ if key.key_type == keychain.KeyType.PASSPHRASE:
208
+ passphrases.add(key.value)
209
+ passphrases.add("")
210
+ return passphrases
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.17.dev34
3
+ Version: 3.17.dev36
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -124,7 +124,7 @@ dissect/target/plugins/apps/browser/browser.py,sha256=rBIwcgdl73gm-8APwx2jEUAYXR
124
124
  dissect/target/plugins/apps/browser/chrome.py,sha256=hxS8gqpBwoCrPaxNpllIa6K9DtsSGzn6XXcUaHyes6w,3048
125
125
  dissect/target/plugins/apps/browser/chromium.py,sha256=1oaQhMN5mJysw0VIVpTEmRCAifgv-mUQxZwrGmGHqAQ,27875
126
126
  dissect/target/plugins/apps/browser/edge.py,sha256=woXzZtHPWmfcV8vbxGKHELKru5JRb32MAXs43_b4K4E,2883
127
- dissect/target/plugins/apps/browser/firefox.py,sha256=Y8QdSgPZktYy4IF36aI1Jfbw_ucysx82PNljnyUCmRY,27025
127
+ dissect/target/plugins/apps/browser/firefox.py,sha256=Msicw-13AJWbXRRF6m_p4L84rXAjsIYGFRve29cPY2M,30806
128
128
  dissect/target/plugins/apps/browser/iexplore.py,sha256=MqMonoaM5lj0ZFqGwS4F-P1eLmnLvX7VQGE9S3hxXag,8739
129
129
  dissect/target/plugins/apps/container/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
130
130
  dissect/target/plugins/apps/container/docker.py,sha256=67Eih9AfUbqsP-HlnlwoHi4rSAnVCZWM76sEyO_1m18,15316
@@ -260,6 +260,7 @@ dissect/target/plugins/os/windows/amcache.py,sha256=ZZNOs3bILTf0AGkDkhoatndl0j39
260
260
  dissect/target/plugins/os/windows/catroot.py,sha256=eSfVqXvWWZpXoxKB1FT_evjXXNmlD7wHhA3lYpfQDeQ,11043
261
261
  dissect/target/plugins/os/windows/cim.py,sha256=jsrpu6TZpBUh7VWI9AV2Ib5bebTwsvqOwRfa5gjJd7c,3056
262
262
  dissect/target/plugins/os/windows/clfs.py,sha256=begVsZ-CY97Ksh6S1g03LjyBgu8ERY2hfNDWYPj0GXI,4872
263
+ dissect/target/plugins/os/windows/credhist.py,sha256=FX_pW-tU9esdvDTSx913kf_CpGE_1jbD6bkjDb-cxHk,7069
263
264
  dissect/target/plugins/os/windows/datetime.py,sha256=tuBOkewmbCW8sFXcYp5p82oM5RCsVwmtC79BDCTLz8k,9472
264
265
  dissect/target/plugins/os/windows/defender.py,sha256=Vp_IP6YKm4igR765WvXJrHQ3RMu7FJKM3VOoR8AybV8,23737
265
266
  dissect/target/plugins/os/windows/env.py,sha256=-u9F9xWy6PUbQmu5Tv_MDoVmy6YB-7CbHokIK_T3S44,13891
@@ -339,10 +340,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
339
340
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
340
341
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
341
342
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
342
- dissect.target-3.17.dev34.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
343
- dissect.target-3.17.dev34.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
344
- dissect.target-3.17.dev34.dist-info/METADATA,sha256=dCuOpFpGY7DjCc27MwZjfgtTnPx1iobAUR1GrzbpOZI,11300
345
- dissect.target-3.17.dev34.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
346
- dissect.target-3.17.dev34.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
347
- dissect.target-3.17.dev34.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
348
- dissect.target-3.17.dev34.dist-info/RECORD,,
343
+ dissect.target-3.17.dev36.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
344
+ dissect.target-3.17.dev36.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
345
+ dissect.target-3.17.dev36.dist-info/METADATA,sha256=jvQ5nLWplAsNNZMyR1nIBwBQZEe-FnQRkk56_YpDAtw,11300
346
+ dissect.target-3.17.dev36.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
347
+ dissect.target-3.17.dev36.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
348
+ dissect.target-3.17.dev36.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
349
+ dissect.target-3.17.dev36.dist-info/RECORD,,